Skip to content

Commit

Permalink
Fix Panic on Introspection of Schema using ToJSON
Browse files Browse the repository at this point in the history
Introspection via ToJSON works differently when used via the
 `Schema.ToJSON` function than when making an introspection query from a
 client such as GraphiQL. In the later case, introspection uses the
 Schema's registered resolver, while in the former case, a
 `resolvable.Schema` is faked

Without the `Meta` being set on this `resolvable.Schema` (following the
 recent changes to address race conditions around the Meta schema), this
 resulted in a panic when using this form of schema introspection.
  • Loading branch information
dackroyd committed May 8, 2019
1 parent b068bc1 commit c126f0e
Show file tree
Hide file tree
Showing 9 changed files with 3,530 additions and 9 deletions.
1,346 changes: 1,346 additions & 0 deletions example/social/introspect.json

Large diffs are not rendered by default.

2,026 changes: 2,026 additions & 0 deletions example/starwars/introspect.json

Large diffs are not rendered by default.

13 changes: 6 additions & 7 deletions graphql.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"reflect"

"github.com/graph-gophers/graphql-go/errors"
"github.com/graph-gophers/graphql-go/internal/common"
Expand Down Expand Up @@ -37,13 +38,11 @@ func ParseSchema(schemaString string, resolver interface{}, opts ...SchemaOpt) (
return nil, err
}

if resolver != nil {
r, err := resolvable.ApplyResolver(s.schema, resolver)
if err != nil {
return nil, err
}
s.res = r
r, err := resolvable.ApplyResolver(s.schema, resolver)
if err != nil {
return nil, err
}
s.res = r

return s, nil
}
Expand Down Expand Up @@ -156,7 +155,7 @@ func (s *Schema) Validate(queryString string) []*errors.QueryError {
// without a resolver. If the context get cancelled, no further resolvers will be called and a
// the context error will be returned as soon as possible (not immediately).
func (s *Schema) Exec(ctx context.Context, queryString string, operationName string, variables map[string]interface{}) *Response {
if s.res == nil {
if s.res.Resolver == (reflect.Value{}) {
panic("schema created without resolver, can not exec")
}
return s.exec(ctx, queryString, operationName, variables, s.res)
Expand Down
55 changes: 55 additions & 0 deletions graphql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3017,3 +3017,58 @@ func TestErrorPropagation(t *testing.T) {
},
})
}

func TestSchema_Exec_without_resolver(t *testing.T) {
t.Parallel()

type args struct {
Query string
Schema string
}
type want struct {
Panic interface{}
}
testTable := []struct {
Name string
Args args
Want want
}{
{
Name: "schema_without_resolver_errors",
Args: args{
Query: `
query {
hero {
id
name
friends {
name
}
}
}
`,
Schema: starwars.Schema,
},
Want: want{Panic: "schema created without resolver, can not exec"},
},
}

for _, tt := range testTable {
t.Run(tt.Name, func(t *testing.T) {
s := graphql.MustParseSchema(tt.Args.Schema, nil)

defer func() {
r := recover()
if r == nil {
t.Fatal("expected query to panic")
}
if r != tt.Want.Panic {
t.Logf("got: %s", r)
t.Logf("want: %s", tt.Want.Panic)
t.Fail()
}
}()
_ = s.Exec(context.Background(), tt.Args.Query, "", map[string]interface{}{})
})
}
}
4 changes: 4 additions & 0 deletions internal/exec/resolvable/resolvable.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ func (*List) isResolvable() {}
func (*Scalar) isResolvable() {}

func ApplyResolver(s *schema.Schema, resolver interface{}) (*Schema, error) {
if resolver == nil {
return &Schema{Meta: newMeta(s), Schema: *s}, nil
}

b := newBuilder(s)

var query, mutation, subscription Resolvable
Expand Down
1 change: 1 addition & 0 deletions introspection.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ func (s *Schema) Inspect() *introspection.Schema {
// ToJSON encodes the schema in a JSON format used by tools like Relay.
func (s *Schema) ToJSON() ([]byte, error) {
result := s.exec(context.Background(), introspectionQuery, "", nil, &resolvable.Schema{
Meta: s.res.Meta,
Query: &resolvable.Object{},
Schema: *s.schema,
})
Expand Down
89 changes: 89 additions & 0 deletions introspection_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package graphql_test

import (
"bytes"
"encoding/json"
"io/ioutil"
"testing"

"github.com/graph-gophers/graphql-go"
"github.com/graph-gophers/graphql-go/example/social"
"github.com/graph-gophers/graphql-go/example/starwars"
)

func TestSchema_ToJSON(t *testing.T) {
t.Parallel()

type args struct {
Schema *graphql.Schema
}
type want struct {
JSON []byte
}
testTable := []struct {
Name string
Args args
Want want
}{
{
Name: "Social Schema",
Args: args{Schema: graphql.MustParseSchema(social.Schema, &social.Resolver{}, graphql.UseFieldResolvers())},
Want: want{JSON: mustReadFile("example/social/introspect.json")},
},
{
Name: "Star Wars Schema",
Args: args{Schema: graphql.MustParseSchema(starwars.Schema, &starwars.Resolver{})},
Want: want{JSON: mustReadFile("example/starwars/introspect.json")},
},
{
Name: "Star Wars Schema without Resolver",
Args: args{Schema: graphql.MustParseSchema(starwars.Schema, nil)},
Want: want{JSON: mustReadFile("example/starwars/introspect.json")},
},
}

for _, tt := range testTable {
t.Run(tt.Name, func(t *testing.T) {
j, err := tt.Args.Schema.ToJSON()
if err != nil {
t.Fatalf("invalid schema %s", err.Error())
}

// Verify JSON to avoid red herring errors.
got, err := formatJSON(j)
if err != nil {
t.Fatalf("got: invalid JSON: %s", err)
}
want, err := formatJSON(tt.Want.JSON)
if err != nil {
t.Fatalf("want: invalid JSON: %s", err)
}

if !bytes.Equal(got, want) {
t.Logf("got: %s", got)
t.Logf("want: %s", want)
t.Fail()
}
})
}
}

func formatJSON(data []byte) ([]byte, error) {
var v interface{}
if err := json.Unmarshal(data, &v); err != nil {
return nil, err
}
formatted, err := json.Marshal(v)
if err != nil {
return nil, err
}
return formatted, nil
}

func mustReadFile(filename string) []byte {
b, err := ioutil.ReadFile(filename)
if err != nil {
panic(err)
}
return b
}
2 changes: 1 addition & 1 deletion subscription_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ func TestSchemaSubscribe(t *testing.T) {
},
{
Name: "schema_without_resolver_errors",
Schema: &graphql.Schema{},
Schema: graphql.MustParseSchema(schema, nil),
Query: `
subscription onHelloSaid {
helloSaid {
Expand Down
3 changes: 2 additions & 1 deletion subscriptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package graphql
import (
"context"
"errors"
"reflect"

qerrors "github.com/graph-gophers/graphql-go/errors"
"github.com/graph-gophers/graphql-go/internal/common"
Expand All @@ -20,7 +21,7 @@ import (
// further resolvers will be called. The context error will be returned as soon
// as possible (not immediately).
func (s *Schema) Subscribe(ctx context.Context, queryString string, operationName string, variables map[string]interface{}) (<-chan interface{}, error) {
if s.res == nil {
if s.res.Resolver == (reflect.Value{}) {
return nil, errors.New("schema created without resolver, can not subscribe")
}
return s.subscribe(ctx, queryString, operationName, variables, s.res), nil
Expand Down

0 comments on commit c126f0e

Please sign in to comment.