From 49113fc75c67cb4f8cf42ab33b25e2eb35ea6bb8 Mon Sep 17 00:00:00 2001 From: Petra Jaros Date: Thu, 13 Nov 2025 13:22:26 -0500 Subject: [PATCH 1/2] feat: `testutil.RandomCID()` is a `cidlink.Link` This has always been a link and not an actual CID. Fixing the naming would mean a whole bunch of test code changes for little value, but we can at least make it return `cidlink.Link` explicitly and not `ipld.Link` generally, which means that test code no longer has to type-assert it to `cidlink.Link` to get the `cid.CID` from inside it. --- testutil/gen.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testutil/gen.go b/testutil/gen.go index f8e3243..62fbfae 100644 --- a/testutil/gen.go +++ b/testutil/gen.go @@ -75,7 +75,7 @@ func RandomMultiaddr(t *testing.T) multiaddr.Multiaddr { return multiaddr.Join(maddr, port) } -func RandomCID(t *testing.T) datamodel.Link { +func RandomCID(t *testing.T) cidlink.Link { return cidlink.Link{Cid: cid.NewCidV1(cid.Raw, RandomMultihash(t))} } From 0f8e6334a4e66b31a15f97861d9a7b68b942b8b0 Mon Sep 17 00:00:00 2001 From: Petra Jaros Date: Thu, 13 Nov 2025 14:45:37 -0500 Subject: [PATCH 2/2] fix: Deal with `testutil.RandomCID()` being a `cidlink.Link` --- advertisement/advertisement_test.go | 4 +- capabilities/blob/replica/transfer_test.go | 3 +- capabilities/space/content/retrieve.go | 26 ++-- capabilities/space/content/retrieve_test.go | 139 ++++++++++++++++++++ metadata/metadata_test.go | 15 +-- testutil/gen.go | 8 +- 6 files changed, 168 insertions(+), 27 deletions(-) diff --git a/advertisement/advertisement_test.go b/advertisement/advertisement_test.go index c3356bb..2986cd1 100644 --- a/advertisement/advertisement_test.go +++ b/advertisement/advertisement_test.go @@ -84,11 +84,11 @@ func TestShardCID(t *testing.T) { caveats: assert.LocationCaveats{ Content: types.FromHash(testMhs[0]), Location: []url.URL{ - *baseUrl.JoinPath("blob", testCid.String(), digestutil.Format(testCid.(cidlink.Link).Cid.Hash())), + *baseUrl.JoinPath("blob", testCid.String(), digestutil.Format(testCid.Cid.Hash())), }, }, expected: func() *cid.Cid { - cid := testCid.(cidlink.Link).Cid + cid := testCid.Cid return &cid }(), expectErr: false, diff --git a/capabilities/blob/replica/transfer_test.go b/capabilities/blob/replica/transfer_test.go index 04a40c6..6f5cb0e 100644 --- a/capabilities/blob/replica/transfer_test.go +++ b/capabilities/blob/replica/transfer_test.go @@ -3,6 +3,7 @@ package replica_test import ( "testing" + "github.com/ipld/go-ipld-prime" "github.com/stretchr/testify/require" "github.com/storacha/go-libstoracha/capabilities/blob/replica" @@ -36,7 +37,7 @@ func TestRoundTripTransferCaveats(t *testing.T) { func TestRoundTripTransfeOk(t *testing.T) { t.Run("with PDP link", func(t *testing.T) { expectedLocation := testutil.RandomCID(t) - expectedPDP := testutil.RandomCID(t) + var expectedPDP ipld.Link = testutil.RandomCID(t) expectedOk := replica.TransferOk{ Site: expectedLocation, diff --git a/capabilities/space/content/retrieve.go b/capabilities/space/content/retrieve.go index 7e94c29..a3ca2bb 100644 --- a/capabilities/space/content/retrieve.go +++ b/capabilities/space/content/retrieve.go @@ -137,20 +137,22 @@ var Retrieve = validator.NewCapability( RetrieveAbility, schema.DIDString(), RetrieveCaveatsReader, - func(claimed, delegated ucan.Capability[RetrieveCaveats]) failure.Failure { - fail := equalWith(claimed, delegated) - if fail != nil { - return fail - } + RetrieveDerive, +) - fail = equalDigest(claimed, delegated) - if fail != nil { - return fail - } +func RetrieveDerive(claimed, delegated ucan.Capability[RetrieveCaveats]) failure.Failure { + fail := equalWith(claimed, delegated) + if fail != nil { + return fail + } - return validRange(claimed, delegated) - }, -) + fail = equalDigest(claimed, delegated) + if fail != nil { + return fail + } + + return validRange(claimed, delegated) +} // equalWith validates that the claimed capability's `with` field matches the delegated one. func equalWith(claimed, delegated ucan.Capability[RetrieveCaveats]) failure.Failure { diff --git a/capabilities/space/content/retrieve_test.go b/capabilities/space/content/retrieve_test.go index aae50eb..4f77385 100644 --- a/capabilities/space/content/retrieve_test.go +++ b/capabilities/space/content/retrieve_test.go @@ -8,6 +8,7 @@ import ( "github.com/storacha/go-libstoracha/testutil" "github.com/storacha/go-ucanto/core/result/failure" fdm "github.com/storacha/go-ucanto/core/result/failure/datamodel" + "github.com/storacha/go-ucanto/ucan" "github.com/stretchr/testify/require" ) @@ -120,3 +121,141 @@ func TestReadInvalidRangeNotSatisfiableError(t *testing.T) { t.Log(err) }) } + +// TestRetrieveDerive tests the RetrieveDerive function in isolation +func TestRetrieveDerive(t *testing.T) { + digest := testutil.RandomMultihash(t) + spaceDID := "did:example:space" + + delegatedCaveats := content.RetrieveCaveats{ + Blob: content.BlobDigest{Digest: digest}, + Range: content.Range{ + Start: 0, + End: 1024, + }, + } + + delegated := ucan.NewCapability( + content.RetrieveAbility, + spaceDID, + delegatedCaveats, + ) + + t.Run("accepts an identical capability", func(t *testing.T) { + claimed := ucan.NewCapability( + delegated.Can(), + delegated.With(), + delegated.Nb(), + ) + + fail := content.RetrieveDerive(claimed, delegated) + require.NoError(t, fail) + }) + + t.Run("rejects the wrong space", func(t *testing.T) { + claimed := ucan.NewCapability( + delegated.Can(), + "did:example:different-space", + delegated.Nb(), + ) + + fail := content.RetrieveDerive(claimed, delegated) + require.ErrorContains(t, fail, "Resource 'did:example:different-space' doesn't match delegated 'did:example:space'") + }) + + t.Run("rejects a different blob digest", func(t *testing.T) { + differentDigest := testutil.RandomMultihash(t) + claimed := ucan.NewCapability( + delegated.Can(), + delegated.With(), + content.RetrieveCaveats{ + Blob: content.BlobDigest{Digest: differentDigest}, + Range: delegated.Nb().Range, + }, + ) + + fail := content.RetrieveDerive(claimed, delegated) + require.Error(t, fail) + require.ErrorContains(t, fail, "Digest") + }) + + t.Run("accepts a subset range", func(t *testing.T) { + claimed := ucan.NewCapability( + delegated.Can(), + delegated.With(), + content.RetrieveCaveats{ + Blob: delegated.Nb().Blob, + Range: content.Range{ + Start: 100, + End: 500, + }, + }, + ) + + fail := content.RetrieveDerive(claimed, delegated) + require.NoError(t, fail) + }) + + t.Run("rejects range exceeding upper bound", func(t *testing.T) { + claimed := ucan.NewCapability( + delegated.Can(), + delegated.With(), + content.RetrieveCaveats{ + Blob: delegated.Nb().Blob, + Range: content.Range{ + Start: 512, + End: 2048, // Exceeds delegated end of 1024 + }, + }, + ) + + fail := content.RetrieveDerive(claimed, delegated) + require.ErrorContains(t, fail, "End offset 2048 violates imposed 1024 constraint") + }) + + t.Run("rejects range below lower bound", func(t *testing.T) { + delegatedWithOffset := ucan.NewCapability( + content.RetrieveAbility, + spaceDID, + content.RetrieveCaveats{ + Blob: content.BlobDigest{Digest: digest}, + Range: content.Range{ + Start: 100, + End: 1024, + }, + }, + ) + + claimed := ucan.NewCapability( + delegatedWithOffset.Can(), + delegatedWithOffset.With(), + content.RetrieveCaveats{ + Blob: delegatedWithOffset.Nb().Blob, + Range: content.Range{ + Start: 0, // Starts before delegated start of 100 + End: 500, + }, + }, + ) + + fail := content.RetrieveDerive(claimed, delegatedWithOffset) + require.ErrorContains(t, fail, "Start offset 0 violates imposed 100 constraint") + }) + + t.Run("accepts range equal to bounds", func(t *testing.T) { + claimed := ucan.NewCapability( + delegated.Can(), + delegated.With(), + content.RetrieveCaveats{ + Blob: delegated.Nb().Blob, + Range: content.Range{ + Start: 0, + End: 1024, + }, + }, + ) + + fail := content.RetrieveDerive(claimed, delegated) + require.NoError(t, fail) + }) +} diff --git a/metadata/metadata_test.go b/metadata/metadata_test.go index 6339799..83c60e4 100644 --- a/metadata/metadata_test.go +++ b/metadata/metadata_test.go @@ -3,7 +3,6 @@ package metadata_test import ( "testing" - cidlink "github.com/ipld/go-ipld-prime/linking/cid" "github.com/storacha/go-libstoracha/metadata" "github.com/storacha/go-libstoracha/testutil" "github.com/stretchr/testify/require" @@ -11,8 +10,8 @@ import ( func TestRoundTripLocationCommitmentMetadata(t *testing.T) { t.Run("all fields", func(t *testing.T) { - claim := testutil.RandomCID(t).(cidlink.Link).Cid - shard := testutil.RandomCID(t).(cidlink.Link).Cid + claim := testutil.RandomCID(t).Cid + shard := testutil.RandomCID(t).Cid length := uint64(138) rng := metadata.Range{ Offset: 10, @@ -36,7 +35,7 @@ func TestRoundTripLocationCommitmentMetadata(t *testing.T) { }) t.Run("optional shard", func(t *testing.T) { - claim := testutil.RandomCID(t).(cidlink.Link).Cid + claim := testutil.RandomCID(t).Cid length := uint64(138) rng := metadata.Range{ Offset: 10, @@ -59,8 +58,8 @@ func TestRoundTripLocationCommitmentMetadata(t *testing.T) { }) t.Run("optional range", func(t *testing.T) { - claim := testutil.RandomCID(t).(cidlink.Link).Cid - shard := testutil.RandomCID(t).(cidlink.Link).Cid + claim := testutil.RandomCID(t).Cid + shard := testutil.RandomCID(t).Cid meta0 := metadata.LocationCommitmentMetadata{ Shard: &shard, Expiration: 1234, @@ -78,8 +77,8 @@ func TestRoundTripLocationCommitmentMetadata(t *testing.T) { }) t.Run("optional range length", func(t *testing.T) { - claim := testutil.RandomCID(t).(cidlink.Link).Cid - shard := testutil.RandomCID(t).(cidlink.Link).Cid + claim := testutil.RandomCID(t).Cid + shard := testutil.RandomCID(t).Cid rng := metadata.Range{Offset: 10} meta0 := metadata.LocationCommitmentMetadata{ Shard: &shard, diff --git a/testutil/gen.go b/testutil/gen.go index 62fbfae..7f62cae 100644 --- a/testutil/gen.go +++ b/testutil/gen.go @@ -211,9 +211,9 @@ func RandomBitswapProviderResult(t *testing.T) model.ProviderResult { func RandomIndexClaimProviderResult(t *testing.T) model.ProviderResult { indexMeta := metadata.IndexClaimMetadata{ - Index: RandomCID(t).(cidlink.Link).Cid, + Index: RandomCID(t).Cid, Expiration: 0, - Claim: RandomCID(t).(cidlink.Link).Cid, + Claim: RandomCID(t).Cid, } metaBytes := Must(indexMeta.MarshalBinary())(t) @@ -223,12 +223,12 @@ func RandomIndexClaimProviderResult(t *testing.T) model.ProviderResult { } func RandomLocationCommitmentProviderResult(t *testing.T) model.ProviderResult { - shard := RandomCID(t).(cidlink.Link).Cid + shard := RandomCID(t).Cid locationMeta := metadata.LocationCommitmentMetadata{ Shard: &shard, Range: &metadata.Range{Offset: 128}, Expiration: 0, - Claim: RandomCID(t).(cidlink.Link).Cid, + Claim: RandomCID(t).Cid, } metaBytes := Must(locationMeta.MarshalBinary())(t)