diff --git a/v2/pkg/engine/resolve/authorization_test.go b/v2/pkg/engine/resolve/authorization_test.go index 263724a77c..6c71d062d7 100644 --- a/v2/pkg/engine/resolve/authorization_test.go +++ b/v2/pkg/engine/resolve/authorization_test.go @@ -64,7 +64,8 @@ func TestAuthorization(t *testing.T) { }) res := generateTestFederationGraphQLResponse(t, ctrl) - resolveCtx := &Context{ctx: context.Background(), Variables: nil, authorizer: authorizer} + resolveCtx := NewContext(context.Background()) + resolveCtx.authorizer = authorizer return res, resolveCtx, `{"data":{"me":{"id":"1234","username":"Me","reviews":[{"body":"A highly effective form of birth control.","product":{"upc":"top-1","name":"Trilby"}},{"body":"Fedoras are one of the most fashionable hats around and can look great with a variety of outfits.","product":{"upc":"top-2","name":"Fedora"}}]}}}`, @@ -72,7 +73,7 @@ func TestAuthorization(t *testing.T) { assert.Equal(t, int64(2), authorizer.(*testAuthorizer).preFetchCalls.Load()) assert.Equal(t, int64(4), authorizer.(*testAuthorizer).objectFieldCalls.Load()) - require.Nil(t, resolveCtx.subgraphErrors) + require.Len(t, resolveCtx.subgraphErrors, 0) } })) t.Run("validate authorizer args", testFnWithPostEvaluation(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx *Context, expectedOutput string, postEvaluation func(t *testing.T)) { @@ -109,7 +110,9 @@ func TestAuthorization(t *testing.T) { res := generateTestFederationGraphQLResponse(t, ctrl) - return res, &Context{ctx: context.Background(), Variables: nil, authorizer: authorizer}, + resolveCtx := NewContext(context.Background()) + resolveCtx.authorizer = authorizer + return res, resolveCtx, `{"data":{"me":{"id":"1234","username":"Me","reviews":[{"body":"A highly effective form of birth control.","product":{"upc":"top-1","name":"Trilby"}},{"body":"Fedoras are one of the most fashionable hats around and can look great with a variety of outfits.","product":{"upc":"top-2","name":"Fedora"}}]}}}`, func(t *testing.T) { assert.Equal(t, int64(2), authorizer.(*testAuthorizer).preFetchCalls.Load()) @@ -131,7 +134,8 @@ func TestAuthorization(t *testing.T) { }) res := generateTestFederationGraphQLResponse(t, ctrl) - resolveCtx := &Context{ctx: context.Background(), Variables: nil, authorizer: authorizer} + resolveCtx := NewContext(context.Background()) + resolveCtx.authorizer = authorizer return res, resolveCtx, `{"data":{"me":{"id":"1234","username":"Me","reviews":[{"body":"A highly effective form of birth control.","product":{"upc":"top-1","name":"Trilby"}},{"body":"Fedoras are one of the most fashionable hats around and can look great with a variety of outfits.","product":{"upc":"top-2","name":"Fedora"}}]}}}`, @@ -139,7 +143,7 @@ func TestAuthorization(t *testing.T) { assert.Equal(t, int64(2), authorizer.(*testAuthorizer).preFetchCalls.Load()) assert.Equal(t, int64(4), authorizer.(*testAuthorizer).objectFieldCalls.Load()) - require.Nil(t, resolveCtx.subgraphErrors) + require.Len(t, resolveCtx.subgraphErrors, 0) } })) t.Run("disallow field with extension", testFnWithPostEvaluation(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx *Context, expectedOutput string, postEvaluation func(t *testing.T)) { @@ -159,7 +163,8 @@ func TestAuthorization(t *testing.T) { authorizer.(*testAuthorizer).responseExtension = []byte(`{"missingScopes":["id"]}`) res := generateTestFederationGraphQLResponse(t, ctrl) - resolveCtx := &Context{ctx: context.Background(), Variables: nil, authorizer: authorizer} + resolveCtx := NewContext(context.Background()) + resolveCtx.authorizer = authorizer return res, resolveCtx, `{"data":{"me":{"id":"1234","username":"Me","reviews":[{"body":"A highly effective form of birth control.","product":{"upc":"top-1","name":"Trilby"}},{"body":"Fedoras are one of the most fashionable hats around and can look great with a variety of outfits.","product":{"upc":"top-2","name":"Fedora"}}]}},"extensions":{"authorization":{"missingScopes":["id"]}}}`, @@ -167,7 +172,7 @@ func TestAuthorization(t *testing.T) { assert.Equal(t, int64(2), authorizer.(*testAuthorizer).preFetchCalls.Load()) assert.Equal(t, int64(4), authorizer.(*testAuthorizer).objectFieldCalls.Load()) - require.Nil(t, resolveCtx.subgraphErrors) + require.Len(t, resolveCtx.subgraphErrors, 0) } })) t.Run("no authorization rules/checks", testFnWithPostEvaluation(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx *Context, expectedOutput string, postEvaluation func(t *testing.T)) { @@ -179,7 +184,8 @@ func TestAuthorization(t *testing.T) { }) res := generateTestFederationGraphQLResponseWithoutAuthorizationRules(t, ctrl) - resolveCtx := &Context{ctx: context.Background(), Variables: nil, authorizer: authorizer} + resolveCtx := NewContext(context.Background()) + resolveCtx.authorizer = authorizer return res, resolveCtx, `{"data":{"me":{"id":"1234","username":"Me","reviews":[{"body":"A highly effective form of birth control.","product":{"upc":"top-1","name":"Trilby"}},{"body":"Fedoras are one of the most fashionable hats around and can look great with a variety of outfits.","product":{"upc":"top-2","name":"Fedora"}}]}}}`, @@ -187,7 +193,7 @@ func TestAuthorization(t *testing.T) { assert.Equal(t, int64(0), authorizer.(*testAuthorizer).preFetchCalls.Load()) assert.Equal(t, int64(0), authorizer.(*testAuthorizer).objectFieldCalls.Load()) - require.Nil(t, resolveCtx.subgraphErrors) + require.Len(t, resolveCtx.subgraphErrors, 0) } })) t.Run("disallow root fetch", testFnWithPostEvaluation(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx *Context, expectedOutput string, postEvaluation func(t *testing.T)) { @@ -204,7 +210,8 @@ func TestAuthorization(t *testing.T) { }) res := generateTestFederationGraphQLResponse(t, ctrl) - resolveCtx := &Context{ctx: context.Background(), Variables: nil, authorizer: authorizer} + resolveCtx := NewContext(context.Background()) + resolveCtx.authorizer = authorizer return res, resolveCtx, `{"errors":[{"message":"Unauthorized request to Subgraph 'users' at Path 'query', Reason: Not allowed to fetch from users Subgraph.","extensions":{"code":"UNAUTHORIZED_FIELD_OR_TYPE"}},{"message":"Failed to fetch from Subgraph 'reviews' at Path 'query.me'.","extensions":{"errors":[{"message":"Failed to render Fetch Input","path":["me"]}]}},{"message":"Failed to fetch from Subgraph 'products' at Path 'query.me.reviews.@.product'.","extensions":{"errors":[{"message":"Failed to render Fetch Input","path":["me","reviews","@","product"]}]}}],"data":{"me":null}}`, @@ -212,9 +219,10 @@ func TestAuthorization(t *testing.T) { assert.Equal(t, int64(1), authorizer.(*testAuthorizer).preFetchCalls.Load()) assert.Equal(t, int64(0), authorizer.(*testAuthorizer).objectFieldCalls.Load()) + expectedSubgraph := "users" var subgraphError *SubgraphError - require.ErrorAs(t, resolveCtx.subgraphErrors, &subgraphError) - require.Equal(t, "users", subgraphError.DataSourceInfo.Name) + require.ErrorAs(t, resolveCtx.subgraphErrors[expectedSubgraph], &subgraphError) + require.Equal(t, expectedSubgraph, subgraphError.DataSourceInfo.Name) require.Equal(t, "query", subgraphError.Path) require.Equal(t, "Not allowed to fetch from users Subgraph", subgraphError.Reason) require.Equal(t, 0, subgraphError.ResponseCode) @@ -235,7 +243,8 @@ func TestAuthorization(t *testing.T) { }) res := generateTestFederationGraphQLResponse(t, ctrl) - resolveCtx := &Context{ctx: context.Background(), Variables: nil, authorizer: authorizer} + resolveCtx := NewContext(context.Background()) + resolveCtx.authorizer = authorizer return res, resolveCtx, `{"errors":[{"message":"Unauthorized request to Subgraph 'users' at Path 'query'.","extensions":{"code":"UNAUTHORIZED_FIELD_OR_TYPE"}},{"message":"Failed to fetch from Subgraph 'reviews' at Path 'query.me'.","extensions":{"errors":[{"message":"Failed to render Fetch Input","path":["me"]}]}},{"message":"Failed to fetch from Subgraph 'products' at Path 'query.me.reviews.@.product'.","extensions":{"errors":[{"message":"Failed to render Fetch Input","path":["me","reviews","@","product"]}]}}],"data":{"me":null}}`, @@ -243,9 +252,10 @@ func TestAuthorization(t *testing.T) { assert.Equal(t, int64(1), authorizer.(*testAuthorizer).preFetchCalls.Load()) assert.Equal(t, int64(0), authorizer.(*testAuthorizer).objectFieldCalls.Load()) + expectedSubgraph := "users" var subgraphError *SubgraphError - require.ErrorAs(t, resolveCtx.subgraphErrors, &subgraphError) - require.Equal(t, "users", subgraphError.DataSourceInfo.Name) + require.ErrorAs(t, resolveCtx.subgraphErrors[expectedSubgraph], &subgraphError) + require.Equal(t, expectedSubgraph, subgraphError.DataSourceInfo.Name) require.Equal(t, "query", subgraphError.Path) require.Equal(t, "", subgraphError.Reason) require.Equal(t, 0, subgraphError.ResponseCode) @@ -268,20 +278,23 @@ func TestAuthorization(t *testing.T) { res := generateTestFederationGraphQLResponse(t, ctrl) - resolveCtx := Context{ctx: context.Background(), Variables: nil, authorizer: authorizer} + resolveCtx := NewContext(context.Background()) + resolveCtx.authorizer = authorizer - return res, &resolveCtx, + return res, resolveCtx, `{"errors":[{"message":"Unauthorized request to Subgraph 'products' at Path 'query.me.reviews.@.product', Reason: Not allowed to fetch from products Subgraph.","extensions":{"code":"UNAUTHORIZED_FIELD_OR_TYPE"}}],"data":{"me":{"id":"1234","username":"Me","reviews":[null,null]}}}`, func(t *testing.T) { assert.Equal(t, int64(2), authorizer.(*testAuthorizer).preFetchCalls.Load()) assert.Equal(t, int64(4), authorizer.(*testAuthorizer).objectFieldCalls.Load()) + expectedSubgraph := "products" + require.NotEmpty(t, resolveCtx.subgraphErrors) - require.EqualError(t, resolveCtx.subgraphErrors, "Failed to fetch from Subgraph 'products' at Path: 'query.me.reviews.@.product', Reason: Not allowed to fetch from products Subgraph.") + require.EqualError(t, resolveCtx.subgraphErrors[expectedSubgraph], "Failed to fetch from Subgraph 'products' at Path: 'query.me.reviews.@.product', Reason: Not allowed to fetch from products Subgraph.") var subgraphError *SubgraphError - require.ErrorAs(t, resolveCtx.subgraphErrors, &subgraphError) - require.Equal(t, "products", subgraphError.DataSourceInfo.Name) + require.ErrorAs(t, resolveCtx.subgraphErrors[expectedSubgraph], &subgraphError) + require.Equal(t, expectedSubgraph, subgraphError.DataSourceInfo.Name) require.Equal(t, "query.me.reviews.@.product", subgraphError.Path) require.Equal(t, "Not allowed to fetch from products Subgraph", subgraphError.Reason) require.Equal(t, 0, subgraphError.ResponseCode) @@ -302,7 +315,8 @@ func TestAuthorization(t *testing.T) { }) res := generateTestFederationGraphQLResponse(t, ctrl) - resolveCtx := &Context{ctx: context.Background(), Variables: nil, authorizer: authorizer} + resolveCtx := NewContext(context.Background()) + resolveCtx.authorizer = authorizer return res, resolveCtx, `{"errors":[{"message":"Unauthorized to load field 'Query.me.reviews.product.data.name', Reason: Not allowed to fetch name on Product.","path":["me","reviews",0,"product","data","name"],"extensions":{"code":"UNAUTHORIZED_FIELD_OR_TYPE"}},{"message":"Unauthorized to load field 'Query.me.reviews.product.data.name', Reason: Not allowed to fetch name on Product.","path":["me","reviews",1,"product","data","name"],"extensions":{"code":"UNAUTHORIZED_FIELD_OR_TYPE"}}],"data":{"me":{"id":"1234","username":"Me","reviews":[null,null]}}}`, @@ -310,9 +324,11 @@ func TestAuthorization(t *testing.T) { assert.Equal(t, int64(2), authorizer.(*testAuthorizer).preFetchCalls.Load()) assert.Equal(t, int64(4), authorizer.(*testAuthorizer).objectFieldCalls.Load()) + expectedSubgraph := "products" + var subgraphError *SubgraphError - require.ErrorAs(t, resolveCtx.subgraphErrors, &subgraphError) - require.Equal(t, "products", subgraphError.DataSourceInfo.Name) + require.ErrorAs(t, resolveCtx.subgraphErrors[expectedSubgraph], &subgraphError) + require.Equal(t, expectedSubgraph, subgraphError.DataSourceInfo.Name) require.Equal(t, "Query.me.reviews.product.data.name", subgraphError.Path) require.Equal(t, "Not allowed to fetch name on Product", subgraphError.Reason) require.Equal(t, 0, subgraphError.ResponseCode) @@ -335,7 +351,8 @@ func TestAuthorization(t *testing.T) { }) res := generateTestFederationGraphQLResponse(t, ctrl) - resolveCtx := &Context{ctx: context.Background(), Variables: nil, authorizer: authorizer} + resolveCtx := NewContext(context.Background()) + resolveCtx.authorizer = authorizer return res, resolveCtx, `{"errors":[{"message":"Unauthorized request to Subgraph 'products' at Path 'query.me.reviews.@.product', Reason: Not allowed to fetch from products Subgraph.","extensions":{"code":"UNAUTHORIZED_FIELD_OR_TYPE"}}],"data":{"me":{"id":"1234","username":"Me","reviews":[null,null]}}}`, @@ -343,9 +360,10 @@ func TestAuthorization(t *testing.T) { assert.Equal(t, int64(2), authorizer.(*testAuthorizer).preFetchCalls.Load()) assert.Equal(t, int64(4), authorizer.(*testAuthorizer).objectFieldCalls.Load()) + expectedSubgraph := "products" var subgraphError *SubgraphError - require.ErrorAs(t, resolveCtx.subgraphErrors, &subgraphError) - require.Equal(t, "products", subgraphError.DataSourceInfo.Name) + require.ErrorAs(t, resolveCtx.subgraphErrors[expectedSubgraph], &subgraphError) + require.Equal(t, expectedSubgraph, subgraphError.DataSourceInfo.Name) require.Equal(t, "query.me.reviews.@.product", subgraphError.Path) require.Equal(t, "Not allowed to fetch from products Subgraph", subgraphError.Reason) require.Equal(t, 0, subgraphError.ResponseCode) @@ -362,8 +380,9 @@ func TestAuthorization(t *testing.T) { res := generateTestFederationGraphQLResponse(t, ctrl) - return res, Context{ctx: context.Background(), Variables: nil, authorizer: authorizer}, - `` + resolveCtx := NewContext(context.Background()) + resolveCtx.authorizer = authorizer + return res, *resolveCtx, `` })) t.Run("disallow nullable field", testFnWithPostEvaluation(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx *Context, expectedOutput string, postEvaluation func(t *testing.T)) { @@ -379,7 +398,8 @@ func TestAuthorization(t *testing.T) { }) res := generateTestFederationGraphQLResponse(t, ctrl) - resolveCtx := &Context{ctx: context.Background(), Variables: nil, authorizer: authorizer} + resolveCtx := NewContext(context.Background()) + resolveCtx.authorizer = authorizer return res, resolveCtx, `{"errors":[{"message":"Unauthorized to load field 'Query.me.reviews.body', Reason: Not allowed to fetch body on Review.","path":["me","reviews",0,"body"],"extensions":{"code":"UNAUTHORIZED_FIELD_OR_TYPE"}},{"message":"Unauthorized to load field 'Query.me.reviews.body', Reason: Not allowed to fetch body on Review.","path":["me","reviews",1,"body"],"extensions":{"code":"UNAUTHORIZED_FIELD_OR_TYPE"}}],"data":{"me":{"id":"1234","username":"Me","reviews":[{"body":null,"product":{"upc":"top-1","name":"Trilby"}},{"body":null,"product":{"upc":"top-2","name":"Fedora"}}]}}}`, @@ -387,9 +407,10 @@ func TestAuthorization(t *testing.T) { assert.Equal(t, int64(2), authorizer.(*testAuthorizer).preFetchCalls.Load()) assert.Equal(t, int64(4), authorizer.(*testAuthorizer).objectFieldCalls.Load()) + expectedSubgraph := "reviews" var subgraphError *SubgraphError - require.ErrorAs(t, resolveCtx.subgraphErrors, &subgraphError) - require.Equal(t, "reviews", subgraphError.DataSourceInfo.Name) + require.ErrorAs(t, resolveCtx.subgraphErrors[expectedSubgraph], &subgraphError) + require.Equal(t, expectedSubgraph, subgraphError.DataSourceInfo.Name) require.Equal(t, "Query.me.reviews.body", subgraphError.Path) require.Equal(t, "Not allowed to fetch body on Review", subgraphError.Reason) require.Equal(t, 0, subgraphError.ResponseCode) @@ -408,7 +429,8 @@ func TestAuthorization(t *testing.T) { }) res := generateTestFederationGraphQLResponse(t, ctrl) - resolveCtx := &Context{ctx: context.Background(), Variables: nil, authorizer: authorizer} + resolveCtx := NewContext(context.Background()) + resolveCtx.authorizer = authorizer return res, resolveCtx, `{"errors":[{"message":"Unauthorized to load field 'Query.me.reviews.body'.","path":["me","reviews",0,"body"],"extensions":{"code":"UNAUTHORIZED_FIELD_OR_TYPE"}},{"message":"Unauthorized to load field 'Query.me.reviews.body'.","path":["me","reviews",1,"body"],"extensions":{"code":"UNAUTHORIZED_FIELD_OR_TYPE"}}],"data":{"me":{"id":"1234","username":"Me","reviews":[{"body":null,"product":{"upc":"top-1","name":"Trilby"}},{"body":null,"product":{"upc":"top-2","name":"Fedora"}}]}}}`, @@ -416,9 +438,10 @@ func TestAuthorization(t *testing.T) { assert.Equal(t, int64(2), authorizer.(*testAuthorizer).preFetchCalls.Load()) assert.Equal(t, int64(4), authorizer.(*testAuthorizer).objectFieldCalls.Load()) + expectedSubgraph := "reviews" var subgraphError *SubgraphError - require.ErrorAs(t, resolveCtx.subgraphErrors, &subgraphError) - require.Equal(t, "reviews", subgraphError.DataSourceInfo.Name) + require.ErrorAs(t, resolveCtx.subgraphErrors[expectedSubgraph], &subgraphError) + require.Equal(t, expectedSubgraph, subgraphError.DataSourceInfo.Name) require.Equal(t, "Query.me.reviews.body", subgraphError.Path) require.Equal(t, "", subgraphError.Reason) require.Equal(t, 0, subgraphError.ResponseCode) @@ -439,7 +462,8 @@ func TestAuthorization(t *testing.T) { }) res := generateTestFederationGraphQLResponse(t, ctrl) - resolveCtx := &Context{ctx: context.Background(), Variables: nil, authorizer: authorizer} + resolveCtx := NewContext(context.Background()) + resolveCtx.authorizer = authorizer return res, resolveCtx, `{"errors":[{"message":"Unauthorized request to Subgraph 'products' at Path 'query.me.reviews.@.product', Reason: Not allowed to fetch name on Product.","extensions":{"code":"UNAUTHORIZED_FIELD_OR_TYPE"}}],"data":{"me":{"id":"1234","username":"Me","reviews":[null,null]}}}`, @@ -447,9 +471,10 @@ func TestAuthorization(t *testing.T) { assert.Equal(t, int64(2), authorizer.(*testAuthorizer).preFetchCalls.Load()) assert.Equal(t, int64(4), authorizer.(*testAuthorizer).objectFieldCalls.Load()) + expectedSubgraph := "products" var subgraphError *SubgraphError - require.ErrorAs(t, resolveCtx.subgraphErrors, &subgraphError) - require.Equal(t, "products", subgraphError.DataSourceInfo.Name) + require.ErrorAs(t, resolveCtx.subgraphErrors[expectedSubgraph], &subgraphError) + require.Equal(t, expectedSubgraph, subgraphError.DataSourceInfo.Name) require.Equal(t, "query.me.reviews.@.product", subgraphError.Path) require.Equal(t, "Not allowed to fetch name on Product", subgraphError.Reason) require.Equal(t, 0, subgraphError.ResponseCode) @@ -470,7 +495,8 @@ func TestAuthorization(t *testing.T) { }) res := generateTestFederationGraphQLResponse(t, ctrl) - resolveCtx := &Context{ctx: context.Background(), Variables: nil, authorizer: authorizer} + resolveCtx := NewContext(context.Background()) + resolveCtx.authorizer = authorizer return res, resolveCtx, `{"errors":[{"message":"Unauthorized to load field 'Query.me.reviews.product.data.name', Reason: Not allowed to fetch name on Product.","path":["me","reviews",0,"product","data","name"],"extensions":{"code":"UNAUTHORIZED_FIELD_OR_TYPE"}},{"message":"Unauthorized to load field 'Query.me.reviews.product.data.name', Reason: Not allowed to fetch name on Product.","path":["me","reviews",1,"product","data","name"],"extensions":{"code":"UNAUTHORIZED_FIELD_OR_TYPE"}}],"data":{"me":{"id":"1234","username":"Me","reviews":[null,null]}}}`, @@ -478,9 +504,10 @@ func TestAuthorization(t *testing.T) { assert.Equal(t, int64(2), authorizer.(*testAuthorizer).preFetchCalls.Load()) assert.Equal(t, int64(4), authorizer.(*testAuthorizer).objectFieldCalls.Load()) + expectedSubgraph := "products" var subgraphError *SubgraphError - require.ErrorAs(t, resolveCtx.subgraphErrors, &subgraphError) - require.Equal(t, "products", subgraphError.DataSourceInfo.Name) + require.ErrorAs(t, resolveCtx.subgraphErrors[expectedSubgraph], &subgraphError) + require.Equal(t, expectedSubgraph, subgraphError.DataSourceInfo.Name) require.Equal(t, "Query.me.reviews.product.data.name", subgraphError.Path) require.Equal(t, "Not allowed to fetch name on Product", subgraphError.Reason) require.Equal(t, 0, subgraphError.ResponseCode) @@ -502,7 +529,9 @@ func TestAuthorization(t *testing.T) { res := generateTestFederationGraphQLResponse(t, ctrl) - return res, Context{ctx: context.Background(), Variables: nil, authorizer: authorizer}, + resolveCtx := NewContext(context.Background()) + resolveCtx.authorizer = authorizer + return res, *resolveCtx, `{"errors":[{"message":"Unauthorized to load field 'Query.me.reviews.product.data.name', Reason: Not allowed to fetch name on Product.","path":["me","reviews",0,"product","data","name"]},{"message":"Unauthorized to load field 'Query.me.reviews.product.data.name', Reason: Not allowed to fetch name on Product.","path":["me","reviews",1,"product","data","name"]}],"data":{"me":{"id":"1234","username":"Me","reviews":[null,null]}}}` })) } diff --git a/v2/pkg/engine/resolve/context.go b/v2/pkg/engine/resolve/context.go index 65d2d6b900..474dd66a60 100644 --- a/v2/pkg/engine/resolve/context.go +++ b/v2/pkg/engine/resolve/context.go @@ -6,6 +6,7 @@ import ( "errors" "io" "net/http" + "sort" "time" "github.com/wundergraph/astjson" @@ -13,6 +14,7 @@ import ( "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/datasource/httpclient" ) +// Context should not ever be initialized directly, and should be initialized via the NewContext function type Context struct { ctx context.Context Variables *astjson.Value @@ -31,7 +33,7 @@ type Context struct { rateLimiter RateLimiter fieldRenderer FieldValueRenderer - subgraphErrors error + subgraphErrors map[string]error } type ExecutionOptions struct { @@ -138,11 +140,26 @@ func (c *Context) SetRateLimiter(limiter RateLimiter) { } func (c *Context) SubgraphErrors() error { - return c.subgraphErrors + if len(c.subgraphErrors) == 0 { + return nil + } + + // Ensure the errors are appended in an idempotent order + keys := make([]string, 0, len(c.subgraphErrors)) + for k := range c.subgraphErrors { + keys = append(keys, k) + } + sort.Strings(keys) + + var joined error + for _, k := range keys { + joined = errors.Join(joined, c.subgraphErrors[k]) + } + return joined } -func (c *Context) appendSubgraphErrors(errs ...error) { - c.subgraphErrors = errors.Join(c.subgraphErrors, errors.Join(errs...)) +func (c *Context) appendSubgraphErrors(ds DataSourceInfo, errs ...error) { + c.subgraphErrors[ds.Name] = errors.Join(c.subgraphErrors[ds.Name], errors.Join(errs...)) } type Request struct { @@ -155,7 +172,8 @@ func NewContext(ctx context.Context) *Context { panic("nil context.Context") } return &Context{ - ctx: ctx, + ctx: ctx, + subgraphErrors: make(map[string]error), } } @@ -190,6 +208,13 @@ func (c *Context) clone(ctx context.Context) *Context { } } + if c.subgraphErrors != nil { + cpy.subgraphErrors = make(map[string]error, len(c.subgraphErrors)) + for k, v := range c.subgraphErrors { + cpy.subgraphErrors[k] = v + } + } + return &cpy } diff --git a/v2/pkg/engine/resolve/extensions_test.go b/v2/pkg/engine/resolve/extensions_test.go index 22e489eca3..66f7e2f21e 100644 --- a/v2/pkg/engine/resolve/extensions_test.go +++ b/v2/pkg/engine/resolve/extensions_test.go @@ -19,7 +19,9 @@ func TestExtensions(t *testing.T) { res := generateTestFederationGraphQLResponse(t, ctrl) - return res, &Context{ctx: context.Background(), Variables: nil, authorizer: authorizer}, + resolveCtx := NewContext(context.Background()) + resolveCtx.authorizer = authorizer + return res, resolveCtx, `{"errors":[{"message":"Unauthorized request to Subgraph 'users' at Path 'query', Reason: test.","extensions":{"code":"UNAUTHORIZED_FIELD_OR_TYPE"}},{"message":"Failed to fetch from Subgraph 'reviews' at Path 'query.me'.","extensions":{"errors":[{"message":"Failed to render Fetch Input","path":["me"]}]}},{"message":"Failed to fetch from Subgraph 'products' at Path 'query.me.reviews.@.product'.","extensions":{"errors":[{"message":"Failed to render Fetch Input","path":["me","reviews","@","product"]}]}}],"data":{"me":null}}`, func(t *testing.T) {} })) @@ -44,7 +46,11 @@ func TestExtensions(t *testing.T) { res := generateTestFederationGraphQLResponse(t, ctrl) - return res, &Context{ctx: context.Background(), Variables: nil, authorizer: authorizer, rateLimiter: limiter, RateLimitOptions: RateLimitOptions{Enable: true, IncludeStatsInResponseExtension: true}}, + resolveCtx := NewContext(context.Background()) + resolveCtx.authorizer = authorizer + resolveCtx.rateLimiter = limiter + resolveCtx.RateLimitOptions = RateLimitOptions{Enable: true, IncludeStatsInResponseExtension: true} + return res, resolveCtx, `{"errors":[{"message":"Unauthorized request to Subgraph 'users' at Path 'query', Reason: test.","extensions":{"code":"UNAUTHORIZED_FIELD_OR_TYPE"}},{"message":"Failed to fetch from Subgraph 'reviews' at Path 'query.me'.","extensions":{"errors":[{"message":"Failed to render Fetch Input","path":["me"]}]}},{"message":"Failed to fetch from Subgraph 'products' at Path 'query.me.reviews.@.product'.","extensions":{"errors":[{"message":"Failed to render Fetch Input","path":["me","reviews","@","product"]}]}}],"data":{"me":null},"extensions":{"authorization":{"missingScopes":[["read:users"]]},"rateLimit":{"Policy":"policy","Allowed":0,"Used":0}}}`, func(t *testing.T) {} })) @@ -69,7 +75,11 @@ func TestExtensions(t *testing.T) { res := generateTestFederationGraphQLResponse(t, ctrl) - return res, &Context{ctx: context.Background(), Variables: nil, authorizer: authorizer, rateLimiter: limiter, RateLimitOptions: RateLimitOptions{Enable: true, IncludeStatsInResponseExtension: true}}, + resolveCtx := NewContext(context.Background()) + resolveCtx.authorizer = authorizer + resolveCtx.rateLimiter = limiter + resolveCtx.RateLimitOptions = RateLimitOptions{Enable: true, IncludeStatsInResponseExtension: true} + return res, resolveCtx, `{"errors":[{"message":"Unauthorized request to Subgraph 'users' at Path 'query', Reason: test.","extensions":{"code":"UNAUTHORIZED_FIELD_OR_TYPE"}},{"message":"Failed to fetch from Subgraph 'reviews' at Path 'query.me'.","extensions":{"errors":[{"message":"Failed to render Fetch Input","path":["me"]}]}},{"message":"Failed to fetch from Subgraph 'products' at Path 'query.me.reviews.@.product'.","extensions":{"errors":[{"message":"Failed to render Fetch Input","path":["me","reviews","@","product"]}]}}],"data":{"me":null},"extensions":{"authorization":{"missingScopes":[["read:users"]]},"rateLimit":{"Policy":"policy","Allowed":0,"Used":0}}}`, func(t *testing.T) {} })) @@ -91,7 +101,11 @@ func TestExtensions(t *testing.T) { res := generateTestFederationGraphQLResponse(t, ctrl) - return res, &Context{ctx: context.Background(), Variables: nil, authorizer: authorizer, rateLimiter: limiter, RateLimitOptions: RateLimitOptions{Enable: true, IncludeStatsInResponseExtension: true}}, + resolveCtx := NewContext(context.Background()) + resolveCtx.authorizer = authorizer + resolveCtx.rateLimiter = limiter + resolveCtx.RateLimitOptions = RateLimitOptions{Enable: true, IncludeStatsInResponseExtension: true} + return res, resolveCtx, `{"errors":[{"message":"Rate limit exceeded for Subgraph 'users' at Path 'query', Reason: rate limit exceeded."},{"message":"Failed to fetch from Subgraph 'reviews' at Path 'query.me'.","extensions":{"errors":[{"message":"Failed to render Fetch Input","path":["me"]}]}},{"message":"Failed to fetch from Subgraph 'products' at Path 'query.me.reviews.@.product'.","extensions":{"errors":[{"message":"Failed to render Fetch Input","path":["me","reviews","@","product"]}]}}],"data":{"me":null},"extensions":{"rateLimit":{"Policy":"policy","Allowed":0,"Used":1}}}`, func(t *testing.T) {} })) @@ -116,7 +130,11 @@ func TestExtensions(t *testing.T) { res := generateTestFederationGraphQLResponse(t, ctrl) - ctx = &Context{ctx: context.Background(), Variables: nil, authorizer: authorizer, rateLimiter: limiter, RateLimitOptions: RateLimitOptions{Enable: true, IncludeStatsInResponseExtension: true}, TracingOptions: TraceOptions{Enable: true, IncludeTraceOutputInResponseExtensions: true, EnablePredictableDebugTimings: true, Debug: true}} + ctx = NewContext(context.Background()) + ctx.authorizer = authorizer + ctx.rateLimiter = limiter + ctx.RateLimitOptions = RateLimitOptions{Enable: true, IncludeStatsInResponseExtension: true} + ctx.TracingOptions = TraceOptions{Enable: true, IncludeTraceOutputInResponseExtensions: true, EnablePredictableDebugTimings: true, Debug: true} ctx.ctx = SetTraceStart(ctx.ctx, true) return res, ctx, diff --git a/v2/pkg/engine/resolve/loader.go b/v2/pkg/engine/resolve/loader.go index 7a14d61dce..263f997a7e 100644 --- a/v2/pkg/engine/resolve/loader.go +++ b/v2/pkg/engine/resolve/loader.go @@ -63,10 +63,10 @@ func (ri *ResponseInfo) GetResponseBody() string { return ri.responseBody.String() } -func newResponseInfo(res *result, subgraphError error) *ResponseInfo { +func newResponseInfo(res *result, subgraphErrors map[string]error) *ResponseInfo { responseInfo := &ResponseInfo{ StatusCode: res.statusCode, - Err: subgraphError, + Err: subgraphErrors[res.ds.Name], responseBody: res.out, } if res.httpResponseContext != nil { @@ -740,7 +740,7 @@ func (l *Loader) appendSubgraphError(res *result, fetchItem *FetchItem, value *a subgraphError.AppendDownstreamError(&gErr) } - l.ctx.appendSubgraphErrors(res.err, subgraphError) + l.ctx.appendSubgraphErrors(res.ds, res.err, subgraphError) return nil } @@ -1088,7 +1088,7 @@ func (l *Loader) renderErrorsFailedDeps(fetchItem *FetchItem, res *result) error } func (l *Loader) renderErrorsFailedToFetch(fetchItem *FetchItem, res *result, reason string) error { - l.ctx.appendSubgraphErrors(res.err, NewSubgraphError(res.ds, fetchItem.ResponsePath, reason, res.statusCode)) + l.ctx.appendSubgraphErrors(res.ds, res.err, NewSubgraphError(res.ds, fetchItem.ResponsePath, reason, res.statusCode)) errorObject, err := astjson.ParseWithoutCache(l.renderSubgraphBaseError(res.ds, fetchItem.ResponsePath, reason)) if err != nil { return err @@ -1104,7 +1104,7 @@ func (l *Loader) renderErrorsStatusFallback(fetchItem *FetchItem, res *result, s reason += fmt.Sprintf(": %s", statusText) } - l.ctx.appendSubgraphErrors(res.err, NewSubgraphError(res.ds, fetchItem.ResponsePath, reason, res.statusCode)) + l.ctx.appendSubgraphErrors(res.ds, res.err, NewSubgraphError(res.ds, fetchItem.ResponsePath, reason, res.statusCode)) errorObject, err := astjson.ParseWithoutCache(fmt.Sprintf(`{"message":"%s"}`, reason)) if err != nil { @@ -1133,7 +1133,7 @@ func (l *Loader) renderSubgraphBaseError(ds DataSourceInfo, path, reason string) func (l *Loader) renderAuthorizationRejectedErrors(fetchItem *FetchItem, res *result) error { for i := range res.authorizationRejectedReasons { - l.ctx.appendSubgraphErrors(res.err, NewSubgraphError(res.ds, fetchItem.ResponsePath, res.authorizationRejectedReasons[i], res.statusCode)) + l.ctx.appendSubgraphErrors(res.ds, res.err, NewSubgraphError(res.ds, fetchItem.ResponsePath, res.authorizationRejectedReasons[i], res.statusCode)) } pathPart := l.renderAtPathErrorPart(fetchItem.ResponsePath) extensionErrorCode := fmt.Sprintf(`"extensions":{"code":"%s"}`, errorcodes.UnauthorizedFieldOrType) @@ -1174,7 +1174,7 @@ func (l *Loader) renderAuthorizationRejectedErrors(fetchItem *FetchItem, res *re } func (l *Loader) renderRateLimitRejectedErrors(fetchItem *FetchItem, res *result) error { - l.ctx.appendSubgraphErrors(res.err, NewRateLimitError(res.ds.Name, fetchItem.ResponsePath, res.rateLimitRejectedReason)) + l.ctx.appendSubgraphErrors(res.ds, res.err, NewRateLimitError(res.ds.Name, fetchItem.ResponsePath, res.rateLimitRejectedReason)) pathPart := l.renderAtPathErrorPart(fetchItem.ResponsePath) var ( err error diff --git a/v2/pkg/engine/resolve/loader_hooks_test.go b/v2/pkg/engine/resolve/loader_hooks_test.go index 4b7b3ea6c5..fa61417b3f 100644 --- a/v2/pkg/engine/resolve/loader_hooks_test.go +++ b/v2/pkg/engine/resolve/loader_hooks_test.go @@ -56,10 +56,8 @@ func TestLoaderHooks_FetchPipeline(t *testing.T) { pair.WriteErr([]byte("errorMessage"), nil, nil, nil) return writeGraphqlResponse(pair, w, false) }) - resolveCtx := Context{ - ctx: context.Background(), - LoaderHooks: NewTestLoaderHooks(), - } + resolveCtx := NewContext(context.Background()) + resolveCtx.LoaderHooks = NewTestLoaderHooks() return &GraphQLResponse{ Info: &GraphQLResponseInfo{ OperationType: ast.OperationTypeQuery, @@ -88,7 +86,7 @@ func TestLoaderHooks_FetchPipeline(t *testing.T) { }, }, }, - }, &resolveCtx, `{"errors":[{"message":"Failed to fetch from Subgraph 'Users' at Path 'query'.","extensions":{"errors":[{"message":"errorMessage"}]}}],"data":{"name":null}}`, + }, resolveCtx, `{"errors":[{"message":"Failed to fetch from Subgraph 'Users' at Path 'query'.","extensions":{"errors":[{"message":"errorMessage"}]}}],"data":{"name":null}}`, func(t *testing.T) { loaderHooks := resolveCtx.LoaderHooks.(*TestLoaderHooks) @@ -130,10 +128,8 @@ func TestLoaderHooks_FetchPipeline(t *testing.T) { pair.WriteErr([]byte("errorMessage"), nil, nil, nil) return writeGraphqlResponse(pair, w, false) }) - resolveCtx := &Context{ - ctx: context.Background(), - LoaderHooks: NewTestLoaderHooks(), - } + resolveCtx := NewContext(context.Background()) + resolveCtx.LoaderHooks = NewTestLoaderHooks() resp := &GraphQLResponse{ Info: &GraphQLResponseInfo{ OperationType: ast.OperationTypeQuery, @@ -198,10 +194,8 @@ func TestLoaderHooks_FetchPipeline(t *testing.T) { pair.WriteErr([]byte("errorMessage"), nil, nil, nil) return writeGraphqlResponse(pair, w, false) }) - resolveCtx := &Context{ - ctx: context.Background(), - LoaderHooks: NewTestLoaderHooks(), - } + resolveCtx := NewContext(context.Background()) + resolveCtx.LoaderHooks = NewTestLoaderHooks() return &GraphQLResponse{ Info: &GraphQLResponseInfo{ OperationType: ast.OperationTypeQuery, @@ -263,10 +257,8 @@ func TestLoaderHooks_FetchPipeline(t *testing.T) { pair.WriteErr([]byte("errorMessage"), nil, nil, nil) return writeGraphqlResponse(pair, w, false) }) - resolveCtx := Context{ - ctx: context.Background(), - LoaderHooks: NewTestLoaderHooks(), - } + resolveCtx := NewContext(context.Background()) + resolveCtx.LoaderHooks = NewTestLoaderHooks() return &GraphQLResponse{ Info: &GraphQLResponseInfo{ OperationType: ast.OperationTypeQuery, @@ -297,7 +289,7 @@ func TestLoaderHooks_FetchPipeline(t *testing.T) { }, }, }, - }, &resolveCtx, `{"errors":[{"message":"Failed to fetch from Subgraph 'Users' at Path 'query'.","extensions":{"errors":[{"message":"errorMessage"}]}}],"data":{"name":null}}`, + }, resolveCtx, `{"errors":[{"message":"Failed to fetch from Subgraph 'Users' at Path 'query'.","extensions":{"errors":[{"message":"errorMessage"}]}}],"data":{"name":null}}`, func(t *testing.T) { loaderHooks := resolveCtx.LoaderHooks.(*TestLoaderHooks) @@ -329,10 +321,8 @@ func TestLoaderHooks_FetchPipeline(t *testing.T) { pair.WriteErr([]byte("errorMessage2"), nil, nil, []byte("{\"code\":\"BAD_USER_INPUT\"}")) return writeGraphqlResponse(pair, w, false) }) - resolveCtx := Context{ - ctx: context.Background(), - LoaderHooks: NewTestLoaderHooks(), - } + resolveCtx := NewContext(context.Background()) + resolveCtx.LoaderHooks = NewTestLoaderHooks() return &GraphQLResponse{ Info: &GraphQLResponseInfo{ OperationType: ast.OperationTypeQuery, @@ -361,7 +351,7 @@ func TestLoaderHooks_FetchPipeline(t *testing.T) { }, }, }, - }, &resolveCtx, `{"errors":[{"message":"Failed to fetch from Subgraph 'Users' at Path 'query'.","extensions":{"errors":[{"message":"errorMessage"},{"message":"errorMessage2"}]}}],"data":{"name":null}}`, + }, resolveCtx, `{"errors":[{"message":"Failed to fetch from Subgraph 'Users' at Path 'query'.","extensions":{"errors":[{"message":"errorMessage"},{"message":"errorMessage2"}]}}],"data":{"name":null}}`, func(t *testing.T) { loaderHooks := resolveCtx.LoaderHooks.(*TestLoaderHooks) @@ -420,7 +410,7 @@ func TestLoaderHooks_FetchPipeline(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"errors":[{"message":"errorMessage","extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}},{"message":"errorMessage2","extensions":{"code":"BAD_USER_INPUT"}}],"data":{"name":null}}` + }, *NewContext(context.Background()), `{"errors":[{"message":"errorMessage","extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}},{"message":"errorMessage2","extensions":{"code":"BAD_USER_INPUT"}}],"data":{"name":null}}` })) t.Run("Propagate all extension fields from subgraph errors when allow all option is enabled", testFnSubgraphErrorsWithAllowAllExtensionFields(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { @@ -458,7 +448,7 @@ func TestLoaderHooks_FetchPipeline(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"errors":[{"message":"errorMessage","extensions":{"code":"GRAPHQL_VALIDATION_FAILED","foo":"bar"}},{"message":"errorMessage2","extensions":{"code":"BAD_USER_INPUT"}}],"data":{"name":null}}` + }, *NewContext(context.Background()), `{"errors":[{"message":"errorMessage","extensions":{"code":"GRAPHQL_VALIDATION_FAILED","foo":"bar"}},{"message":"errorMessage2","extensions":{"code":"BAD_USER_INPUT"}}],"data":{"name":null}}` })) t.Run("Include datasource name as serviceName extension field", testFnSubgraphErrorsWithExtensionFieldServiceName(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { @@ -496,7 +486,7 @@ func TestLoaderHooks_FetchPipeline(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"errors":[{"message":"errorMessage","extensions":{"code":"GRAPHQL_VALIDATION_FAILED","serviceName":"Users"}},{"message":"errorMessage2","extensions":{"code":"BAD_USER_INPUT","serviceName":"Users"}}],"data":{"name":null}}` + }, *NewContext(context.Background()), `{"errors":[{"message":"errorMessage","extensions":{"code":"GRAPHQL_VALIDATION_FAILED","serviceName":"Users"}},{"message":"errorMessage2","extensions":{"code":"BAD_USER_INPUT","serviceName":"Users"}}],"data":{"name":null}}` })) t.Run("Include datasource name as serviceName when extensions is null", testFnSubgraphErrorsWithExtensionFieldServiceName(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { @@ -534,7 +524,7 @@ func TestLoaderHooks_FetchPipeline(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"errors":[{"message":"errorMessage","extensions":{"code":"DOWNSTREAM_SERVICE_ERROR","serviceName":"Users"}},{"message":"errorMessage2","extensions":{"code":"DOWNSTREAM_SERVICE_ERROR","serviceName":"Users"}}],"data":{"name":null}}` + }, *NewContext(context.Background()), `{"errors":[{"message":"errorMessage","extensions":{"code":"DOWNSTREAM_SERVICE_ERROR","serviceName":"Users"}},{"message":"errorMessage2","extensions":{"code":"DOWNSTREAM_SERVICE_ERROR","serviceName":"Users"}}],"data":{"name":null}}` })) t.Run("Include datasource name as serviceName when extensions is an empty object", testFnSubgraphErrorsWithExtensionFieldServiceName(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { @@ -572,7 +562,7 @@ func TestLoaderHooks_FetchPipeline(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"errors":[{"message":"errorMessage","extensions":{"code":"DOWNSTREAM_SERVICE_ERROR","serviceName":"Users"}},{"message":"errorMessage2","extensions":{"code":"DOWNSTREAM_SERVICE_ERROR","serviceName":"Users"}}],"data":{"name":null}}` + }, *NewContext(context.Background()), `{"errors":[{"message":"errorMessage","extensions":{"code":"DOWNSTREAM_SERVICE_ERROR","serviceName":"Users"}},{"message":"errorMessage2","extensions":{"code":"DOWNSTREAM_SERVICE_ERROR","serviceName":"Users"}}],"data":{"name":null}}` })) t.Run("Fallback to default extension code value when no code field was set", testFnSubgraphErrorsWithExtensionDefaultCode(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { @@ -610,7 +600,7 @@ func TestLoaderHooks_FetchPipeline(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"errors":[{"message":"errorMessage","extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}},{"message":"errorMessage2","extensions":{"code":"DOWNSTREAM_SERVICE_ERROR"}}],"data":{"name":null}}` + }, *NewContext(context.Background()), `{"errors":[{"message":"errorMessage","extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}},{"message":"errorMessage2","extensions":{"code":"DOWNSTREAM_SERVICE_ERROR"}}],"data":{"name":null}}` })) t.Run("Fallback to default extension code value when extensions is null", testFnSubgraphErrorsWithExtensionDefaultCode(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { @@ -648,7 +638,7 @@ func TestLoaderHooks_FetchPipeline(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"errors":[{"message":"errorMessage","extensions":{"code":"DOWNSTREAM_SERVICE_ERROR"}},{"message":"errorMessage2","extensions":{"code":"DOWNSTREAM_SERVICE_ERROR"}}],"data":{"name":null}}` + }, *NewContext(context.Background()), `{"errors":[{"message":"errorMessage","extensions":{"code":"DOWNSTREAM_SERVICE_ERROR"}},{"message":"errorMessage2","extensions":{"code":"DOWNSTREAM_SERVICE_ERROR"}}],"data":{"name":null}}` })) t.Run("Fallback to default extension code value when extensions is an empty object", testFnSubgraphErrorsWithExtensionDefaultCode(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { @@ -686,7 +676,7 @@ func TestLoaderHooks_FetchPipeline(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"errors":[{"message":"errorMessage","extensions":{"code":"DOWNSTREAM_SERVICE_ERROR"}},{"message":"errorMessage2","extensions":{"code":"DOWNSTREAM_SERVICE_ERROR"}}],"data":{"name":null}}` + }, *NewContext(context.Background()), `{"errors":[{"message":"errorMessage","extensions":{"code":"DOWNSTREAM_SERVICE_ERROR"}},{"message":"errorMessage2","extensions":{"code":"DOWNSTREAM_SERVICE_ERROR"}}],"data":{"name":null}}` })) } diff --git a/v2/pkg/engine/resolve/loader_test.go b/v2/pkg/engine/resolve/loader_test.go index 4ed83d4443..b19aaafec5 100644 --- a/v2/pkg/engine/resolve/loader_test.go +++ b/v2/pkg/engine/resolve/loader_test.go @@ -284,9 +284,7 @@ func TestLoader_LoadGraphQLResponseData(t *testing.T) { }, }, } - ctx := &Context{ - ctx: context.Background(), - } + ctx := NewContext(context.Background()) resolvable := NewResolvable(ResolvableOptions{}) loader := &Loader{} err := resolvable.Init(ctx, nil, ast.OperationTypeQuery) @@ -373,9 +371,7 @@ func TestLoader_MergeErrorDifferingTypes(t *testing.T) { }, }, } - ctx := &Context{ - ctx: context.Background(), - } + ctx := NewContext(context.Background()) resolvable := NewResolvable(ResolvableOptions{}) loader := &Loader{} err := resolvable.Init(ctx, nil, ast.OperationTypeQuery) @@ -464,9 +460,7 @@ func TestLoader_MergeErrorDifferingArrayLength(t *testing.T) { }, }, } - ctx := &Context{ - ctx: context.Background(), - } + ctx := NewContext(context.Background()) resolvable := NewResolvable(ResolvableOptions{}) loader := &Loader{} err := resolvable.Init(ctx, nil, ast.OperationTypeQuery) @@ -745,10 +739,8 @@ func TestLoader_LoadGraphQLResponseDataWithExtensions(t *testing.T) { }, }, } - ctx := &Context{ - ctx: context.Background(), - Extensions: []byte(`{"foo":"bar"}`), - } + ctx := NewContext(context.Background()) + ctx.Extensions = []byte(`{"foo":"bar"}`) resolvable := NewResolvable(ResolvableOptions{}) loader := &Loader{} err := resolvable.Init(ctx, nil, ast.OperationTypeQuery) @@ -1021,9 +1013,7 @@ func BenchmarkLoader_LoadGraphQLResponseData(b *testing.B) { }, }, } - ctx := &Context{ - ctx: context.Background(), - } + ctx := NewContext(context.Background()) resolvable := NewResolvable(ResolvableOptions{}) loader := &Loader{} expected := `{"errors":[],"data":{"topProducts":[{"name":"Table","__typename":"Product","upc":"1","reviews":[{"body":"Love Table!","author":{"__typename":"User","id":"1","name":"user-1"}},{"body":"Prefer other Table.","author":{"__typename":"User","id":"2","name":"user-2"}}],"stock":8},{"name":"Couch","__typename":"Product","upc":"2","reviews":[{"body":"Couch Too expensive.","author":{"__typename":"User","id":"1","name":"user-1"}}],"stock":2},{"name":"Chair","__typename":"Product","upc":"3","reviews":[{"body":"Chair Could be better.","author":{"__typename":"User","id":"2","name":"user-2"}}],"stock":5}]}}` @@ -1116,14 +1106,12 @@ func TestLoader_RedactHeaders(t *testing.T) { }, } - ctx := &Context{ - ctx: context.Background(), - Request: Request{ - Header: http.Header{"Authorization": []string{"value"}}, - }, - TracingOptions: TraceOptions{ - Enable: true, - }, + ctx := NewContext(context.Background()) + ctx.Request = Request{ + Header: http.Header{"Authorization": []string{"value"}}, + } + ctx.TracingOptions = TraceOptions{ + Enable: true, } resolvable := NewResolvable(ResolvableOptions{}) loader := &Loader{} @@ -1418,9 +1406,7 @@ func TestLoader_InvalidBatchItemCount(t *testing.T) { }, }, } - ctx := &Context{ - ctx: context.Background(), - } + ctx := NewContext(context.Background()) resolvable := NewResolvable(ResolvableOptions{}) loader := &Loader{} err := resolvable.Init(ctx, nil, ast.OperationTypeQuery) diff --git a/v2/pkg/engine/resolve/ratelimit_test.go b/v2/pkg/engine/resolve/ratelimit_test.go index b7a6b00d41..d72f842bd3 100644 --- a/v2/pkg/engine/resolve/ratelimit_test.go +++ b/v2/pkg/engine/resolve/ratelimit_test.go @@ -55,7 +55,10 @@ func TestRateLimiter(t *testing.T) { res := generateTestFederationGraphQLResponse(t, ctrl) - return res, &Context{ctx: context.Background(), Variables: nil, rateLimiter: limiter, RateLimitOptions: RateLimitOptions{Enable: true}}, + resolveCtx := NewContext(context.Background()) + resolveCtx.rateLimiter = limiter + resolveCtx.RateLimitOptions = RateLimitOptions{Enable: true} + return res, resolveCtx, `{"data":{"me":{"id":"1234","username":"Me","reviews":[{"body":"A highly effective form of birth control.","product":{"upc":"top-1","name":"Trilby"}},{"body":"Fedoras are one of the most fashionable hats around and can look great with a variety of outfits.","product":{"upc":"top-2","name":"Fedora"}}]}}}`, func(t *testing.T) { assert.Equal(t, int64(3), limiter.rateLimitPreFetchCalls.Load()) @@ -73,7 +76,10 @@ func TestRateLimiter(t *testing.T) { res := generateTestFederationGraphQLResponse(t, ctrl) - return res, &Context{ctx: context.Background(), Variables: nil, rateLimiter: limiter, RateLimitOptions: RateLimitOptions{Enable: true, IncludeStatsInResponseExtension: true}}, + resolveCtx := NewContext(context.Background()) + resolveCtx.rateLimiter = limiter + resolveCtx.RateLimitOptions = RateLimitOptions{Enable: true, IncludeStatsInResponseExtension: true} + return res, resolveCtx, `{"data":{"me":{"id":"1234","username":"Me","reviews":[{"body":"A highly effective form of birth control.","product":{"upc":"top-1","name":"Trilby"}},{"body":"Fedoras are one of the most fashionable hats around and can look great with a variety of outfits.","product":{"upc":"top-2","name":"Fedora"}}]}},"extensions":{"rateLimit":{"Policy":"10 requests per second","Allowed":10,"Used":3}}}`, func(t *testing.T) { assert.Equal(t, int64(3), limiter.rateLimitPreFetchCalls.Load()) @@ -89,7 +95,10 @@ func TestRateLimiter(t *testing.T) { res := generateTestFederationGraphQLResponse(t, ctrl) - return res, &Context{ctx: context.Background(), Variables: nil, rateLimiter: limiter, RateLimitOptions: RateLimitOptions{Enable: true}}, + resolveCtx := NewContext(context.Background()) + resolveCtx.rateLimiter = limiter + resolveCtx.RateLimitOptions = RateLimitOptions{Enable: true} + return res, resolveCtx, `{"errors":[{"message":"Rate limit exceeded for Subgraph 'users' at Path 'query', Reason: rate limit exceeded."},{"message":"Failed to fetch from Subgraph 'reviews' at Path 'query.me'.","extensions":{"errors":[{"message":"Failed to render Fetch Input","path":["me"]}]}},{"message":"Failed to fetch from Subgraph 'products' at Path 'query.me.reviews.@.product'.","extensions":{"errors":[{"message":"Failed to render Fetch Input","path":["me","reviews","@","product"]}]}}],"data":{"me":null}}`, func(t *testing.T) { assert.Equal(t, int64(1), limiter.rateLimitPreFetchCalls.Load()) @@ -105,7 +114,10 @@ func TestRateLimiter(t *testing.T) { res := generateTestFederationGraphQLResponse(t, ctrl) - return res, &Context{ctx: context.Background(), Variables: nil, rateLimiter: limiter, RateLimitOptions: RateLimitOptions{Enable: true, ErrorExtensionCode: RateLimitErrorExtensionCode{Enabled: true, Code: "RATE_LIMIT_EXCEEDED"}}}, + resolveCtx := NewContext(context.Background()) + resolveCtx.rateLimiter = limiter + resolveCtx.RateLimitOptions = RateLimitOptions{Enable: true, ErrorExtensionCode: RateLimitErrorExtensionCode{Enabled: true, Code: "RATE_LIMIT_EXCEEDED"}} + return res, resolveCtx, `{"errors":[{"message":"Rate limit exceeded for Subgraph 'users' at Path 'query', Reason: rate limit exceeded.","extensions":{"code":"RATE_LIMIT_EXCEEDED"}},{"message":"Failed to fetch from Subgraph 'reviews' at Path 'query.me'.","extensions":{"errors":[{"message":"Failed to render Fetch Input","path":["me"]}]}},{"message":"Failed to fetch from Subgraph 'products' at Path 'query.me.reviews.@.product'.","extensions":{"errors":[{"message":"Failed to render Fetch Input","path":["me","reviews","@","product"]}]}}],"data":{"me":null}}`, func(t *testing.T) { assert.Equal(t, int64(1), limiter.rateLimitPreFetchCalls.Load()) @@ -121,7 +133,10 @@ func TestRateLimiter(t *testing.T) { res := generateTestFederationGraphQLResponse(t, ctrl) - return res, Context{ctx: context.Background(), Variables: nil, rateLimiter: limiter, RateLimitOptions: RateLimitOptions{Enable: true}}, "" + resolveCtx := NewContext(context.Background()) + resolveCtx.rateLimiter = limiter + resolveCtx.RateLimitOptions = RateLimitOptions{Enable: true} + return res, *resolveCtx, "" })) t.Run("deny nested", testFnWithPostEvaluation(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx *Context, expectedOutput string, postEvaluation func(t *testing.T)) { @@ -140,7 +155,10 @@ func TestRateLimiter(t *testing.T) { res := generateTestFederationGraphQLResponse(t, ctrl) - return res, &Context{ctx: context.Background(), Variables: nil, rateLimiter: limiter, RateLimitOptions: RateLimitOptions{Enable: true}}, + resolveCtx := NewContext(context.Background()) + resolveCtx.rateLimiter = limiter + resolveCtx.RateLimitOptions = RateLimitOptions{Enable: true} + return res, resolveCtx, `{"errors":[{"message":"Rate limit exceeded for Subgraph 'products' at Path 'query.me.reviews.@.product', Reason: rate limit exceeded."}],"data":{"me":{"id":"1234","username":"Me","reviews":[null,null]}}}`, func(t *testing.T) { assert.Equal(t, int64(3), limiter.rateLimitPreFetchCalls.Load()) @@ -165,7 +183,10 @@ func TestRateLimiter(t *testing.T) { res := generateTestFederationGraphQLResponse(t, ctrl) - return res, &Context{ctx: context.Background(), Variables: nil, rateLimiter: limiter, RateLimitOptions: RateLimitOptions{Enable: true, IncludeStatsInResponseExtension: true}}, + resolveCtx := NewContext(context.Background()) + resolveCtx.rateLimiter = limiter + resolveCtx.RateLimitOptions = RateLimitOptions{Enable: true, IncludeStatsInResponseExtension: true} + return res, resolveCtx, `{"errors":[{"message":"Rate limit exceeded for Subgraph 'products' at Path 'query.me.reviews.@.product', Reason: rate limit exceeded."}],"data":{"me":{"id":"1234","username":"Me","reviews":[null,null]}},"extensions":{"rateLimit":{"Policy":"1 request per second","Allowed":1,"Used":3}}}`, func(t *testing.T) { assert.Equal(t, int64(3), limiter.rateLimitPreFetchCalls.Load()) @@ -188,7 +209,10 @@ func TestRateLimiter(t *testing.T) { res := generateTestFederationGraphQLResponse(t, ctrl) - return res, &Context{ctx: context.Background(), Variables: nil, rateLimiter: limiter, RateLimitOptions: RateLimitOptions{Enable: true}}, + resolveCtx := NewContext(context.Background()) + resolveCtx.rateLimiter = limiter + resolveCtx.RateLimitOptions = RateLimitOptions{Enable: true} + return res, resolveCtx, `{"errors":[{"message":"Rate limit exceeded for Subgraph 'products' at Path 'query.me.reviews.@.product'."}],"data":{"me":{"id":"1234","username":"Me","reviews":[null,null]}}}`, func(t *testing.T) { assert.Equal(t, int64(3), limiter.rateLimitPreFetchCalls.Load()) @@ -211,6 +235,9 @@ func TestRateLimiter(t *testing.T) { res := generateTestFederationGraphQLResponse(t, ctrl) - return res, Context{ctx: context.Background(), Variables: nil, rateLimiter: limiter, RateLimitOptions: RateLimitOptions{Enable: true}}, "" + resolveCtx := NewContext(context.Background()) + resolveCtx.rateLimiter = limiter + resolveCtx.RateLimitOptions = RateLimitOptions{Enable: true} + return res, *resolveCtx, "" })) } diff --git a/v2/pkg/engine/resolve/resolvable.go b/v2/pkg/engine/resolve/resolvable.go index 5219c910d1..13df1db315 100644 --- a/v2/pkg/engine/resolve/resolvable.go +++ b/v2/pkg/engine/resolve/resolvable.go @@ -758,7 +758,7 @@ func (r *Resolvable) addRejectFieldError(reason string, ds DataSourceInfo, field } else { errorMessage = fmt.Sprintf("Unauthorized to load field '%s', Reason: %s.", fieldPath, reason) } - r.ctx.appendSubgraphErrors(errors.New(errorMessage), + r.ctx.appendSubgraphErrors(ds, errors.New(errorMessage), NewSubgraphError(ds, fieldPath, reason, 0)) fastjsonext.AppendErrorWithExtensionsCodeToArray(r.astjsonArena, r.errors, errorMessage, errorcodes.UnauthorizedFieldOrType, r.path) r.popNodePathElement(nodePath) diff --git a/v2/pkg/engine/resolve/resolve_federation_test.go b/v2/pkg/engine/resolve/resolve_federation_test.go index 2547c6d104..2afdef6725 100644 --- a/v2/pkg/engine/resolve/resolve_federation_test.go +++ b/v2/pkg/engine/resolve/resolve_federation_test.go @@ -176,7 +176,7 @@ func TestResolveGraphQLResponse_Federation(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"data":{"user":{"account":{"name":"John Doe","shippingInfo":{"zip":"12345"}}}}}` + }, *NewContext(context.Background()), `{"data":{"user":{"account":{"name":"John Doe","shippingInfo":{"zip":"12345"}}}}}` })) t.Run("federation with shareable", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { @@ -367,7 +367,7 @@ func TestResolveGraphQLResponse_Federation(t *testing.T) { }, }, }, - }, Context{ctx: context.Background(), Variables: nil}, `{"data":{"me":{"details":{"forename":"John","surname":"Smith","middlename":"A","age":21}}}}` + }, *NewContext(context.Background()), `{"data":{"me":{"details":{"forename":"John","surname":"Smith","middlename":"A","age":21}}}}` })) }) @@ -523,7 +523,7 @@ func TestResolveGraphQLResponse_Federation(t *testing.T) { }, }, }, - }, Context{ctx: context.Background(), Variables: nil}, `{"data":{"user":{"name":"Bill","infoOrAddress":[{"age":21},{"line1":"Munich"}]}}}` + }, *NewContext(context.Background()), `{"data":{"user":{"name":"Bill","infoOrAddress":[{"age":21},{"line1":"Munich"}]}}}` })) t.Run("batching on union - all not matching items", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { @@ -669,7 +669,7 @@ func TestResolveGraphQLResponse_Federation(t *testing.T) { }, }, }, - }, Context{ctx: context.Background(), Variables: nil}, `{"data":{"user":{"name":"Bill","infoOrAddress":[{},{}]}}}` + }, *NewContext(context.Background()), `{"data":{"user":{"name":"Bill","infoOrAddress":[{},{}]}}}` })) t.Run("batching on a field", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { @@ -813,7 +813,7 @@ func TestResolveGraphQLResponse_Federation(t *testing.T) { }, }, }, - }, Context{ctx: context.Background(), Variables: nil}, `{"data":{"users":[{"name":"Bill","info":{"age":21}},{"name":"John","info":{"age":22}},{"name":"Jane","info":{"age":23}}]}}` + }, *NewContext(context.Background()), `{"data":{"users":[{"name":"Bill","info":{"age":21}},{"name":"John","info":{"age":22}},{"name":"Jane","info":{"age":23}}]}}` })) t.Run("batching with duplicates", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { @@ -954,7 +954,7 @@ func TestResolveGraphQLResponse_Federation(t *testing.T) { }, }, }, - }, Context{ctx: context.Background(), Variables: nil}, `{"data":{"users":[{"name":"Bill","info":{"age":77}},{"name":"John","info":{"age":77}},{"name":"Jane","info":{"age":77}}]}}` + }, *NewContext(context.Background()), `{"data":{"users":[{"name":"Bill","info":{"age":77}},{"name":"John","info":{"age":77}},{"name":"Jane","info":{"age":77}}]}}` })) t.Run("batching with null entry", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { @@ -1099,7 +1099,7 @@ func TestResolveGraphQLResponse_Federation(t *testing.T) { }, }, }, - }, Context{ctx: context.Background(), Variables: nil}, `{"data":{"users":[{"name":"Bill","info":{"age":21}},{"name":"John","info":null},{"name":"Jane","info":{"age":23}}]}}` + }, *NewContext(context.Background()), `{"data":{"users":[{"name":"Bill","info":{"age":21}},{"name":"John","info":null},{"name":"Jane","info":{"age":23}}]}}` })) t.Run("batching with all null entries", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { @@ -1237,7 +1237,7 @@ func TestResolveGraphQLResponse_Federation(t *testing.T) { }, }, }, - }, Context{ctx: context.Background(), Variables: nil}, `{"data":{"users":[{"name":"Bill","info":null},{"name":"John","info":null},{"name":"Jane","info":null}]}}` + }, *NewContext(context.Background()), `{"data":{"users":[{"name":"Bill","info":null},{"name":"John","info":null},{"name":"Jane","info":null}]}}` })) t.Run("batching with render error", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { @@ -1382,7 +1382,7 @@ func TestResolveGraphQLResponse_Federation(t *testing.T) { }, }, }, - }, Context{ctx: context.Background(), Variables: nil}, `{"errors":[{"message":"Cannot return null for non-nullable field 'Query.users.info.age'.","path":["users",0,"info","age"]}],"data":null}` + }, *NewContext(context.Background()), `{"errors":[{"message":"Cannot return null for non-nullable field 'Query.users.info.age'.","path":["users",0,"info","age"]}],"data":null}` })) }) @@ -1518,7 +1518,7 @@ func TestResolveGraphQLResponse_Federation(t *testing.T) { }, }, }, - }, Context{ctx: context.Background(), Variables: nil}, `{"data":{"user":{"name":"Bill","info":{"age":21}}}}` + }, *NewContext(context.Background()), `{"data":{"user":{"name":"Bill","info":{"age":21}}}}` })) t.Run("null info data", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { @@ -1646,7 +1646,7 @@ func TestResolveGraphQLResponse_Federation(t *testing.T) { }, }, }, - }, Context{ctx: context.Background(), Variables: nil}, `{"data":{"user":{"name":"Bill","info":null}}}` + }, *NewContext(context.Background()), `{"data":{"user":{"name":"Bill","info":null}}}` })) t.Run("wrong type data", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { @@ -1774,7 +1774,7 @@ func TestResolveGraphQLResponse_Federation(t *testing.T) { }, }, }, - }, Context{ctx: context.Background(), Variables: nil}, `{"errors":[{"message":"Cannot return null for non-nullable field 'Query.user.info.age'.","path":["user","info","age"]}],"data":{"user":{"name":"Bill","info":null}}}` + }, *NewContext(context.Background()), `{"errors":[{"message":"Cannot return null for non-nullable field 'Query.user.info.age'.","path":["user","info","age"]}],"data":{"user":{"name":"Bill","info":null}}}` })) t.Run("not matching type data", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { @@ -1903,7 +1903,7 @@ func TestResolveGraphQLResponse_Federation(t *testing.T) { }, }, }, - }, Context{ctx: context.Background(), Variables: nil}, `{"data":{"user":{"name":"Bill","info":{}}}}` + }, *NewContext(context.Background()), `{"data":{"user":{"name":"Bill","info":{}}}}` })) }) }) @@ -2145,7 +2145,7 @@ func TestResolveGraphQLResponse_Federation(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"data":{"user":{"account":{"address":{"fullAddress":"line1 line2 line3-1 city-1 country-1 zip-1"}}}}}` + }, *NewContext(context.Background()), `{"data":{"user":{"account":{"address":{"fullAddress":"line1 line2 line3-1 city-1 country-1 zip-1"}}}}}` })) t.Run("nested batching", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { @@ -2417,7 +2417,7 @@ func TestResolveGraphQLResponse_Federation(t *testing.T) { }, }, }, - }, Context{ctx: context.Background(), Variables: nil}, `{"data":{"topProducts":[{"name":"Table","stock":8,"reviews":[{"body":"Love Table!","author":{"name":"user-1"}},{"body":"Prefer other Table.","author":{"name":"user-2"}}]},{"name":"Couch","stock":2,"reviews":[{"body":"Couch Too expensive.","author":{"name":"user-1"}}]},{"name":"Chair","stock":5,"reviews":[{"body":"Chair Could be better.","author":{"name":"user-2"}}]}]}}` + }, *NewContext(context.Background()), `{"data":{"topProducts":[{"name":"Table","stock":8,"reviews":[{"body":"Love Table!","author":{"name":"user-1"}},{"body":"Prefer other Table.","author":{"name":"user-2"}}]},{"name":"Couch","stock":2,"reviews":[{"body":"Couch Too expensive.","author":{"name":"user-1"}}]},{"name":"Chair","stock":5,"reviews":[{"body":"Chair Could be better.","author":{"name":"user-2"}}]}]}}` })) t.Run("nested batching single root result", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { @@ -2689,7 +2689,7 @@ func TestResolveGraphQLResponse_Federation(t *testing.T) { }, }, }, - }, Context{ctx: context.Background(), Variables: nil}, `{"data":{"topProducts":[{"name":"Table","stock":8,"reviews":[{"body":"Love Table!","author":{"name":"user-1"}}]}]}}` + }, *NewContext(context.Background()), `{"data":{"topProducts":[{"name":"Table","stock":8,"reviews":[{"body":"Love Table!","author":{"name":"user-1"}}]}]}}` })) t.Run("nested batching of direct array children", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { @@ -2829,7 +2829,7 @@ func TestResolveGraphQLResponse_Federation(t *testing.T) { }, }, }, - }, Context{ctx: context.Background(), Variables: nil}, `{"data":{"accounts":[{"name":"User"},{"type":"super"},{"subject":"posts"}]}}` + }, *NewContext(context.Background()), `{"data":{"accounts":[{"name":"User"},{"type":"super"},{"subject":"posts"}]}}` })) t.Run("nested fetch should apply to only single type", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { @@ -2986,6 +2986,6 @@ func TestResolveGraphQLResponse_Federation(t *testing.T) { }, }, }, - }, Context{ctx: context.Background(), Variables: nil}, `{"data":{"accounts":[{"some":{"title":"User1"}},{"some":{"__typename":"User","id":"2"}},{"some":{"title":"User3"}}]}}` + }, *NewContext(context.Background()), `{"data":{"accounts":[{"some":{"title":"User1"}},{"some":{"__typename":"User","id":"2"}},{"some":{"title":"User3"}}]}}` })) } diff --git a/v2/pkg/engine/resolve/resolve_test.go b/v2/pkg/engine/resolve/resolve_test.go index 967dd0b0cc..78fe5188a4 100644 --- a/v2/pkg/engine/resolve/resolve_test.go +++ b/v2/pkg/engine/resolve/resolve_test.go @@ -167,12 +167,12 @@ func TestResolver_ResolveNode(t *testing.T) { Data: &Object{ Nullable: true, }, - }, Context{ctx: context.Background()}, `{"data":{}}` + }, *NewContext(context.Background()), `{"data":{}}` })) t.Run("empty object", testFn(false, func(t *testing.T, ctrl *gomock.Controller) (response *GraphQLResponse, ctx Context, expectedOutput string) { return &GraphQLResponse{ Data: &Object{}, - }, Context{ctx: context.Background()}, `{"data":{}}` + }, *NewContext(context.Background()), `{"data":{}}` })) t.Run("BigInt", testFn(false, func(t *testing.T, ctrl *gomock.Controller) (response *GraphQLResponse, ctx Context, expectedOutput string) { return &GraphQLResponse{ @@ -206,7 +206,7 @@ func TestResolver_ResolveNode(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"data":{"n":12345,"ns_small":"12346","ns_big":"1152921504606846976"}}` + }, *NewContext(context.Background()), `{"data":{"n":12345,"ns_small":"12346","ns_big":"1152921504606846976"}}` })) t.Run("Scalar", testFn(false, func(t *testing.T, ctrl *gomock.Controller) (response *GraphQLResponse, ctx Context, expectedOutput string) { return &GraphQLResponse{ @@ -266,7 +266,7 @@ func TestResolver_ResolveNode(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"data":{"int":12345,"float":3.5,"int_str":"12346","bigint_str":"1152921504606846976","str":"value","object":{"foo":"bar"},"encoded_object":"{\"foo\": \"bar\"}"}}` + }, *NewContext(context.Background()), `{"data":{"int":12345,"float":3.5,"int_str":"12346","bigint_str":"1152921504606846976","str":"value","object":{"foo":"bar"},"encoded_object":"{\"foo\": \"bar\"}"}}` })) t.Run("object with null field", testFn(false, func(t *testing.T, ctrl *gomock.Controller) (response *GraphQLResponse, ctx Context, expectedOutput string) { return &GraphQLResponse{ @@ -278,7 +278,7 @@ func TestResolver_ResolveNode(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"data":{"foo":null}}` + }, *NewContext(context.Background()), `{"data":{"foo":null}}` })) t.Run("default graphql object", testFn(false, func(t *testing.T, ctrl *gomock.Controller) (response *GraphQLResponse, ctx Context, expectedOutput string) { return &GraphQLResponse{ @@ -290,7 +290,7 @@ func TestResolver_ResolveNode(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"data":{"data":null}}` + }, *NewContext(context.Background()), `{"data":{"data":null}}` })) t.Run("graphql object with simple data source", testFn(false, func(t *testing.T, ctrl *gomock.Controller) (response *GraphQLResponse, ctx Context, expectedOutput string) { return &GraphQLResponse{ @@ -346,7 +346,7 @@ func TestResolver_ResolveNode(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"data":{"user":{"id":"1","name":"Jens","registered":true,"pet":{"name":"Barky","kind":"Dog"}}}}` + }, *NewContext(context.Background()), `{"data":{"user":{"id":"1","name":"Jens","registered":true,"pet":{"name":"Barky","kind":"Dog"}}}}` })) t.Run("fetch with context variable resolver", testFn(true, func(t *testing.T, ctrl *gomock.Controller) (response *GraphQLResponse, ctx Context, expectedOutput string) { mockDataSource := NewMockDataSource(ctrl) @@ -357,7 +357,7 @@ func TestResolver_ResolveNode(t *testing.T) { return }). Return(nil) - return &GraphQLResponse{ + res := &GraphQLResponse{ Fetches: Single(&SingleFetch{ FetchConfiguration: FetchConfiguration{ DataSource: mockDataSource, @@ -395,7 +395,10 @@ func TestResolver_ResolveNode(t *testing.T) { }, }, }, - }, Context{ctx: context.Background(), Variables: astjson.MustParseBytes([]byte(`{"id":1}`))}, `{"data":{"name":"Jens"}}` + } + resolveCtx := NewContext(context.Background()) + resolveCtx.Variables = astjson.MustParseBytes([]byte(`{"id":1}`)) + return res, *resolveCtx, `{"data":{"name":"Jens"}}` })) t.Run("resolve array of strings", testFn(false, func(t *testing.T, ctrl *gomock.Controller) (response *GraphQLResponse, ctx Context, expectedOutput string) { return &GraphQLResponse{ @@ -415,7 +418,7 @@ func TestResolver_ResolveNode(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"data":{"strings":["Alex","true","123"]}}` + }, *NewContext(context.Background()), `{"data":{"strings":["Alex","true","123"]}}` })) t.Run("resolve array of mixed scalar types", testFn(false, func(t *testing.T, ctrl *gomock.Controller) (response *GraphQLResponse, ctx Context, expectedOutput string) { return &GraphQLResponse{ @@ -435,7 +438,7 @@ func TestResolver_ResolveNode(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"errors":[{"message":"String cannot represent non-string value: \"123\"","path":["strings",2]}],"data":null}` + }, *NewContext(context.Background()), `{"errors":[{"message":"String cannot represent non-string value: \"123\"","path":["strings",2]}],"data":null}` })) t.Run("resolve array items", func(t *testing.T) { t.Run("with unescape json enabled", func(t *testing.T) { @@ -458,7 +461,7 @@ func TestResolver_ResolveNode(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"data":{"jsonList":[{"field":"value"}]}}` + }, *NewContext(context.Background()), `{"data":{"jsonList":[{"field":"value"}]}}` })) }) t.Run("with unescape json disabled", func(t *testing.T) { @@ -481,7 +484,7 @@ func TestResolver_ResolveNode(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"data":{"jsonList":["{\"field\":\"value\"}"]}}` + }, *NewContext(context.Background()), `{"data":{"jsonList":["{\"field\":\"value\"}"]}}` })) }) }) @@ -588,7 +591,7 @@ func TestResolver_ResolveNode(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"data":{"synchronousFriends":[{"id":1,"name":"Alex"},{"id":2,"name":"Patric"}],"asynchronousFriends":[{"id":1,"name":"Alex"},{"id":2,"name":"Patric"}],"nullableFriends":null,"strings":["foo","bar","baz"],"integers":[123,456,789],"floats":[1.2,3.4,5.6],"booleans":[true,false,true]}}` + }, *NewContext(context.Background()), `{"data":{"synchronousFriends":[{"id":1,"name":"Alex"},{"id":2,"name":"Patric"}],"asynchronousFriends":[{"id":1,"name":"Alex"},{"id":2,"name":"Patric"}],"nullableFriends":null,"strings":["foo","bar","baz"],"integers":[123,456,789],"floats":[1.2,3.4,5.6],"booleans":[true,false,true]}}` })) t.Run("array response from data source", testFn(false, func(t *testing.T, ctrl *gomock.Controller) (response *GraphQLResponse, ctx Context, expectedOutput string) { return &GraphQLResponse{ @@ -621,7 +624,7 @@ func TestResolver_ResolveNode(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, + }, *NewContext(context.Background()), `{"data":{"pets":[{"name":"Woofie"},{}]}}` })) t.Run("non null object with field condition can be null", testFn(false, func(t *testing.T, ctrl *gomock.Controller) (response *GraphQLResponse, ctx Context, expectedOutput string) { @@ -648,7 +651,7 @@ func TestResolver_ResolveNode(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, + }, *NewContext(context.Background()), `{"data":{"cat":{}}}` })) t.Run("object with multiple type conditions", testFn(false, func(t *testing.T, ctrl *gomock.Controller) (response *GraphQLResponse, ctx Context, expectedOutput string) { @@ -708,7 +711,7 @@ func TestResolver_ResolveNode(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, + }, *NewContext(context.Background()), `{"data":{"namespaceCreate":{"code":"UserAlreadyHasPersonalNamespace","message":""}}}` })) t.Run("resolve fieldsets based on __typename", testFn(false, func(t *testing.T, ctrl *gomock.Controller) (response *GraphQLResponse, ctx Context, expectedOutput string) { @@ -737,7 +740,7 @@ func TestResolver_ResolveNode(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, + }, *NewContext(context.Background()), `{"data":{"pets":[{"name":"Woofie"},{}]}}` })) @@ -780,7 +783,7 @@ func TestResolver_ResolveNode(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, + }, *NewContext(context.Background()), `{"data":{"pet":{"id":"1","detail":null}}}` })) @@ -810,7 +813,7 @@ func TestResolver_ResolveNode(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, + }, *NewContext(context.Background()), `{"data":{"pets":[{"name":"Woofie"},{}]}}` })) t.Run("with unescape json enabled", func(t *testing.T) { @@ -840,7 +843,7 @@ func TestResolver_ResolveNode(t *testing.T) { }, // expected output is a JSON object }, - }, Context{ctx: context.Background()}, `{"data":{"data":{"hello":"world","numberAsString":"1","number":1,"bool":true,"null":null,"array":[1,2,3],"object":{"key":"value"}}}}` + }, *NewContext(context.Background()), `{"data":{"data":{"hello":"world","numberAsString":"1","number":1,"bool":true,"null":null,"array":[1,2,3],"object":{"key":"value"}}}}` })) t.Run("json array within a string", testFn(false, func(t *testing.T, ctrl *gomock.Controller) (response *GraphQLResponse, ctx Context, expectedOutput string) { return &GraphQLResponse{ @@ -868,7 +871,7 @@ func TestResolver_ResolveNode(t *testing.T) { }, // expected output is a JSON array }, - }, Context{ctx: context.Background()}, `{"data":{"data":[1,2,3]}}` + }, *NewContext(context.Background()), `{"data":{"data":[1,2,3]}}` })) t.Run("plain scalar values within a string", func(t *testing.T) { t.Run("boolean", testFn(false, func(t *testing.T, ctrl *gomock.Controller) (response *GraphQLResponse, ctx Context, expectedOutput string) { @@ -893,7 +896,7 @@ func TestResolver_ResolveNode(t *testing.T) { }, // expected output is a string }, - }, Context{ctx: context.Background()}, `{"data":{"data":true}}` + }, *NewContext(context.Background()), `{"data":{"data":true}}` })) t.Run("int", testFn(false, func(t *testing.T, ctrl *gomock.Controller) (response *GraphQLResponse, ctx Context, expectedOutput string) { return &GraphQLResponse{ @@ -921,7 +924,7 @@ func TestResolver_ResolveNode(t *testing.T) { }, // expected output is a string }, - }, Context{ctx: context.Background()}, `{"data":{"data":1}}` + }, *NewContext(context.Background()), `{"data":{"data":1}}` })) t.Run("float", testFn(false, func(t *testing.T, ctrl *gomock.Controller) (response *GraphQLResponse, ctx Context, expectedOutput string) { return &GraphQLResponse{ @@ -949,7 +952,7 @@ func TestResolver_ResolveNode(t *testing.T) { }, // expected output is a string }, - }, Context{ctx: context.Background()}, `{"data":{"data":2.0}}` + }, *NewContext(context.Background()), `{"data":{"data":2.0}}` })) t.Run("null", testFn(false, func(t *testing.T, ctrl *gomock.Controller) (response *GraphQLResponse, ctx Context, expectedOutput string) { return &GraphQLResponse{ @@ -977,7 +980,7 @@ func TestResolver_ResolveNode(t *testing.T) { }, // expected output is a string }, - }, Context{ctx: context.Background()}, `{"data":{"data":null}}` + }, *NewContext(context.Background()), `{"data":{"data":null}}` })) t.Run("string", testFn(false, func(t *testing.T, ctrl *gomock.Controller) (response *GraphQLResponse, ctx Context, expectedOutput string) { return &GraphQLResponse{ @@ -1004,7 +1007,7 @@ func TestResolver_ResolveNode(t *testing.T) { }, // expect data value to be valid JSON string }, - }, Context{ctx: context.Background()}, `{"data":{"data":"hello world"}}` + }, *NewContext(context.Background()), `{"data":{"data":"hello world"}}` })) }) }) @@ -1025,7 +1028,7 @@ func TestResolver_ResolveNode(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"data":{"id":"1"}}` + }, *NewContext(context.Background()), `{"data":{"id":"1"}}` })) t.Run("custom nullable", testGraphQLErrFn(func(t *testing.T, r *Resolver, ctrl *gomock.Controller) (response *GraphQLResponse, ctx Context, expectedErr string) { return &GraphQLResponse{ @@ -1044,7 +1047,7 @@ func TestResolver_ResolveNode(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"errors":[{"message":"Cannot return null for non-nullable field 'Query.id'.","path":["id"]}],"data":null}` + }, *NewContext(context.Background()), `{"errors":[{"message":"Cannot return null for non-nullable field 'Query.id'.","path":["id"]}],"data":null}` })) t.Run("custom error", testFn(false, func(t *testing.T, ctrl *gomock.Controller) (response *GraphQLResponse, ctx Context, expectedOut string) { return &GraphQLResponse{ @@ -1062,7 +1065,7 @@ func TestResolver_ResolveNode(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"errors":[{"message":"custom error","path":["id"]}],"data":null}` + }, *NewContext(context.Background()), `{"errors":[{"message":"custom error","path":["id"]}],"data":null}` })) } @@ -1433,7 +1436,7 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { Data: &Object{ Nullable: true, }, - }, Context{ctx: context.Background()}, `{"data":{}}` + }, *NewContext(context.Background()), `{"data":{}}` })) t.Run("__typename without renaming", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { return &GraphQLResponse{ @@ -1489,7 +1492,7 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"data":{"user":{"id":1,"name":"Jannik","__typename":"User","aliased":"User","rewritten":"User"}}}` + }, *NewContext(context.Background()), `{"data":{"user":{"id":1,"name":"Jannik","__typename":"User","aliased":"User","rewritten":"User"}}}` })) t.Run("__typename checks", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { return &GraphQLResponse{ @@ -1548,7 +1551,7 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"errors":[{"message":"Subgraph 'Users' returned invalid value 'NotUser' for __typename field.","extensions":{"code":"INVALID_GRAPHQL"}}],"data":null}` + }, *NewContext(context.Background()), `{"errors":[{"message":"Subgraph 'Users' returned invalid value 'NotUser' for __typename field.","extensions":{"code":"INVALID_GRAPHQL"}}],"data":null}` })) t.Run("__typename checks apollo compatibility object", testFnApolloCompatibility(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { return &GraphQLResponse{ @@ -1610,7 +1613,7 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"data":null,"extensions":{"valueCompletion":[{"message":"Invalid __typename found for object at field Query.user.","path":["user"],"extensions":{"code":"INVALID_GRAPHQL"}}]}}` + }, *NewContext(context.Background()), `{"data":null,"extensions":{"valueCompletion":[{"message":"Invalid __typename found for object at field Query.user.","path":["user"],"extensions":{"code":"INVALID_GRAPHQL"}}]}}` }, nil)) t.Run("__typename checks apollo compatibility array", testFnApolloCompatibility(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { return &GraphQLResponse{ @@ -1674,7 +1677,7 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"data":null,"extensions":{"valueCompletion":[{"message":"Invalid __typename found for object at array element of type User at index 0.","path":["users",0],"extensions":{"code":"INVALID_GRAPHQL"}}]}}` + }, *NewContext(context.Background()), `{"data":null,"extensions":{"valueCompletion":[{"message":"Invalid __typename found for object at array element of type User at index 0.","path":["users",0],"extensions":{"code":"INVALID_GRAPHQL"}}]}}` }, nil)) t.Run("__typename with renaming", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { return &GraphQLResponse{ @@ -1771,7 +1774,7 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"errors":[{"message":"Cannot return null for non-nullable field 'Query.country'.","path":["country"]}],"data":null}` + }, *NewContext(context.Background()), `{"errors":[{"message":"Cannot return null for non-nullable field 'Query.country'.","path":["country"]}],"data":null}` })) t.Run("empty graphql response for non-nullable array query field", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { return &GraphQLResponse{ @@ -1797,7 +1800,7 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"errors":[{"message":"Cannot return null for non-nullable field 'Query.countries'.","path":["countries"]}],"data":null}` + }, *NewContext(context.Background()), `{"errors":[{"message":"Cannot return null for non-nullable field 'Query.countries'.","path":["countries"]}],"data":null}` })) t.Run("fetch with simple error without datasource ID", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { mockDataSource := NewMockDataSource(ctrl) @@ -1829,7 +1832,7 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"errors":[{"message":"Failed to fetch from Subgraph.","extensions":{"errors":[{"message":"errorMessage"}]}}],"data":{"name":null}}` + }, *NewContext(context.Background()), `{"errors":[{"message":"Failed to fetch from Subgraph.","extensions":{"errors":[{"message":"errorMessage"}]}}],"data":{"name":null}}` })) t.Run("fetch with simple error without datasource ID no subgraph error forwarding", testFnNoSubgraphErrorForwarding(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { mockDataSource := NewMockDataSource(ctrl) @@ -1861,7 +1864,7 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"errors":[{"message":"Failed to fetch from Subgraph at Path 'query'."}],"data":{"name":null}}` + }, *NewContext(context.Background()), `{"errors":[{"message":"Failed to fetch from Subgraph at Path 'query'."}],"data":{"name":null}}` })) t.Run("fetch with simple error", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { mockDataSource := NewMockDataSource(ctrl) @@ -1897,7 +1900,7 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"errors":[{"message":"Failed to fetch from Subgraph 'Users' at Path 'query'.","extensions":{"errors":[{"message":"errorMessage"}]}}],"data":{"name":null}}` + }, *NewContext(context.Background()), `{"errors":[{"message":"Failed to fetch from Subgraph 'Users' at Path 'query'.","extensions":{"errors":[{"message":"errorMessage"}]}}],"data":{"name":null}}` })) t.Run("fetch with simple error in pass through Subgraph Error Mode", testFnSubgraphErrorsPassthrough(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { mockDataSource := NewMockDataSource(ctrl) @@ -1933,7 +1936,7 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"errors":[{"message":"errorMessage"}],"data":{"name":null}}` + }, *NewContext(context.Background()), `{"errors":[{"message":"errorMessage"}],"data":{"name":null}}` })) t.Run("fetch with pass through mode and omit custom fields", testFnSubgraphErrorsPassthroughAndOmitCustomFields(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { mockDataSource := NewMockDataSource(ctrl) @@ -1971,7 +1974,7 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"errors":[{"message":"errorMessage","extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}],"data":{"name":null}}` + }, *NewContext(context.Background()), `{"errors":[{"message":"errorMessage","extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}],"data":{"name":null}}` })) t.Run("fetch with returned err (with DataSourceID)", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { mockDataSource := NewMockDataSource(ctrl) @@ -2005,7 +2008,7 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"errors":[{"message":"Failed to fetch from Subgraph 'Users' at Path 'query'."}],"data":{"name":null}}` + }, *NewContext(context.Background()), `{"errors":[{"message":"Failed to fetch from Subgraph 'Users' at Path 'query'."}],"data":{"name":null}}` })) t.Run("fetch with returned err (no DataSourceID)", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { mockDataSource := NewMockDataSource(ctrl) @@ -2035,7 +2038,7 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"errors":[{"message":"Failed to fetch from Subgraph at Path 'query'."}],"data":{"name":null}}` + }, *NewContext(context.Background()), `{"errors":[{"message":"Failed to fetch from Subgraph at Path 'query'."}],"data":{"name":null}}` })) t.Run("fetch with returned err and non-nullable root field", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { mockDataSource := NewMockDataSource(ctrl) @@ -2069,7 +2072,7 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"errors":[{"message":"Failed to fetch from Subgraph 'Users' at Path 'query'."}],"data":null}` + }, *NewContext(context.Background()), `{"errors":[{"message":"Failed to fetch from Subgraph 'Users' at Path 'query'."}],"data":null}` })) t.Run("root field with nested non-nullable fields returns null", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { return &GraphQLResponse{ @@ -2104,7 +2107,7 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"errors":[{"message":"Cannot return null for non-nullable field 'Query.user.name'.","path":["user","name"]}],"data":{"user":null}}` + }, *NewContext(context.Background()), `{"errors":[{"message":"Cannot return null for non-nullable field 'Query.user.name'.","path":["user","name"]}],"data":{"user":null}}` })) t.Run("multiple root fields with nested non-nullable fields each return null", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { return &GraphQLResponse{ @@ -2162,7 +2165,7 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"errors":[{"message":"Cannot return null for non-nullable field 'Query.one.name'.","path":["one","name"]},{"message":"Cannot return null for non-nullable field 'Query.two.age'.","path":["two","age"]}],"data":{"one":null,"two":null}}` + }, *NewContext(context.Background()), `{"errors":[{"message":"Cannot return null for non-nullable field 'Query.one.name'.","path":["one","name"]},{"message":"Cannot return null for non-nullable field 'Query.two.age'.","path":["two","age"]}],"data":{"one":null,"two":null}}` })) t.Run("root field with double nested non-nullable field returns partial data", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { return &GraphQLResponse{ @@ -2213,7 +2216,7 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"errors":[{"message":"Cannot return null for non-nullable field 'Query.user.nested.name'.","path":["user","nested","name"]}],"data":{"user":{"nested":null,"age":1}}}` + }, *NewContext(context.Background()), `{"errors":[{"message":"Cannot return null for non-nullable field 'Query.user.nested.name'.","path":["user","nested","name"]}],"data":{"user":{"nested":null,"age":1}}}` })) t.Run("fetch with two Errors", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { mockDataSource := NewMockDataSource(ctrl) @@ -2246,7 +2249,7 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"errors":[{"message":"Failed to fetch from Subgraph at Path 'query'.","extensions":{"errors":[{"message":"errorMessage1"},{"message":"errorMessage2"}]}}],"data":{"name":null}}` + }, *NewContext(context.Background()), `{"errors":[{"message":"Failed to fetch from Subgraph at Path 'query'.","extensions":{"errors":[{"message":"errorMessage1"},{"message":"errorMessage2"}]}}],"data":{"name":null}}` })) t.Run("non-nullable object in nullable field", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { return &GraphQLResponse{ @@ -2283,7 +2286,7 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"data":{"nullableField":null}}` + }, *NewContext(context.Background()), `{"data":{"nullableField":null}}` })) t.Run("interface response", func(t *testing.T) { @@ -2346,13 +2349,13 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { t.Run("interface response with matching type", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { return obj(`{"thing":{"id":"1","abstractThing":{"__typename":"ConcreteOne","name":"foo"}}}`), - Context{ctx: context.Background()}, + *NewContext(context.Background()), `{"data":{"thing":{"id":"1","abstractThing":{"name":"foo","__typename":"ConcreteOne"}}}}` })) t.Run("interface response with not matching type", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { return obj(`{"thing":{"id":"1","abstractThing":{"__typename":"ConcreteTwo"}}}`), - Context{ctx: context.Background()}, + *NewContext(context.Background()), `{"data":{"thing":{"id":"1","abstractThing":{}}}}` })) }) @@ -2410,13 +2413,13 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { t.Run("interface response with matching type", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { return obj(`{"data":{"things":[{"id":"1","abstractThing":{"__typename":"ConcreteOne","name":"foo"}}]}}`), - Context{ctx: context.Background()}, + *NewContext(context.Background()), `{"data":{"things":[{"id":"1","abstractThing":{"name":"foo"}}]}}` })) t.Run("interface response with not matching type", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { return obj(`{"data":{"things":[{"id":"1","abstractThing":{"__typename":"ConcreteTwo"}}]}}`), - Context{ctx: context.Background()}, + *NewContext(context.Background()), `{"data":{"things":[{"id":"1","abstractThing":{}}]}}` })) }) @@ -2450,7 +2453,7 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"data":{"nullableArray":[]}}` + }, *NewContext(context.Background()), `{"data":{"nullableArray":[]}}` })) t.Run("empty not nullable array should resolve correctly", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { return &GraphQLResponse{ @@ -2480,7 +2483,7 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"data":{"notNullableArray":[]}}` + }, *NewContext(context.Background()), `{"data":{"notNullableArray":[]}}` })) t.Run("when data null not nullable array should resolve to data null and errors", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { return &GraphQLResponse{ @@ -2532,7 +2535,7 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"errors":[{"message":"Failed to fetch from Subgraph at Path 'query', Reason: no data or errors in response."}],"data":null}` + }, *NewContext(context.Background()), `{"errors":[{"message":"Failed to fetch from Subgraph at Path 'query', Reason: no data or errors in response."}],"data":null}` })) t.Run("when data null and errors present not nullable array should result to null data upstream error and resolve error", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { return &GraphQLResponse{ @@ -2573,7 +2576,7 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"errors":[{"message":"Failed to fetch from Subgraph at Path 'query'.","extensions":{"errors":[{"message":"Could not get name","locations":[{"line":3,"column":5}],"path":["todos","0","name"]}]}}],"data":{"todos":null}}` + }, *NewContext(context.Background()), `{"errors":[{"message":"Failed to fetch from Subgraph at Path 'query'.","extensions":{"errors":[{"message":"Could not get name","locations":[{"line":3,"column":5}],"path":["todos","0","name"]}]}}],"data":{"todos":null}}` })) t.Run("complex GraphQL Server plan", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { serviceOne := NewMockDataSource(ctrl) @@ -2613,7 +2616,7 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { return writeGraphqlResponse(pair, w, false) }) - return &GraphQLResponse{ + res := &GraphQLResponse{ Fetches: Sequence( Parallel( SingleWithPath(&SingleFetch{ @@ -2814,7 +2817,10 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { }, }, }, - }, Context{ctx: context.Background(), Variables: astjson.MustParseBytes([]byte(`{"firstArg":"firstArgValue","thirdArg":123,"secondArg": true, "fourthArg": 12.34}`))}, `{"data":{"serviceOne":{"fieldOne":"fieldOneValue"},"serviceTwo":{"fieldTwo":"fieldTwoValue","serviceOneResponse":{"fieldOne":"fieldOneValue"}},"anotherServiceOne":{"fieldOne":"anotherFieldOneValue"},"secondServiceTwo":{"fieldTwo":"secondFieldTwoValue"},"reusingServiceOne":{"fieldOne":"reUsingFieldOneValue"}}}` + } + resolveCtx := NewContext(context.Background()) + resolveCtx.Variables = astjson.MustParseBytes([]byte(`{"firstArg":"firstArgValue","thirdArg":123,"secondArg": true, "fourthArg": 12.34}`)) + return res, *resolveCtx, `{"data":{"serviceOne":{"fieldOne":"fieldOneValue"},"serviceTwo":{"fieldTwo":"fieldTwoValue","serviceOneResponse":{"fieldOne":"fieldOneValue"}},"anotherServiceOne":{"fieldOne":"anotherFieldOneValue"},"secondServiceTwo":{"fieldTwo":"secondFieldTwoValue"},"reusingServiceOne":{"fieldOne":"reUsingFieldOneValue"}}}` })) t.Run("federation", func(t *testing.T) { t.Run("simple", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { @@ -3033,7 +3039,7 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { }, }, }, - }, Context{ctx: context.Background(), Variables: nil}, `{"data":{"me":{"id":"1234","username":"Me","reviews":[{"body":"A highly effective form of birth control.","product":{"upc":"top-1","name":"Furby"}},{"body":"Fedoras are one of the most fashionable hats around and can look great with a variety of outfits.","product":{"upc":"top-2","name":"Trilby"}}]}}}` + }, *NewContext(context.Background()), `{"data":{"me":{"id":"1234","username":"Me","reviews":[{"body":"A highly effective form of birth control.","product":{"upc":"top-1","name":"Furby"}},{"body":"Fedoras are one of the most fashionable hats around and can look great with a variety of outfits.","product":{"upc":"top-2","name":"Trilby"}}]}}}` })) t.Run("federation with batch", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { userService := NewMockDataSource(ctrl) @@ -3236,7 +3242,7 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { }, }, }, - }, Context{ctx: context.Background(), Variables: nil}, `{"data":{"me":{"id":"1234","username":"Me","reviews":[{"body":"A highly effective form of birth control.","product":{"upc":"top-1","name":"Trilby"}},{"body":"Fedoras are one of the most fashionable hats around and can look great with a variety of outfits.","product":{"upc":"top-2","name":"Fedora"}}]}}}` + }, *NewContext(context.Background()), `{"data":{"me":{"id":"1234","username":"Me","reviews":[{"body":"A highly effective form of birth control.","product":{"upc":"top-1","name":"Trilby"}},{"body":"Fedoras are one of the most fashionable hats around and can look great with a variety of outfits.","product":{"upc":"top-2","name":"Fedora"}}]}}}` })) t.Run("federation with merge paths", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { userService := NewMockDataSource(ctrl) @@ -3440,7 +3446,7 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { }, }, }, - }, Context{ctx: context.Background(), Variables: nil}, `{"data":{"me":{"id":"1234","username":"Me","reviews":[{"body":"A highly effective form of birth control.","product":{"upc":"top-1","name":"Trilby"}},{"body":"Fedoras are one of the most fashionable hats around and can look great with a variety of outfits.","product":{"upc":"top-2","name":"Fedora"}}]}}}` + }, *NewContext(context.Background()), `{"data":{"me":{"id":"1234","username":"Me","reviews":[{"body":"A highly effective form of birth control.","product":{"upc":"top-1","name":"Trilby"}},{"body":"Fedoras are one of the most fashionable hats around and can look great with a variety of outfits.","product":{"upc":"top-2","name":"Fedora"}}]}}}` })) t.Run("federation with null response", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { userService := NewMockDataSource(ctrl) @@ -3672,7 +3678,7 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { }, }, }, - }, Context{ctx: context.Background(), Variables: nil}, `{"data":{"me":{"id":"1234","username":"Me","reviews":[{"body":"foo","product":{"upc":"top-1","name":"Trilby"}},{"body":"bar","product":{"upc":"top-2","name":"Fedora"}},{"body":"baz","product":null},{"body":"bat","product":{"upc":"top-4","name":"Boater"}},{"body":"bal","product":{"upc":"top-5","name":"Top Hat"}},{"body":"ban","product":{"upc":"top-6","name":"Bowler"}}]}}}` + }, *NewContext(context.Background()), `{"data":{"me":{"id":"1234","username":"Me","reviews":[{"body":"foo","product":{"upc":"top-1","name":"Trilby"}},{"body":"bar","product":{"upc":"top-2","name":"Fedora"}},{"body":"baz","product":null},{"body":"bat","product":{"upc":"top-4","name":"Boater"}},{"body":"bal","product":{"upc":"top-5","name":"Top Hat"}},{"body":"ban","product":{"upc":"top-6","name":"Bowler"}}]}}}` })) t.Run("federation with fetch error", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { @@ -3865,7 +3871,7 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { }, }, }, - }, Context{ctx: context.Background(), Variables: nil}, `{"errors":[{"message":"Failed to fetch from Subgraph at Path 'query.me.reviews.@.product', Reason: no data or errors in response."},{"message":"Cannot return null for non-nullable field 'Query.me.reviews.product.name'.","path":["me","reviews",0,"product","name"]},{"message":"Cannot return null for non-nullable field 'Query.me.reviews.product.name'.","path":["me","reviews",1,"product","name"]}],"data":{"me":{"id":"1234","username":"Me","reviews":[null,null]}}}` + }, *NewContext(context.Background()), `{"errors":[{"message":"Failed to fetch from Subgraph at Path 'query.me.reviews.@.product', Reason: no data or errors in response."},{"message":"Cannot return null for non-nullable field 'Query.me.reviews.product.name'.","path":["me","reviews",0,"product","name"]},{"message":"Cannot return null for non-nullable field 'Query.me.reviews.product.name'.","path":["me","reviews",1,"product","name"]}],"data":{"me":{"id":"1234","username":"Me","reviews":[null,null]}}}` })) t.Run("federation with fetch error and non null fields inside an array", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { @@ -4056,7 +4062,7 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { }, }, }, - }, Context{ctx: context.Background(), Variables: nil}, `{"errors":[{"message":"Failed to fetch from Subgraph at Path 'query.me.reviews.@.product', Reason: no data or errors in response."},{"message":"Cannot return null for non-nullable field 'Query.me.reviews.product.name'.","path":["me","reviews",0,"product","name"]}],"data":{"me":{"id":"1234","username":"Me","reviews":null}}}` + }, *NewContext(context.Background()), `{"errors":[{"message":"Failed to fetch from Subgraph at Path 'query.me.reviews.@.product', Reason: no data or errors in response."},{"message":"Cannot return null for non-nullable field 'Query.me.reviews.product.name'.","path":["me","reviews",0,"product","name"]}],"data":{"me":{"id":"1234","username":"Me","reviews":null}}}` })) t.Run("federation with optional variable", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { userService := NewMockDataSource(ctrl) @@ -4095,7 +4101,7 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { return writeGraphqlResponse(pair, w, false) }) - return &GraphQLResponse{ + res := &GraphQLResponse{ Fetches: Sequence( SingleWithPath(&SingleFetch{ InputTemplate: InputTemplate{ @@ -4258,7 +4264,10 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) { }, }, }, - }, Context{ctx: context.Background(), Variables: astjson.MustParseBytes([]byte(`{"companyId":"abc123","date":null}`))}, `{"data":{"me":{"employment":{"id":"xyz987","times":[{"id":"t1","employee":{"id":"xyz987"},"start":"2022-11-02T08:00:00","end":"2022-11-02T12:00:00"}]}}}}` + } + resolveCtx := NewContext(context.Background()) + resolveCtx.Variables = astjson.MustParseBytes([]byte(`{"companyId":"abc123","date":null}`)) + return res, *resolveCtx, `{"data":{"me":{"employment":{"id":"xyz987","times":[{"id":"t1","employee":{"id":"xyz987"},"start":"2022-11-02T08:00:00","end":"2022-11-02T12:00:00"}]}}}}` })) }) } @@ -4304,7 +4313,7 @@ func TestResolver_ApolloCompatibilityMode_FetchError(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"data":null,"extensions":{"valueCompletion":[{"message":"Cannot return null for non-nullable field Query.name.","path":["name"],"extensions":{"code":"INVALID_GRAPHQL"}}]}}` + }, *NewContext(context.Background()), `{"data":null,"extensions":{"valueCompletion":[{"message":"Cannot return null for non-nullable field Query.name.","path":["name"],"extensions":{"code":"INVALID_GRAPHQL"}}]}}` }, &options)) t.Run("simple fetch with fetch error suppression - response with error", testFnApolloCompatibility(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { @@ -4343,7 +4352,7 @@ func TestResolver_ApolloCompatibilityMode_FetchError(t *testing.T) { }, }, }, - }, Context{ctx: context.Background()}, `{"errors":[{"message":"Cannot query field 'name' on type 'Query'"}],"data":null}` + }, *NewContext(context.Background()), `{"errors":[{"message":"Cannot query field 'name' on type 'Query'"}],"data":null}` }, &options)) t.Run("complex fetch with fetch error suppression", testFnApolloCompatibility(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { @@ -4535,7 +4544,7 @@ func TestResolver_ApolloCompatibilityMode_FetchError(t *testing.T) { }, }, }, - }, Context{ctx: context.Background(), Variables: nil}, `{"errors":[{"message":"errorMessage"}],"data":{"me":{"id":"1234","username":"Me","reviews":null}}}` + }, *NewContext(context.Background()), `{"errors":[{"message":"errorMessage"}],"data":{"me":{"id":"1234","username":"Me","reviews":null}}}` }, &options)) }