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