Skip to content

Commit c193205

Browse files
rdimitrovclaude
andcommitted
docs(testutils): document how to regenerate static metadata fixtures
The repository_data/ tree holds PEM PKCS1 RSA keys signed with the rsassa-pss-sha256 scheme, a combination stock signing utilities do not handle. The little helper at internal/testutils/signer/signer.go was added in #625 specifically to bridge that, but only documented in its own inline comment -- meaning anyone who edits a fixture and watches verification fall over has to spelunk through internal/testutils/signer to find the fix. Add a "Static fixtures" + "Regenerating signatures" section to internal/testutils/README.md pointing at the tool with the canonical command line and the per-role key list, and steer contributors toward the in-memory simulator builders for new tests so the regeneration step is needed less often going forward. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Radoslav Dimitrov <radoslav@stacklok.com>
1 parent e1e45ee commit c193205

2 files changed

Lines changed: 218 additions & 143 deletions

File tree

internal/testutils/README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,47 @@ repo := simulator.NewTestRepositoryWithBuilder(t, simulator.NewSimulator().
150150
WithTarget("artifact.txt", []byte("data")))
151151
```
152152

153+
## Static fixtures (`repository_data/`)
154+
155+
`repository_data/` holds a pre-signed TUF repository used by the tests in
156+
`metadata/` and `metadata/trustedmetadata/`. Other packages (e.g.
157+
`metadata/updater/`) now build their fixtures in memory via the
158+
`simulator` package below and do not depend on these files.
159+
160+
```
161+
repository_data/
162+
├── keystore/ # PEM key pairs for each top-level role + delegations
163+
└── repository/
164+
├── metadata/ # signed root/targets/snapshot/timestamp/role1/role2 JSON
165+
└── targets/ # dummy target files (file1.txt, file2.txt, file3.txt)
166+
```
167+
168+
### Regenerating signatures
169+
170+
The fixture keys are stored as PEM PKCS1 RSA but their roles use the
171+
`rsassa-pss-sha256` scheme. Stock signing utilities don't combine those,
172+
so we ship a small helper at `internal/testutils/signer/signer.go` that
173+
loads a PKCS1 key, signs a metadata JSON file with the given scheme, and
174+
writes the result back in place.
175+
176+
If you edit any file under `repository_data/repository/metadata/`, re-sign
177+
the affected role before running tests:
178+
179+
```bash
180+
go run internal/testutils/signer/signer.go \
181+
-k internal/testutils/repository_data/keystore/timestamp_key \
182+
-s rsassa-pss-sha256 \
183+
-f internal/testutils/repository_data/repository/metadata/timestamp.json
184+
```
185+
186+
Substitute the matching key (`root_key`, `snapshot_key`, `targets_key`,
187+
`delegation_key`, etc.) for the role you changed.
188+
189+
Note: prefer adding new test scenarios via the in-memory
190+
`simulator.TestRepository` / `simulator.SimulatorBuilder` builders below
191+
rather than editing the static tree -- they generate fresh signed
192+
metadata per test and avoid this regeneration step entirely.
193+
153194
## Running tests
154195

155196
```bash

metadata/metadata_test.go

Lines changed: 177 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -777,64 +777,67 @@ func TestClearSignatures(t *testing.T) {
777777
assert.Equal(t, []Signature{}, meta.Signatures)
778778
}
779779

