Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Panic on Introspection of Schema using ToJSON #328

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
dackroyd marked this conversation as resolved.
Show resolved Hide resolved
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