Repository: Khan/genqlient
Pull request #83 (2021-09-09 01:34:32)

Add tests for behavior on error
## Summary:
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



Patch:
diff --git a/internal/integration/generated.go b/internal/integration/generated.go
index 76125c8..73dad47 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/schema.graphql b/internal/integration/schema.graphql
index de6d3f5..6ea7069 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 709824f..06635c6 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 6cff7cc..3cf362b 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{})


Test patch:
diff --git a/internal/integration/integration_test.go b/internal/integration/integration_test.go
index 87dd57a..4529372 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 } }`