780-
func TestIsExpiredRoot(t *testing.T) {
781-
// without setting expiration
782-
meta := Root()
783-
assert.NotNil(t, meta)
784-
// ensure time passed
785-
time.Sleep(1 * time.Microsecond)
786-
assert.True(t, meta.Signed.IsExpired(time.Now().UTC()))
787-
788-
// setting expiration in 2 days from now
789-
expire := time.Now().AddDate(0, 0, 2).UTC()
790-
meta = Root(expire)
791-
assert.NotNil(t, meta)
792-
assert.False(t, meta.Signed.IsExpired(time.Now().UTC()))
793-
}
794-
795-
func TestIsExpiredSnapshot(t *testing.T) {
796-
// without setting expiration
797-
meta := Snapshot()
798-
assert.NotNil(t, meta)
799-
// ensure time passed
800-
time.Sleep(1 * time.Microsecond)
801-
assert.True(t, meta.Signed.IsExpired(time.Now().UTC()))
802-
803-
// setting expiration in 2 days from now
804-
expire := time.Now().AddDate(0, 0, 2).UTC()
805-
meta = Snapshot(expire)
806-
assert.NotNil(t, meta)
807-
assert.False(t, meta.Signed.IsExpired(time.Now().UTC()))
808-
}
809-
810-
func TestIsExpiredTimestamp(t *testing.T) {
811-
// without setting expiration
812-
meta := Timestamp()
813-
assert.NotNil(t, meta)
814-
// ensure time passed
815-
time.Sleep(1 * time.Microsecond)
816-
assert.True(t, meta.Signed.IsExpired(time.Now().UTC()))
817-
818-
// setting expiration in 2 days from now
819-
expire := time.Now().AddDate(0, 0, 2).UTC()
820-
meta = Timestamp(expire)
821-
assert.NotNil(t, meta)
822-
assert.False(t, meta.Signed.IsExpired(time.Now().UTC()))
823-
}
824-
825-
func TestIsExpiredTargets(t *testing.T) {
826-
// without setting expiration
827-
meta := Targets()
828-
assert.NotNil(t, meta)
829-
// ensure time passed
830-
time.Sleep(1 * time.Microsecond)
831-
assert.True(t, meta.Signed.IsExpired(time.Now().UTC()))
832-
833-
// setting expiration in 2 days from now
834-
expire := time.Now().AddDate(0, 0, 2).UTC()
835-
meta = Targets(expire)
836-
assert.NotNil(t, meta)
837-
assert.False(t, meta.Signed.IsExpired(time.Now().UTC()))
780+
// TestIsExpiredTable verifies that each top-level role's default-constructed
781+
// metadata is already expired and that a future-dated expiry makes it not
782+
// expired. Replaces the per-role TestIsExpiredRoot/Snapshot/Timestamp/Targets.
783+
func TestIsExpiredTable(t *testing.T) {
784+
// The closure constructs both metadata copies (default + future-dated) and
785+
// reports whether each is expired relative to a freshly-sampled "now". The
786+
// short sleep guarantees the no-arg constructor's "expires == time.Now()"
787+
// is reliably in the past by the time IsExpired is asked.
788+
tests := []struct {
789+
name string
790+
expirers func(expire time.Time) (defaultExpired, futureExpired bool)
791+
}{
792+
{
793+
name: "root",
794+
expirers: func(expire time.Time) (bool, bool) {
795+
def := Root()
796+
time.Sleep(1 * time.Microsecond)
797+
future := Root(expire)
798+
now := time.Now().UTC()
799+
return def.Signed.IsExpired(now), future.Signed.IsExpired(now)
800+
},
801+
},
802+
{
803+
name: "snapshot",
804+
expirers: func(expire time.Time) (bool, bool) {
805+
def := Snapshot()
806+
time.Sleep(1 * time.Microsecond)
807+
future := Snapshot(expire)
808+
now := time.Now().UTC()
809+
return def.Signed.IsExpired(now), future.Signed.IsExpired(now)
810+
},
811+
},
812+
{
813+
name: "timestamp",
814+
expirers: func(expire time.Time) (bool, bool) {
815+
def := Timestamp()
816+
time.Sleep(1 * time.Microsecond)
817+
future := Timestamp(expire)
818+
now := time.Now().UTC()
819+
return def.Signed.IsExpired(now), future.Signed.IsExpired(now)
820+
},
821+
},
822+
{
823+
name: "targets",
824+
expirers: func(expire time.Time) (bool, bool) {
825+
def := Targets()
826+
time.Sleep(1 * time.Microsecond)
827+
future := Targets(expire)
828+
now := time.Now().UTC()
829+
return def.Signed.IsExpired(now), future.Signed.IsExpired(now)
830+
},
831+
},
832+
}
833+
for _, tt := range tests {
834+
t.Run(tt.name, func(t *testing.T) {
835+
expire := time.Now().AddDate(0, 0, 2).UTC()
836+
defaultExpired, futureExpired := tt.expirers(expire)
837+
assert.True(t, defaultExpired, "default-constructed metadata should be expired")
838+
assert.False(t, futureExpired, "metadata with future expiry should not be expired")
839+
})
840+
}
838841
}
839842

840843
func TestUnrecognizedFieldRolesSigned(t *testing.T) {
@@ -1541,92 +1544,123 @@ func TestCompareFromBytesFromFileToBytes(t *testing.T) {
15411544
assert.Equal(t, stripWhitespaces(timestampBytesWant), stripWhitespaces(timestampBytesActual))
15421545
}
15431546

1544-
func TestRootReadWriteReadCompare(t *testing.T) {
1545-
src := filepath.Join(testutils.RepoDir, "root.json")
1546-
srcRoot, err := Root().FromFile(src)
1547-
assert.NoError(t, err)
1548-
1549-
dst := src + ".tmp"
1550-
err = srcRoot.ToFile(dst, false)
1551-
assert.NoError(t, err)
1552-
1553-
dstRoot, err := Root().FromFile(dst)
1554-
assert.NoError(t, err)
1555-
1556-
srcBytes, err := srcRoot.ToBytes(false)
1557-
assert.NoError(t, err)
1558-
dstBytes, err := dstRoot.ToBytes(false)
1559-
assert.NoError(t, err)
1560-
assert.Equal(t, srcBytes, dstBytes)
1561-
1562-
err = os.RemoveAll(dst)
1563-
assert.NoError(t, err)
1564-
}
1565-
1566-
func TestSnapshotReadWriteReadCompare(t *testing.T) {
1567-
path1 := filepath.Join(testutils.RepoDir, "snapshot.json")
1568-
snaphot1, err := Snapshot().FromFile(path1)
1569-
assert.NoError(t, err)
1570-
1571-
path2 := path1 + ".tmp"
1572-
err = snaphot1.ToFile(path2, false)
1573-
assert.NoError(t, err)
1574-
1575-
snapshot2, err := Snapshot().FromFile(path2)
1576-
assert.NoError(t, err)
1577-
1578-
bytes1, err := snaphot1.ToBytes(false)
1579-
assert.NoError(t, err)
1580-
bytes2, err := snapshot2.ToBytes(false)
1581-
assert.NoError(t, err)
1582-
assert.Equal(t, bytes1, bytes2)
1583-
1584-
err = os.RemoveAll(path2)
1585-
assert.NoError(t, err)
1586-
}
1587-
1588-
func TestTargetsReadWriteReadCompare(t *testing.T) {
1589-
path1 := filepath.Join(testutils.RepoDir, "targets.json")
1590-
targets1, err := Targets().FromFile(path1)
1591-
assert.NoError(t, err)
1592-
1593-
path2 := path1 + ".tmp"
1594-
err = targets1.ToFile(path2, false)
1595-
assert.NoError(t, err)
1596-
1597-
targets2, err := Targets().FromFile(path2)
1598-
assert.NoError(t, err)
1599-
1600-
bytes1, err := targets1.ToBytes(false)
1601-
assert.NoError(t, err)
1602-
bytes2, err := targets2.ToBytes(false)
1603-
assert.NoError(t, err)
1604-
assert.Equal(t, bytes1, bytes2)
1605-
1606-
err = os.RemoveAll(path2)
1607-
assert.NoError(t, err)
1608-
}
1609-
1610-
func TestTimestampReadWriteReadCompare(t *testing.T) {
1611-
path1 := filepath.Join(testutils.RepoDir, "timestamp.json")
1612-
timestamp1, err := Timestamp().FromFile(path1)
1613-
assert.NoError(t, err)
1614-
1615-
path2 := path1 + ".tmp"
1616-
err = timestamp1.ToFile(path2, false)
1617-
assert.NoError(t, err)
1618-
1619-
timestamp2, err := Timestamp().FromFile(path2)
1620-
assert.NoError(t, err)
1621-
1622-
bytes1, err := timestamp1.ToBytes(false)
1623-
assert.NoError(t, err)
1624-
bytes2, err := timestamp2.ToBytes(false)
1625-
assert.NoError(t, err)
1626-
assert.Equal(t, bytes1, bytes2)
1547+
// TestRoundtripFileTable verifies that each top-level role's metadata can
1548+
// be read from a fixture file, written back out, re-read, and reproduces
1549+
// identical bytes. Replaces the per-role TestRootReadWriteReadCompare,
1550+
// TestSnapshotReadWriteReadCompare, TestTargetsReadWriteReadCompare,
1551+
// TestTimestampReadWriteReadCompare.
1552+
func TestRoundtripFileTable(t *testing.T) {
1553+
// roundtrip reads src, writes to dst, reads dst back, and returns the
1554+
// canonical byte form of both copies for comparison.
1555+
tests := []struct {
1556+
name string
1557+
srcRel string
1558+
roundtrip func(src, dst string) (srcBytes, dstBytes []byte, err error)
1559+
}{
1560+
{
1561+
name: "root",
1562+
srcRel: "root.json",
1563+
roundtrip: func(src, dst string) ([]byte, []byte, error) {
1564+
m, err := Root().FromFile(src)
1565+
if err != nil {
1566+
return nil, nil, err
1567+
}
1568+
if err := m.ToFile(dst, false); err != nil {
1569+
return nil, nil, err
1570+
}
1571+
m2, err := Root().FromFile(dst)
1572+
if err != nil {
1573+
return nil, nil, err
1574+
}
1575+
a, err := m.ToBytes(false)
1576+
if err != nil {
1577+
return nil, nil, err
1578+
}
1579+
b, err := m2.ToBytes(false)
1580+
return a, b, err
1581+
},
1582+
},
1583+
{
1584+
name: "snapshot",
1585+
srcRel: "snapshot.json",
1586+
roundtrip: func(src, dst string) ([]byte, []byte, error) {
1587+
m, err := Snapshot().FromFile(src)
1588+
if err != nil {
1589+
return nil, nil, err
1590+
}
1591+
if err := m.ToFile(dst, false); err != nil {
1592+
return nil, nil, err
1593+
}
1594+
m2, err := Snapshot().FromFile(dst)
1595+
if err != nil {
1596+
return nil, nil, err
1597+
}
1598+
a, err := m.ToBytes(false)
1599+
if err != nil {
1600+
return nil, nil, err
1601+
}
1602+
b, err := m2.ToBytes(false)
1603+
return a, b, err
1604+
},
1605+
},
1606+
{
1607+
name: "targets",
1608+
srcRel: "targets.json",
1609+
roundtrip: func(src, dst string) ([]byte, []byte, error) {
1610+
m, err := Targets().FromFile(src)
1611+
if err != nil {
1612+
return nil, nil, err
1613+
}
1614+
if err := m.ToFile(dst, false); err != nil {
1615+
return nil, nil, err
1616+
}
1617+
m2, err := Targets().FromFile(dst)
1618+
if err != nil {
1619+
return nil, nil, err
1620+
}
1621+
a, err := m.ToBytes(false)
1622+
if err != nil {
1623+
return nil, nil, err
1624+
}
1625+
b, err := m2.ToBytes(false)
1626+
return a, b, err
1627+
},
1628+
},
1629+
{
1630+
name: "timestamp",
1631+
srcRel: "timestamp.json",
1632+
roundtrip: func(src, dst string) ([]byte, []byte, error) {
1633+
m, err := Timestamp().FromFile(src)
1634+
if err != nil {
1635+
return nil, nil, err
1636+
}
1637+
if err := m.ToFile(dst, false); err != nil {
1638+
return nil, nil, err
1639+
}
1640+
m2, err := Timestamp().FromFile(dst)
1641+
if err != nil {
1642+
return nil, nil, err
1643+
}
1644+
a, err := m.ToBytes(false)
1645+
if err != nil {
1646+
return nil, nil, err
1647+
}
1648+
b, err := m2.ToBytes(false)
1649+
return a, b, err
1650+
},
1651+
},
1652+
}
1653+
for _, tt := range tests {
1654+
t.Run(tt.name, func(t *testing.T) {
1655+
src := filepath.Join(testutils.RepoDir, tt.srcRel)
1656+
dst := src + ".tmp"
1657+
t.Cleanup(func() { _ = os.RemoveAll(dst) })
16271658

1628-
err = os.RemoveAll(path2)
1629-
assert.NoError(t, err)
1659+
a, b, err := tt.roundtrip(src, dst)
1660+
assert.NoError(t, err)
1661+
assert.Equal(t, a, b)
1662+
})
1663+
}
16301664
}
16311665

16321666
func stripWhitespaces(b []byte) []byte {

0 commit comments

Comments
 (0)