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:
@@ -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,
|
||||
|
||||
@@ -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 } }`
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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{})
|
||||
|
||||
Reference in New Issue
Block a user