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  }