From 292a8d0504f2ce55a095d82f4d8fce0d66d5981d Mon Sep 17 00:00:00 2001 From: Ben Kraft Date: Wed, 8 Sep 2021 18:31:23 -0700 Subject: [PATCH] Add tests for behavior on error We guarantee that we never return a nil response, so you can safely do ``` resp, err := myQuery(...) return resp.Field.SubField, err ``` And furthermore, if the error was a GraphQL error, `resp` may even be nonzero; other, non-failing fields may be set. (This depends on the server, of course.) But we weren't testing either of those. Now we do. Test plan: make check Reviewers: marksandstrom, adam, miguel --- internal/integration/generated.go | 35 ++++++++++++++ internal/integration/integration_test.go | 33 ++++++++++++++ internal/integration/schema.graphql | 1 + internal/integration/server/gqlgen_exec.go | 53 ++++++++++++++++++++++ internal/integration/server/server.go | 6 +++ 5 files changed, 128 insertions(+) diff --git a/internal/integration/generated.go b/internal/integration/generated.go index 76125c85..73dad471 100644 --- a/internal/integration/generated.go +++ b/internal/integration/generated.go @@ -266,6 +266,17 @@ func (v *UserFields) UnmarshalJSON(b []byte) error { return nil } +// failingQueryMeUser includes the requested fields of the GraphQL type User. +type failingQueryMeUser struct { + Id string `json:"id"` +} + +// failingQueryResponse is returned by failingQuery on success. +type failingQueryResponse struct { + Fail bool `json:"fail"` + Me failingQueryMeUser `json:"me"` +} + // queryWithFragmentsBeingsAnimal includes the requested fields of the GraphQL type Animal. type queryWithFragmentsBeingsAnimal struct { Typename string `json:"__typename"` @@ -1074,6 +1085,30 @@ query simpleQuery { return &retval, err } +func failingQuery( + ctx context.Context, + client graphql.Client, +) (*failingQueryResponse, error) { + var err error + + var retval failingQueryResponse + err = client.MakeRequest( + ctx, + "failingQuery", + ` +query failingQuery { + fail + me { + id + } +} +`, + &retval, + nil, + ) + return &retval, err +} + func queryWithVariables( ctx context.Context, client graphql.Client, diff --git a/internal/integration/integration_test.go b/internal/integration/integration_test.go index 87dd57a3..45293720 100644 --- a/internal/integration/integration_test.go +++ b/internal/integration/integration_test.go @@ -34,6 +34,39 @@ func TestSimpleQuery(t *testing.T) { assert.Equal(t, 17, resp.Me.LuckyNumber) } +func TestServerError(t *testing.T) { + _ = `# @genqlient + query failingQuery { fail me { id } }` + + ctx := context.Background() + server := server.RunServer() + defer server.Close() + client := graphql.NewClient(server.URL, http.DefaultClient) + + resp, err := failingQuery(ctx, client) + // As long as we get some response back, we should still return a full + // response -- and indeed in this case it should even have another field + // (which didn't err) set. + assert.Error(t, err) + assert.NotNil(t, resp) + assert.Equal(t, "1", resp.Me.Id) +} + +func TestNetworkError(t *testing.T) { + ctx := context.Background() + client := graphql.NewClient("https://nothing.invalid/graphql", http.DefaultClient) + + resp, err := failingQuery(ctx, client) + // As we guarantee in the README, even on network error you always get a + // non-nil response; this is so you can write e.g. + // resp, err := failingQuery(ctx) + // return resp.Me.Id, err + // without a bunch of extra ceremony. + assert.Error(t, err) + assert.NotNil(t, resp) + assert.Equal(t, new(failingQueryResponse), resp) +} + func TestVariables(t *testing.T) { _ = `# @genqlient query queryWithVariables($id: ID!) { user(id: $id) { id name luckyNumber } }` diff --git a/internal/integration/schema.graphql b/internal/integration/schema.graphql index de6d3f50..6ea7069e 100644 --- a/internal/integration/schema.graphql +++ b/internal/integration/schema.graphql @@ -4,6 +4,7 @@ type Query { being(id: ID!): Being beings(ids: [ID!]!): [Being]! lotteryWinner(number: Int!): Lucky + fail: Boolean } type User implements Being & Lucky { diff --git a/internal/integration/server/gqlgen_exec.go b/internal/integration/server/gqlgen_exec.go index 709824fe..06635c64 100644 --- a/internal/integration/server/gqlgen_exec.go +++ b/internal/integration/server/gqlgen_exec.go @@ -61,6 +61,7 @@ type ComplexityRoot struct { Query struct { Being func(childComplexity int, id string) int Beings func(childComplexity int, ids []string) int + Fail func(childComplexity int) int LotteryWinner func(childComplexity int, number int) int Me func(childComplexity int) int User func(childComplexity int, id string) int @@ -80,6 +81,7 @@ type QueryResolver interface { Being(ctx context.Context, id string) (Being, error) Beings(ctx context.Context, ids []string) ([]Being, error) LotteryWinner(ctx context.Context, number int) (Lucky, error) + Fail(ctx context.Context) (*bool, error) } type executableSchema struct { @@ -170,6 +172,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Query.Beings(childComplexity, args["ids"].([]string)), true + case "Query.fail": + if e.complexity.Query.Fail == nil { + break + } + + return e.complexity.Query.Fail(childComplexity), true + case "Query.lotteryWinner": if e.complexity.Query.LotteryWinner == nil { break @@ -285,6 +294,7 @@ var sources = []*ast.Source{ being(id: ID!): Being beings(ids: [ID!]!): [Being]! lotteryWinner(number: Int!): Lucky + fail: Boolean } type User implements Being & Lucky { @@ -867,6 +877,38 @@ func (ec *executionContext) _Query_lotteryWinner(ctx context.Context, field grap return ec.marshalOLucky2githubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐLucky(ctx, field.Selections, res) } +func (ec *executionContext) _Query_fail(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Query", + Field: field, + Args: nil, + IsMethod: true, + IsResolver: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().Fail(rctx) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*bool) + fc.Result = res + return ec.marshalOBoolean2ᚖbool(ctx, field.Selections, res) +} + func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -2371,6 +2413,17 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr res = ec._Query_lotteryWinner(ctx, field) return res }) + case "fail": + field := field + out.Concurrently(i, func() (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_fail(ctx, field) + return res + }) case "__type": out.Values[i] = ec._Query___type(ctx, field) case "__schema": diff --git a/internal/integration/server/server.go b/internal/integration/server/server.go index 6cff7ccc..3cf362bb 100644 --- a/internal/integration/server/server.go +++ b/internal/integration/server/server.go @@ -2,6 +2,7 @@ package server import ( "context" + "fmt" "net/http/httptest" "github.com/99designs/gqlgen/graphql/handler" @@ -82,6 +83,11 @@ func (r *queryResolver) LotteryWinner(ctx context.Context, number int) (Lucky, e return nil, nil } +func (r *queryResolver) Fail(ctx context.Context) (*bool, error) { + f := true + return &f, fmt.Errorf("oh no") +} + func RunServer() *httptest.Server { gqlgenServer := handler.New(NewExecutableSchema(Config{Resolvers: &resolver{}})) gqlgenServer.AddTransport(transport.POST{})