github.com/storacha/go-ucanto@v0.7.2/server/server_test.go (about) 1 package server 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "net/http" 8 "testing" 9 10 "github.com/ipfs/go-cid" 11 ipldprime "github.com/ipld/go-ipld-prime" 12 "github.com/ipld/go-ipld-prime/datamodel" 13 cidlink "github.com/ipld/go-ipld-prime/linking/cid" 14 "github.com/ipld/go-ipld-prime/node/basicnode" 15 ipldschema "github.com/ipld/go-ipld-prime/schema" 16 "github.com/storacha/go-ucanto/client" 17 "github.com/storacha/go-ucanto/core/delegation" 18 "github.com/storacha/go-ucanto/core/invocation" 19 "github.com/storacha/go-ucanto/core/ipld" 20 "github.com/storacha/go-ucanto/core/receipt" 21 "github.com/storacha/go-ucanto/core/receipt/fx" 22 "github.com/storacha/go-ucanto/core/result" 23 fdm "github.com/storacha/go-ucanto/core/result/failure/datamodel" 24 "github.com/storacha/go-ucanto/core/schema" 25 "github.com/storacha/go-ucanto/testing/fixtures" 26 "github.com/storacha/go-ucanto/testing/helpers" 27 "github.com/storacha/go-ucanto/transport/car/request" 28 "github.com/storacha/go-ucanto/transport/car/response" 29 thttp "github.com/storacha/go-ucanto/transport/http" 30 "github.com/storacha/go-ucanto/ucan" 31 "github.com/storacha/go-ucanto/validator" 32 "github.com/stretchr/testify/require" 33 ) 34 35 type uploadAddCaveats struct { 36 Root ipld.Link 37 } 38 39 func (c uploadAddCaveats) ToIPLD() (ipld.Node, error) { 40 np := basicnode.Prototype.Any 41 nb := np.NewBuilder() 42 ma, _ := nb.BeginMap(1) 43 if c != (uploadAddCaveats{}) { 44 ma.AssembleKey().AssignString("root") 45 ma.AssembleValue().AssignLink(c.Root) 46 } 47 ma.Finish() 48 return nb.Build(), nil 49 } 50 51 func uploadAddCaveatsType() ipldschema.Type { 52 ts := helpers.Must(ipldprime.LoadSchemaBytes([]byte(` 53 type UploadAddCaveats struct { 54 root Link 55 } 56 `))) 57 return ts.TypeByName("UploadAddCaveats") 58 } 59 60 type uploadAddSuccess struct { 61 Root ipldprime.Link 62 Status string 63 } 64 65 func (ok uploadAddSuccess) ToIPLD() (ipld.Node, error) { 66 np := basicnode.Prototype.Any 67 nb := np.NewBuilder() 68 ma, _ := nb.BeginMap(2) 69 ma.AssembleKey().AssignString("root") 70 ma.AssembleValue().AssignLink(ok.Root) 71 ma.AssembleKey().AssignString("status") 72 ma.AssembleValue().AssignString(ok.Status) 73 ma.Finish() 74 return nb.Build(), nil 75 } 76 77 type uploadAddFailure struct { 78 name string 79 message string 80 } 81 82 func (x uploadAddFailure) Name() string { 83 return x.name 84 } 85 86 func (x uploadAddFailure) Error() string { 87 return x.message 88 } 89 90 func (x uploadAddFailure) ToIPLD() (ipld.Node, error) { 91 np := basicnode.Prototype.Any 92 nb := np.NewBuilder() 93 ma, _ := nb.BeginMap(2) 94 ma.AssembleKey().AssignString("name") 95 ma.AssembleValue().AssignString(x.name) 96 ma.AssembleKey().AssignString("message") 97 ma.AssembleValue().AssignString(x.message) 98 ma.Finish() 99 return nb.Build(), nil 100 } 101 102 var rcptsch = []byte(` 103 type Result union { 104 | UploadAddSuccess "ok" 105 | Any "error" 106 } representation keyed 107 108 type UploadAddSuccess struct { 109 root Link 110 status String 111 } 112 `) 113 114 // asFailure binds the IPLD node to a FailureModel if possible. This works 115 // around IPLD requiring data to match the schema exactly 116 func asFailure(t testing.TB, n ipld.Node) fdm.FailureModel { 117 t.Helper() 118 require.Equal(t, n.Kind(), datamodel.Kind_Map) 119 f := fdm.FailureModel{} 120 121 nn, err := n.LookupByString("name") 122 if err == nil { 123 name, err := nn.AsString() 124 require.NoError(t, err) 125 f.Name = &name 126 } 127 128 mn, err := n.LookupByString("message") 129 require.NoError(t, err) 130 msg, err := mn.AsString() 131 require.NoError(t, err) 132 f.Message = msg 133 134 sn, err := n.LookupByString("stack") 135 if err == nil { 136 stack, err := sn.AsString() 137 require.NoError(t, err) 138 f.Stack = &stack 139 } 140 141 return f 142 } 143 144 func TestExecute(t *testing.T) { 145 t.Run("self-signed", func(t *testing.T) { 146 uploadadd := validator.NewCapability( 147 "upload/add", 148 schema.DIDString(), 149 schema.Struct[uploadAddCaveats](uploadAddCaveatsType(), nil), 150 nil, 151 ) 152 153 server := helpers.Must(NewServer( 154 fixtures.Service, 155 WithServiceMethod( 156 uploadadd.Can(), 157 Provide(uploadadd, func(ctx context.Context, cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ictx InvocationContext) (result.Result[uploadAddSuccess, uploadAddFailure], fx.Effects, error) { 158 return result.Ok[uploadAddSuccess, uploadAddFailure](uploadAddSuccess{Root: cap.Nb().Root, Status: "done"}), nil, nil 159 }), 160 ), 161 )) 162 163 conn := helpers.Must(client.NewConnection(fixtures.Service, server)) 164 rt := cidlink.Link{Cid: cid.MustParse("bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui")} 165 cap := uploadadd.New(fixtures.Service.DID().String(), uploadAddCaveats{Root: rt}) 166 inv, err := invocation.Invoke(fixtures.Service, fixtures.Service, cap) 167 require.NoError(t, err) 168 169 resp, err := client.Execute(t.Context(), []invocation.Invocation{inv}, conn) 170 require.NoError(t, err) 171 172 // get the receipt link for the invocation from the response 173 rcptlnk, ok := resp.Get(inv.Link()) 174 require.True(t, ok, "missing receipt for invocation: %s", inv.Link()) 175 176 reader := helpers.Must(receipt.NewReceiptReader[uploadAddSuccess, ipld.Node](rcptsch)) 177 rcpt := helpers.Must(reader.Read(rcptlnk, resp.Blocks())) 178 179 result.MatchResultR0(rcpt.Out(), func(ok uploadAddSuccess) { 180 t.Logf("%+v\n", ok) 181 require.Equal(t, ok.Root, rt) 182 require.Equal(t, ok.Status, "done") 183 }, func(x ipld.Node) { 184 f := asFailure(t, x) 185 t.Log(f.Message) 186 t.Log(*f.Stack) 187 require.Nil(t, f) 188 }) 189 }) 190 191 t.Run("delegated", func(t *testing.T) { 192 uploadadd := validator.NewCapability( 193 "upload/add", 194 schema.DIDString(), 195 schema.Struct[uploadAddCaveats](uploadAddCaveatsType(), nil), 196 nil, 197 ) 198 199 server := helpers.Must(NewServer( 200 fixtures.Service, 201 WithServiceMethod( 202 uploadadd.Can(), 203 Provide(uploadadd, func(ctx context.Context, cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ictx InvocationContext) (result.Result[uploadAddSuccess, uploadAddFailure], fx.Effects, error) { 204 return result.Ok[uploadAddSuccess, uploadAddFailure](uploadAddSuccess{Root: cap.Nb().Root, Status: "done"}), nil, nil 205 }), 206 ), 207 )) 208 209 conn := helpers.Must(client.NewConnection(fixtures.Service, server)) 210 rt := cidlink.Link{Cid: cid.MustParse("bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui")} 211 cap := uploadadd.New(fixtures.Service.DID().String(), uploadAddCaveats{Root: rt}) 212 dgl, err := delegation.Delegate( 213 fixtures.Service, 214 fixtures.Alice, 215 []ucan.Capability[uploadAddCaveats]{ 216 ucan.NewCapability(uploadadd.Can(), fixtures.Service.DID().String(), uploadAddCaveats{}), 217 }, 218 ) 219 require.NoError(t, err) 220 221 prfs := []delegation.Proof{delegation.FromDelegation(dgl)} 222 inv, err := invocation.Invoke(fixtures.Alice, fixtures.Service, cap, delegation.WithProof(prfs...)) 223 require.NoError(t, err) 224 225 resp, err := client.Execute(t.Context(), []invocation.Invocation{inv}, conn) 226 require.NoError(t, err) 227 228 // get the receipt link for the invocation from the response 229 rcptlnk, ok := resp.Get(inv.Link()) 230 require.True(t, ok, "missing receipt for invocation: %s", inv.Link()) 231 232 reader := helpers.Must(receipt.NewReceiptReader[uploadAddSuccess, ipld.Node](rcptsch)) 233 rcpt := helpers.Must(reader.Read(rcptlnk, resp.Blocks())) 234 235 result.MatchResultR0(rcpt.Out(), func(ok uploadAddSuccess) { 236 t.Logf("%+v\n", ok) 237 require.Equal(t, ok.Root, rt) 238 require.Equal(t, ok.Status, "done") 239 }, func(x ipld.Node) { 240 f := asFailure(t, x) 241 t.Log(f.Message) 242 t.Log(*f.Stack) 243 require.Nil(t, f) 244 }) 245 }) 246 247 t.Run("not found", func(t *testing.T) { 248 server := helpers.Must(NewServer(fixtures.Service)) 249 conn := helpers.Must(client.NewConnection(fixtures.Service, server)) 250 251 rt := cidlink.Link{Cid: cid.MustParse("bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui")} 252 capability := ucan.NewCapability( 253 "upload/add", 254 fixtures.Alice.DID().String(), 255 uploadAddCaveats{Root: rt}, 256 ) 257 258 invs := []invocation.Invocation{helpers.Must(invocation.Invoke(fixtures.Alice, fixtures.Service, capability))} 259 resp := helpers.Must(client.Execute(t.Context(), invs, conn)) 260 rcptlnk, ok := resp.Get(invs[0].Link()) 261 require.True(t, ok, "missing receipt for invocation: %s", invs[0].Link()) 262 263 reader := helpers.Must(receipt.NewReceiptReader[uploadAddSuccess, ipld.Node](rcptsch)) 264 rcpt := helpers.Must(reader.Read(rcptlnk, resp.Blocks())) 265 266 result.MatchResultR0(rcpt.Out(), func(uploadAddSuccess) { 267 t.Fatalf("expected error: %s", invs[0].Link()) 268 }, func(x ipld.Node) { 269 f := asFailure(t, x) 270 t.Logf("%s %+v\n", *f.Name, f) 271 require.Equal(t, *f.Name, "HandlerNotFoundError") 272 }) 273 }) 274 275 t.Run("execution error", func(t *testing.T) { 276 uploadadd := validator.NewCapability( 277 "upload/add", 278 schema.DIDString(), 279 schema.Struct[uploadAddCaveats](uploadAddCaveatsType(), nil), 280 nil, 281 ) 282 283 server := helpers.Must(NewServer( 284 fixtures.Service, 285 WithServiceMethod( 286 uploadadd.Can(), 287 Provide(uploadadd, func(ctx context.Context, cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ictx InvocationContext) (result.Result[uploadAddSuccess, uploadAddFailure], fx.Effects, error) { 288 return nil, nil, fmt.Errorf("test error") 289 }), 290 ), 291 )) 292 293 conn := helpers.Must(client.NewConnection(fixtures.Service, server)) 294 rt := cidlink.Link{Cid: cid.MustParse("bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui")} 295 cap := uploadadd.New(fixtures.Alice.DID().String(), uploadAddCaveats{Root: rt}) 296 invs := []invocation.Invocation{helpers.Must(invocation.Invoke(fixtures.Alice, fixtures.Service, cap))} 297 resp := helpers.Must(client.Execute(t.Context(), invs, conn)) 298 rcptlnk, ok := resp.Get(invs[0].Link()) 299 require.True(t, ok, "missing receipt for invocation: %s", invs[0].Link()) 300 301 reader := helpers.Must(receipt.NewReceiptReader[uploadAddSuccess, ipld.Node](rcptsch)) 302 rcpt := helpers.Must(reader.Read(rcptlnk, resp.Blocks())) 303 304 result.MatchResultR0(rcpt.Out(), func(uploadAddSuccess) { 305 t.Fatalf("expected error: %s", invs[0].Link()) 306 }, func(x ipld.Node) { 307 f := asFailure(t, x) 308 t.Logf("%s %+v\n", *f.Name, f) 309 require.Equal(t, *f.Name, "HandlerExecutionError") 310 }) 311 }) 312 313 t.Run("failure", func(t *testing.T) { 314 uploadadd := validator.NewCapability( 315 "upload/add", 316 schema.DIDString(), 317 schema.Struct[uploadAddCaveats](uploadAddCaveatsType(), nil), 318 nil, 319 ) 320 321 server := helpers.Must(NewServer( 322 fixtures.Service, 323 WithServiceMethod( 324 uploadadd.Can(), 325 Provide(uploadadd, func(ctx context.Context, cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ictx InvocationContext) (result.Result[uploadAddSuccess, uploadAddFailure], fx.Effects, error) { 326 return result.Error[uploadAddSuccess](uploadAddFailure{name: "UploadAddError", message: "boom"}), nil, nil 327 }), 328 ), 329 )) 330 331 conn := helpers.Must(client.NewConnection(fixtures.Service, server)) 332 rt := cidlink.Link{Cid: cid.MustParse("bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui")} 333 cap := uploadadd.New(fixtures.Alice.DID().String(), uploadAddCaveats{Root: rt}) 334 invs := []invocation.Invocation{helpers.Must(invocation.Invoke(fixtures.Alice, fixtures.Service, cap))} 335 resp := helpers.Must(client.Execute(t.Context(), invs, conn)) 336 rcptlnk, ok := resp.Get(invs[0].Link()) 337 require.True(t, ok, "missing receipt for invocation: %s", invs[0].Link()) 338 339 reader := helpers.Must(receipt.NewReceiptReader[uploadAddSuccess, ipld.Node](rcptsch)) 340 rcpt := helpers.Must(reader.Read(rcptlnk, resp.Blocks())) 341 342 result.MatchResultR0(rcpt.Out(), func(uploadAddSuccess) { 343 t.Fatalf("expected error: %s", invs[0].Link()) 344 }, func(x ipld.Node) { 345 f := asFailure(t, x) 346 t.Logf("%s %+v\n", *f.Name, f) 347 require.Equal(t, *f.Name, "UploadAddError") 348 }) 349 }) 350 351 t.Run("invalid audience", func(t *testing.T) { 352 uploadadd := validator.NewCapability( 353 "upload/add", 354 schema.DIDString(), 355 schema.Struct[uploadAddCaveats](uploadAddCaveatsType(), nil), 356 nil, 357 ) 358 359 server := helpers.Must(NewServer( 360 fixtures.Service, 361 WithServiceMethod( 362 uploadadd.Can(), 363 Provide(uploadadd, func(ctx context.Context, cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ictx InvocationContext) (result.Result[uploadAddSuccess, uploadAddFailure], fx.Effects, error) { 364 return result.Ok[uploadAddSuccess, uploadAddFailure](uploadAddSuccess{Root: cap.Nb().Root, Status: "done"}), nil, nil 365 }), 366 ), 367 )) 368 369 conn := helpers.Must(client.NewConnection(fixtures.Service, server)) 370 rt := cidlink.Link{Cid: cid.MustParse("bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui")} 371 cap := uploadadd.New(fixtures.Alice.DID().String(), uploadAddCaveats{Root: rt}) 372 373 // invocation audience different from the service 374 invs := []invocation.Invocation{helpers.Must(invocation.Invoke(fixtures.Alice, fixtures.Bob, cap))} 375 376 resp, err := client.Execute(t.Context(), invs, conn) 377 require.NoError(t, err) 378 379 rcptlnk, ok := resp.Get(invs[0].Link()) 380 require.True(t, ok, "missing receipt for invocation: %s", invs[0].Link()) 381 382 reader := helpers.Must(receipt.NewReceiptReader[uploadAddSuccess, ipld.Node](rcptsch)) 383 rcpt := helpers.Must(reader.Read(rcptlnk, resp.Blocks())) 384 385 result.MatchResultR0(rcpt.Out(), func(uploadAddSuccess) { 386 t.Fatalf("expected InvalidAudienceError but got ok") 387 }, func(x ipld.Node) { 388 f := asFailure(t, x) 389 require.Equal(t, *f.Name, "InvalidAudienceError") 390 }) 391 }) 392 393 t.Run("alternative audience", func(t *testing.T) { 394 uploadadd := validator.NewCapability( 395 "upload/add", 396 schema.DIDString(), 397 schema.Struct[uploadAddCaveats](uploadAddCaveatsType(), nil), 398 nil, 399 ) 400 401 server := helpers.Must(NewServer( 402 fixtures.Service, 403 WithServiceMethod( 404 uploadadd.Can(), 405 Provide(uploadadd, func(ctx context.Context, cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ictx InvocationContext) (result.Result[uploadAddSuccess, uploadAddFailure], fx.Effects, error) { 406 return result.Ok[uploadAddSuccess, uploadAddFailure](uploadAddSuccess{Root: cap.Nb().Root, Status: "done"}), nil, nil 407 }), 408 ), 409 WithAlternativeAudiences(fixtures.Bob), 410 )) 411 412 conn := helpers.Must(client.NewConnection(fixtures.Service, server)) 413 rt := cidlink.Link{Cid: cid.MustParse("bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui")} 414 cap := uploadadd.New(fixtures.Alice.DID().String(), uploadAddCaveats{Root: rt}) 415 416 // invocation audience different from the service, but an accepted alternative audience 417 invs := []invocation.Invocation{helpers.Must(invocation.Invoke(fixtures.Alice, fixtures.Bob, cap))} 418 419 resp, err := client.Execute(t.Context(), invs, conn) 420 require.NoError(t, err) 421 422 rcptlnk, ok := resp.Get(invs[0].Link()) 423 require.True(t, ok, "missing receipt for invocation: %s", invs[0].Link()) 424 425 reader := helpers.Must(receipt.NewReceiptReader[uploadAddSuccess, ipld.Node](rcptsch)) 426 rcpt := helpers.Must(reader.Read(rcptlnk, resp.Blocks())) 427 428 result.MatchResultR0(rcpt.Out(), func(ok uploadAddSuccess) { 429 require.Equal(t, ok.Root, rt) 430 require.Equal(t, ok.Status, "done") 431 }, func(x ipld.Node) { 432 f := asFailure(t, x) 433 t.Fatalf("expected no error but got %s", *f.Name) 434 }) 435 }) 436 } 437 438 func TestHandle(t *testing.T) { 439 t.Run("content type error", func(t *testing.T) { 440 server := helpers.Must(NewServer(fixtures.Service)) 441 442 hd := http.Header{} 443 hd.Set("Content-Type", "unsupported/media") 444 hd.Set("Accept", response.ContentType) 445 446 req := thttp.NewRequest(bytes.NewReader([]byte{}), hd) 447 res := helpers.Must(Handle(t.Context(), server, req)) 448 require.Equal(t, res.Status(), http.StatusUnsupportedMediaType) 449 }) 450 451 t.Run("accept error", func(t *testing.T) { 452 server := helpers.Must(NewServer(fixtures.Service)) 453 454 hd := http.Header{} 455 hd.Set("Content-Type", request.ContentType) 456 hd.Set("Accept", "not/acceptable") 457 458 req := thttp.NewRequest(bytes.NewReader([]byte{}), hd) 459 res := helpers.Must(Handle(t.Context(), server, req)) 460 require.Equal(t, res.Status(), http.StatusNotAcceptable) 461 }) 462 463 t.Run("decode error", func(t *testing.T) { 464 server := helpers.Must(NewServer(fixtures.Service)) 465 466 hd := http.Header{} 467 hd.Set("Content-Type", request.ContentType) 468 hd.Set("Accept", request.ContentType) 469 470 // request with invalid payload 471 req := thttp.NewRequest(bytes.NewReader([]byte{}), hd) 472 res := helpers.Must(Handle(t.Context(), server, req)) 473 require.Equal(t, res.Status(), http.StatusBadRequest) 474 }) 475 }