Add tests for behavior on error (#83)

## 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


Author: benjaminjkraft

Reviewers: jvoll, aberkan, dnerdy, MiguelCastillo, mahtabsabet

Required Reviewers: 

Approved By: jvoll

Checks:  Test (1.17),  Test (1.16),  Test (1.15),  Test (1.14),  Lint,  Test (1.17),  Test (1.16),  Test (1.15),  Test (1.14),  Lint

Pull Request URL: https://github.com/Khan/genqlient/pull/83
This commit is contained in:
Ben Kraft
2021-09-10 15:45:34 -07:00
committed by GitHub
parent e88305ecbd
commit 9e1c98488e
5 changed files with 128 additions and 0 deletions
+35
View File
@@ -266,6 +266,17 @@ func (v *UserFields) UnmarshalJSON(b []byte) error {
return nil 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. // queryWithFragmentsBeingsAnimal includes the requested fields of the GraphQL type Animal.
type queryWithFragmentsBeingsAnimal struct { type queryWithFragmentsBeingsAnimal struct {
Typename string `json:"__typename"` Typename string `json:"__typename"`
@@ -1074,6 +1085,30 @@ query simpleQuery {
return &retval, err 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( func queryWithVariables(
ctx context.Context, ctx context.Context,
client graphql.Client, client graphql.Client,
+33
View File
@@ -34,6 +34,39 @@ func TestSimpleQuery(t *testing.T) {
assert.Equal(t, 17, resp.Me.LuckyNumber) 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) { func TestVariables(t *testing.T) {
_ = `# @genqlient _ = `# @genqlient
query queryWithVariables($id: ID!) { user(id: $id) { id name luckyNumber } }` query queryWithVariables($id: ID!) { user(id: $id) { id name luckyNumber } }`
+1
View File
@@ -4,6 +4,7 @@ type Query {
being(id: ID!): Being being(id: ID!): Being
beings(ids: [ID!]!): [Being]! beings(ids: [ID!]!): [Being]!
lotteryWinner(number: Int!): Lucky lotteryWinner(number: Int!): Lucky
fail: Boolean
} }
type User implements Being & Lucky { type User implements Being & Lucky {
@@ -61,6 +61,7 @@ type ComplexityRoot struct {
Query struct { Query struct {
Being func(childComplexity int, id string) int Being func(childComplexity int, id string) int
Beings func(childComplexity int, ids []string) int Beings func(childComplexity int, ids []string) int
Fail func(childComplexity int) int
LotteryWinner func(childComplexity int, number int) int LotteryWinner func(childComplexity int, number int) int
Me func(childComplexity int) int Me func(childComplexity int) int
User func(childComplexity int, id string) int User func(childComplexity int, id string) int
@@ -80,6 +81,7 @@ type QueryResolver interface {
Being(ctx context.Context, id string) (Being, error) Being(ctx context.Context, id string) (Being, error)
Beings(ctx context.Context, ids []string) ([]Being, error) Beings(ctx context.Context, ids []string) ([]Being, error)
LotteryWinner(ctx context.Context, number int) (Lucky, error) LotteryWinner(ctx context.Context, number int) (Lucky, error)
Fail(ctx context.Context) (*bool, error)
} }
type executableSchema struct { 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 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": case "Query.lotteryWinner":
if e.complexity.Query.LotteryWinner == nil { if e.complexity.Query.LotteryWinner == nil {
break break
@@ -285,6 +294,7 @@ var sources = []*ast.Source{
being(id: ID!): Being being(id: ID!): Being
beings(ids: [ID!]!): [Being]! beings(ids: [ID!]!): [Being]!
lotteryWinner(number: Int!): Lucky lotteryWinner(number: Int!): Lucky
fail: Boolean
} }
type User implements Being & Lucky { 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) 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) { func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { 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) res = ec._Query_lotteryWinner(ctx, field)
return res 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": case "__type":
out.Values[i] = ec._Query___type(ctx, field) out.Values[i] = ec._Query___type(ctx, field)
case "__schema": case "__schema":
+6
View File
@@ -2,6 +2,7 @@ package server
import ( import (
"context" "context"
"fmt"
"net/http/httptest" "net/http/httptest"
"github.com/99designs/gqlgen/graphql/handler" "github.com/99designs/gqlgen/graphql/handler"
@@ -82,6 +83,11 @@ func (r *queryResolver) LotteryWinner(ctx context.Context, number int) (Lucky, e
return nil, nil return nil, nil
} }
func (r *queryResolver) Fail(ctx context.Context) (*bool, error) {
f := true
return &f, fmt.Errorf("oh no")
}
func RunServer() *httptest.Server { func RunServer() *httptest.Server {
gqlgenServer := handler.New(NewExecutableSchema(Config{Resolvers: &resolver{}})) gqlgenServer := handler.New(NewExecutableSchema(Config{Resolvers: &resolver{}}))
gqlgenServer.AddTransport(transport.POST{}) gqlgenServer.AddTransport(transport.POST{})