github.com/storacha/go-ucanto@v0.7.2/core/receipt/receipt_test.go (about) 1 package receipt 2 3 import ( 4 "fmt" 5 "io" 6 "slices" 7 "testing" 8 9 ipldprime "github.com/ipld/go-ipld-prime" 10 "github.com/ipld/go-ipld-prime/schema" 11 "github.com/storacha/go-ucanto/core/dag/blockstore" 12 "github.com/storacha/go-ucanto/core/delegation" 13 "github.com/storacha/go-ucanto/core/invocation" 14 "github.com/storacha/go-ucanto/core/ipld" 15 "github.com/storacha/go-ucanto/core/ipld/block" 16 "github.com/storacha/go-ucanto/core/receipt/fx" 17 "github.com/storacha/go-ucanto/core/receipt/ran" 18 "github.com/storacha/go-ucanto/core/result" 19 "github.com/storacha/go-ucanto/core/result/ok" 20 "github.com/storacha/go-ucanto/testing/fixtures" 21 "github.com/storacha/go-ucanto/testing/helpers" 22 "github.com/storacha/go-ucanto/ucan" 23 "github.com/stretchr/testify/assert" 24 "github.com/stretchr/testify/require" 25 ) 26 27 func TestEffects(t *testing.T) { 28 ran := ran.FromLink(helpers.RandomCID()) 29 out := result.Ok[ok.Unit, ipld.Builder](ok.Unit{}) 30 31 t.Run("as links", func(t *testing.T) { 32 f0 := fx.FromLink(helpers.RandomCID()) 33 f1 := fx.FromLink(helpers.RandomCID()) 34 j := fx.FromLink(helpers.RandomCID()) 35 36 receipt, err := Issue(fixtures.Alice, out, ran, WithFork(f0, f1), WithJoin(j)) 37 require.NoError(t, err) 38 39 effects := receipt.Fx() 40 require.True(t, slices.ContainsFunc(effects.Fork(), func(f fx.Effect) bool { 41 return f.Link().String() == f0.Link().String() 42 })) 43 require.True(t, slices.ContainsFunc(effects.Fork(), func(f fx.Effect) bool { 44 return f.Link().String() == f1.Link().String() 45 })) 46 require.Equal(t, effects.Join().Link(), j.Link()) 47 }) 48 49 t.Run("as invocations", func(t *testing.T) { 50 i0, err := invocation.Invoke( 51 fixtures.Alice, 52 fixtures.Bob, 53 ucan.NewCapability("fx/0", fixtures.Alice.DID().String(), ucan.NoCaveats{}), 54 ) 55 require.NoError(t, err) 56 i1, err := invocation.Invoke( 57 fixtures.Alice, 58 fixtures.Mallory, 59 ucan.NewCapability("fx/1", fixtures.Alice.DID().String(), ucan.NoCaveats{}), 60 ) 61 require.NoError(t, err) 62 i2, err := invocation.Invoke( 63 fixtures.Mallory, 64 fixtures.Bob, 65 ucan.NewCapability("fx/2", fixtures.Alice.DID().String(), ucan.NoCaveats{}), 66 ) 67 require.NoError(t, err) 68 69 f0 := fx.FromInvocation(i0) 70 f1 := fx.FromInvocation(i1) 71 j := fx.FromInvocation(i2) 72 73 receipt, err := Issue(fixtures.Alice, out, ran, WithFork(f0, f1), WithJoin(j)) 74 require.NoError(t, err) 75 76 effects := receipt.Fx() 77 require.True(t, slices.ContainsFunc(effects.Fork(), func(f fx.Effect) bool { 78 return f.Link().String() == f0.Link().String() 79 })) 80 require.True(t, slices.ContainsFunc(effects.Fork(), func(f fx.Effect) bool { 81 return f.Link().String() == f1.Link().String() 82 })) 83 require.Equal(t, effects.Join().Link(), j.Link()) 84 85 for _, effect := range effects.Fork() { 86 _, ok := effect.Invocation() 87 require.True(t, ok) 88 } 89 90 _, ok := effects.Join().Invocation() 91 require.True(t, ok) 92 }) 93 } 94 95 var someTS = mustLoadTS() 96 97 func mustLoadTS() *schema.TypeSystem { 98 someSchema := []byte(` 99 type someOkType struct { 100 someOkProperty String 101 } 102 103 type someErrorType struct { 104 someErrorProperty String 105 } 106 `) 107 ts, err := ipldprime.LoadSchemaBytes(someSchema) 108 if err != nil { 109 panic(fmt.Errorf("loading some schema: %w", err)) 110 } 111 112 return ts 113 } 114 115 type someOkType struct { 116 SomeOkProperty string 117 } 118 119 func (s someOkType) ToIPLD() (ipld.Node, error) { 120 return ipld.WrapWithRecovery(&s, someTS.TypeByName("someOkType")) 121 } 122 123 type someErrorType struct { 124 SomeErrorProperty string 125 } 126 127 func (s someErrorType) ToIPLD() (ipld.Node, error) { 128 return ipld.WrapWithRecovery(&s, someTS.TypeByName("someErrorType")) 129 } 130 131 func TestIssue(t *testing.T) { 132 t.Run("ran as invocation", func(t *testing.T) { 133 inv, err := invocation.Invoke( 134 fixtures.Alice, 135 fixtures.Bob, 136 ucan.NewCapability("ran/invoke", fixtures.Alice.DID().String(), ucan.NoCaveats{}), 137 ) 138 require.NoError(t, err) 139 ran := ran.FromInvocation(inv) 140 141 out := result.Ok[someOkType, someErrorType](someOkType{SomeOkProperty: "some ok value"}) 142 143 issuedRcpt, err := Issue(fixtures.Alice, out, ran) 144 require.NoError(t, err) 145 146 ranInv, ok := issuedRcpt.Ran().Invocation() 147 require.True(t, ok) 148 require.Equal(t, inv.Link().String(), ranInv.Link().String()) 149 }) 150 151 t.Run("ran as link", func(t *testing.T) { 152 inv, err := invocation.Invoke( 153 fixtures.Alice, 154 fixtures.Bob, 155 ucan.NewCapability("ran/invoke", fixtures.Alice.DID().String(), ucan.NoCaveats{}), 156 ) 157 require.NoError(t, err) 158 ran := ran.FromLink(inv.Link()) 159 160 out := result.Ok[someOkType, someErrorType](someOkType{SomeOkProperty: "some ok value"}) 161 162 issuedRcpt, err := Issue(fixtures.Alice, out, ran) 163 require.NoError(t, err) 164 165 ranInv, ok := issuedRcpt.Ran().Invocation() 166 require.False(t, ok) 167 require.Nil(t, ranInv) 168 169 ranInvLink := issuedRcpt.Ran().Link() 170 require.NotNil(t, ranInvLink) 171 require.Equal(t, inv.Link().String(), ranInvLink.String()) 172 }) 173 } 174 175 func TestVerifySignature(t *testing.T) { 176 // Setup test data 177 r := ran.FromLink(helpers.RandomCID()) 178 out := result.Ok[someOkType, someErrorType](someOkType{SomeOkProperty: "foo"}) 179 180 // Create a valid receipt 181 rcpt, err := Issue(fixtures.Alice, out, r) 182 require.NoError(t, err) 183 184 t.Run("verifies valid signature", func(t *testing.T) { 185 valid, err := rcpt.VerifySignature(fixtures.Alice.Verifier()) 186 require.NoError(t, err) 187 require.True(t, valid) 188 }) 189 190 t.Run("fails with wrong verifier", func(t *testing.T) { 191 valid, err := rcpt.VerifySignature(fixtures.Bob.Verifier()) 192 require.NoError(t, err) 193 require.False(t, valid) 194 }) 195 196 t.Run("fails with tampered receipt", func(t *testing.T) { 197 // Create a new receipt with the same data but different signature 198 someRcpt, err := Rebind[someOkType, someErrorType](rcpt, someTS.TypeByName("someOkType"), someTS.TypeByName("someErrorType")) 199 require.NoError(t, err) 200 201 tamperedRcpt, ok := someRcpt.(*receipt[someOkType, someErrorType]) 202 require.True(t, ok) 203 204 // Tamper with the receipt data 205 // SomeOkProperty was "foo" in the original receipt 206 assert.Equal(t, tamperedRcpt.data.Ocm.Out.Ok, &someOkType{SomeOkProperty: "foo"}) 207 tamperedRcpt.data.Ocm.Out.Ok = &someOkType{SomeOkProperty: "bar"} 208 209 // Should fail with original verifier 210 valid, err := tamperedRcpt.VerifySignature(fixtures.Alice.Verifier()) 211 require.NoError(t, err) 212 require.False(t, valid) 213 }) 214 } 215 216 func TestArchiveExtract(t *testing.T) { 217 prf, err := delegation.Delegate( 218 fixtures.Alice, 219 fixtures.Bob, 220 []ucan.Capability[ucan.NoCaveats]{ 221 ucan.NewCapability("test/proof", fixtures.Alice.DID().String(), ucan.NoCaveats{}), 222 }, 223 ) 224 require.NoError(t, err) 225 226 inv, err := invocation.Invoke( 227 fixtures.Alice, 228 fixtures.Bob, 229 ucan.NewCapability("test/attach", fixtures.Alice.DID().String(), ucan.NoCaveats{}), 230 ) 231 require.NoError(t, err) 232 233 ran := ran.FromInvocation(inv) 234 ok := someOkType{SomeOkProperty: "some ok value"} 235 rcpt, err := Issue( 236 fixtures.Alice, 237 result.Ok[someOkType, someErrorType](ok), 238 ran, 239 WithProofs(delegation.Proofs{ 240 delegation.FromDelegation(prf), 241 // include an absent proof to prove things don't break - PUN INTENDED 242 delegation.FromLink(helpers.RandomCID()), 243 }), 244 ) 245 require.NoError(t, err) 246 247 archive := rcpt.Archive() 248 249 archiveBytes, err := io.ReadAll(archive) 250 require.NoError(t, err) 251 extracted, err := Extract(archiveBytes) 252 require.NoError(t, err) 253 254 var rcptBlks []ipld.Block 255 for b, err := range rcpt.Export() { 256 require.NoError(t, err) 257 rcptBlks = append(rcptBlks, b) 258 } 259 260 var extractedBlks []ipld.Block 261 for b, err := range extracted.Export() { 262 require.NoError(t, err) 263 extractedBlks = append(extractedBlks, b) 264 } 265 266 require.Equal(t, len(rcptBlks), len(extractedBlks)) 267 for i, b := range rcptBlks { 268 require.Equal(t, b.Link().String(), extractedBlks[i].Link().String()) 269 } 270 } 271 272 func TestExport(t *testing.T) { 273 prf, err := delegation.Delegate( 274 fixtures.Alice, 275 fixtures.Bob, 276 []ucan.Capability[ucan.NoCaveats]{ 277 ucan.NewCapability("test/proof", fixtures.Alice.DID().String(), ucan.NoCaveats{}), 278 }, 279 ) 280 require.NoError(t, err) 281 282 inv, err := invocation.Invoke( 283 fixtures.Alice, 284 fixtures.Bob, 285 ucan.NewCapability("test/export", fixtures.Alice.DID().String(), ucan.NoCaveats{}), 286 ) 287 require.NoError(t, err) 288 289 forkFx := fx.FromInvocation( 290 helpers.Must( 291 invocation.Invoke( 292 fixtures.Alice, 293 fixtures.Bob, 294 ucan.NewCapability("test/fx/fork", fixtures.Alice.DID().String(), ucan.NoCaveats{}), 295 ), 296 ), 297 ) 298 299 joinFx := fx.FromInvocation( 300 helpers.Must( 301 invocation.Invoke( 302 fixtures.Alice, 303 fixtures.Bob, 304 ucan.NewCapability("test/fx/join", fixtures.Alice.DID().String(), ucan.NoCaveats{}), 305 ), 306 ), 307 ) 308 309 ran := ran.FromInvocation(inv) 310 ok := someOkType{SomeOkProperty: "some ok value"} 311 rcpt, err := Issue( 312 fixtures.Alice, 313 result.Ok[someOkType, someErrorType](ok), 314 ran, 315 WithFork(forkFx, fx.FromLink(helpers.RandomCID())), 316 WithJoin(joinFx), 317 WithProofs(delegation.Proofs{ 318 delegation.FromDelegation(prf), 319 // include an absent proof to prove things don't break - PUN INTENDED 320 delegation.FromLink(helpers.RandomCID()), 321 }), 322 ) 323 require.NoError(t, err) 324 325 bs, err := blockstore.NewBlockStore() 326 require.NoError(t, err) 327 328 var blks []ipld.Block 329 for b, err := range rcpt.Blocks() { 330 require.NoError(t, err) 331 require.NoError(t, bs.Put(b)) 332 blks = append(blks, b) 333 } 334 require.Len(t, blks, 5) 335 require.True(t, slices.ContainsFunc(blks, func(b ipld.Block) bool { 336 return b.Link().String() == prf.Link().String() 337 })) 338 require.True(t, slices.ContainsFunc(blks, func(b ipld.Block) bool { 339 return b.Link().String() == inv.Link().String() 340 })) 341 require.True(t, slices.ContainsFunc(blks, func(b ipld.Block) bool { 342 return b.Link().String() == rcpt.Root().Link().String() 343 })) 344 345 // add an additional block to the blockstore that is not linked to by the receipt 346 otherblk := block.NewBlock(helpers.RandomCID(), helpers.RandomBytes(32)) 347 err = bs.Put(otherblk) 348 require.NoError(t, err) 349 350 // reinstantiate receipt with our new blockstore 351 rcpt, err = NewAnyReceipt(rcpt.Root().Link(), bs) 352 require.NoError(t, err) 353 354 var exblks []ipld.Block 355 // export the receipt from the blockstore 356 for b, err := range rcpt.Export() { 357 require.NoError(t, err) 358 exblks = append(exblks, b) 359 } 360 361 // expect exblks to have the same blocks in the same order and it should not 362 // include otherblk 363 require.Len(t, exblks, len(blks)) 364 for i, b := range blks { 365 require.Equal(t, b.Link().String(), exblks[i].Link().String()) 366 } 367 368 // expect rcpt.Blocks() to include otherblk though... 369 var blklnks []string 370 for b, err := range rcpt.Blocks() { 371 require.NoError(t, err) 372 blklnks = append(blklnks, b.Link().String()) 373 } 374 require.Contains(t, blklnks, otherblk.Link().String()) 375 } 376 377 func TestAttachInvocation(t *testing.T) { 378 inv, err := invocation.Invoke( 379 fixtures.Alice, 380 fixtures.Bob, 381 ucan.NewCapability("ran/invoke", fixtures.Alice.DID().String(), ucan.NoCaveats{}), 382 ) 383 require.NoError(t, err) 384 385 out := result.Ok[someOkType, someErrorType](someOkType{SomeOkProperty: "some ok value"}) 386 387 t.Run("adds invocation to receipt without one", func(t *testing.T) { 388 issuedRcpt, err := Issue(fixtures.Alice, out, ran.FromLink(inv.Link())) 389 require.NoError(t, err) 390 391 ranInv, ok := issuedRcpt.Ran().Invocation() 392 require.False(t, ok) 393 require.Nil(t, ranInv) 394 395 err = issuedRcpt.AttachInvocation(inv) 396 require.NoError(t, err) 397 398 ranInv, ok = issuedRcpt.Ran().Invocation() 399 require.True(t, ok) 400 require.Equal(t, inv.Link().String(), ranInv.Link().String()) 401 }) 402 403 t.Run("doesn't fail if receipt already has invocation and invocations match", func(t *testing.T) { 404 issuedRcpt, err := Issue(fixtures.Alice, out, ran.FromInvocation(inv)) 405 require.NoError(t, err) 406 407 ranInv, ok := issuedRcpt.Ran().Invocation() 408 require.True(t, ok) 409 require.Equal(t, inv.Link().String(), ranInv.Link().String()) 410 411 err = issuedRcpt.AttachInvocation(inv) 412 require.NoError(t, err) 413 }) 414 415 t.Run("fails if receipt invocations don't match", func(t *testing.T) { 416 issuedRcpt, err := Issue(fixtures.Alice, out, ran.FromLink(inv.Link())) 417 require.NoError(t, err) 418 419 inv2, err := invocation.Invoke( 420 fixtures.Alice, 421 fixtures.Service, // previous invocation's audience is Bob 422 ucan.NewCapability("ran/invoke", fixtures.Alice.DID().String(), ucan.NoCaveats{}), 423 ) 424 require.NoError(t, err) 425 426 err = issuedRcpt.AttachInvocation(inv2) 427 require.Error(t, err) 428 }) 429 } 430 431 func TestClone(t *testing.T) { 432 inv, err := invocation.Invoke( 433 fixtures.Alice, 434 fixtures.Bob, 435 ucan.NewCapability("ran/invoke", fixtures.Alice.DID().String(), ucan.NoCaveats{}), 436 ) 437 require.NoError(t, err) 438 439 out := result.Ok[someOkType, someErrorType](someOkType{SomeOkProperty: "some ok value"}) 440 441 rcpt1, err := Issue(fixtures.Alice, out, ran.FromLink(inv.Link())) 442 require.NoError(t, err) 443 444 rcpt2, err := rcpt1.Clone() 445 require.NoError(t, err) 446 447 // attach an invocation to rcpt2 and confirm it doesn't affect rcpt1 448 err = rcpt2.AttachInvocation(inv) 449 require.NoError(t, err) 450 451 rcpt1NumBlocks := 0 452 for range rcpt1.Blocks() { 453 rcpt1NumBlocks++ 454 } 455 rcpt2NumBlocks := 0 456 for range rcpt2.Blocks() { 457 rcpt2NumBlocks++ 458 } 459 require.True(t, rcpt2NumBlocks > rcpt1NumBlocks) 460 } 461 462 func TestAnyReceiptReader(t *testing.T) { 463 ranInv, err := invocation.Invoke( 464 fixtures.Alice, 465 fixtures.Bob, 466 ucan.NewCapability("ran/invoke", fixtures.Alice.DID().String(), ucan.NoCaveats{}), 467 ) 468 require.NoError(t, err) 469 ran := ran.FromInvocation(ranInv) 470 471 out := result.Ok[someOkType, someErrorType](someOkType{SomeOkProperty: "some ok value"}) 472 473 issuedRcpt, err := Issue(fixtures.Alice, out, ran) 474 require.NoError(t, err) 475 476 reader := NewAnyReceiptReader() 477 var anyRcpt AnyReceipt 478 anyRcpt, err = reader.Read(issuedRcpt.Root().Link(), issuedRcpt.Blocks()) 479 require.NoError(t, err) 480 481 concreteRcpt, err := Rebind[*someOkType, *someErrorType](anyRcpt, someTS.TypeByName("someOkType"), someTS.TypeByName("someErrorType")) 482 require.NoError(t, err) 483 484 someOk, someErr := result.Unwrap(concreteRcpt.Out()) 485 require.Equal(t, "some ok value", someOk.SomeOkProperty) 486 require.Nil(t, someErr) 487 }