From 177b9a1a8f94288d69e26efe38e819eeb212409d Mon Sep 17 00:00:00 2001 From: Matias Anaya Date: Wed, 25 Apr 2018 18:33:53 +1000 Subject: [PATCH 1/4] Support subscriptions --- gqltesting/subscriptions.go | 110 ++++++++++++++ internal/exec/resolvable/resolvable.go | 44 ++++-- internal/exec/selected/selected.go | 2 + internal/exec/subscribe.go | 147 +++++++++++++++++++ subscription_test.go | 196 +++++++++++++++++++++++++ subscriptions.go | 91 ++++++++++++ 6 files changed, 577 insertions(+), 13 deletions(-) create mode 100644 gqltesting/subscriptions.go create mode 100644 internal/exec/subscribe.go create mode 100644 subscription_test.go create mode 100644 subscriptions.go diff --git a/gqltesting/subscriptions.go b/gqltesting/subscriptions.go new file mode 100644 index 00000000..7a8ea43e --- /dev/null +++ b/gqltesting/subscriptions.go @@ -0,0 +1,110 @@ +package gqltesting + +import ( + "bytes" + "context" + "encoding/json" + "strconv" + "testing" + + graphql "github.com/graph-gophers/graphql-go" + "github.com/graph-gophers/graphql-go/errors" +) + +// TestResponse models the expected response +type TestResponse struct { + Data json.RawMessage + Errors []*errors.QueryError +} + +// TestSubscription is a GraphQL test case to be used with RunSubscribe. +type TestSubscription struct { + Name string + Schema *graphql.Schema + Query string + OperationName string + Variables map[string]interface{} + ExpectedResults []TestResponse + ExpectedErr error +} + +// RunSubscribes runs the given GraphQL subscription test cases as subtests. +func RunSubscribes(t *testing.T, tests []*TestSubscription) { + for i, test := range tests { + if test.Name == "" { + test.Name = strconv.Itoa(i + 1) + } + + t.Run(test.Name, func(t *testing.T) { + RunSubscribe(t, test) + }) + } +} + +// RunSubscribe runs a single GraphQL subscription test case. +func RunSubscribe(t *testing.T, test *TestSubscription) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + c, err := test.Schema.Subscribe(ctx, test.Query, test.OperationName, test.Variables) + if err != nil { + if err.Error() != test.ExpectedErr.Error() { + t.Fatalf("unexpected error: got %+v, want %+v", err, test.ExpectedErr) + } + + return + } + + var results []*graphql.Response + for res := range c { + results = append(results, res) + } + + for i, expected := range test.ExpectedResults { + res := results[i] + + checkErrorStrings(t, expected.Errors, res.Errors) + + resData, err := res.Data.MarshalJSON() + if err != nil { + t.Fatal(err) + } + got := formatJSON(t, resData) + expectedData, err := expected.Data.MarshalJSON() + if err != nil { + t.Fatal(err) + } + want := formatJSON(t, expectedData) + + if !bytes.Equal(got, want) { + t.Logf("got: %s", got) + t.Logf("want: %s", want) + t.Fail() + } + } +} + +func checkErrorStrings(t *testing.T, expected, actual []*errors.QueryError) { + expectedCount, actualCount := len(expected), len(actual) + + if expectedCount != actualCount { + t.Fatalf("unexpected number of errors: want %d, got %d", expectedCount, actualCount) + } + + if expectedCount > 0 { + for i, want := range expected { + got := actual[i] + + if got.Error() != want.Error() { + t.Fatalf("unexpected error: got %+v, want %+v", got, want) + } + } + + // Return because we're done checking. + return + } + + for _, err := range actual { + t.Errorf("unexpected error: '%s'", err) + } +} diff --git a/internal/exec/resolvable/resolvable.go b/internal/exec/resolvable/resolvable.go index b7c1d93d..aa2fc1bd 100644 --- a/internal/exec/resolvable/resolvable.go +++ b/internal/exec/resolvable/resolvable.go @@ -13,9 +13,10 @@ import ( type Schema struct { schema.Schema - Query Resolvable - Mutation Resolvable - Resolver reflect.Value + Query Resolvable + Mutation Resolvable + Subscription Resolvable + Resolver reflect.Value } type Resolvable interface { @@ -57,7 +58,7 @@ func (*Scalar) isResolvable() {} func ApplyResolver(s *schema.Schema, resolver interface{}) (*Schema, error) { b := newBuilder(s) - var query, mutation Resolvable + var query, mutation, subscription Resolvable if t, ok := s.EntryPoints["query"]; ok { if err := b.assignExec(&query, t, reflect.TypeOf(resolver)); err != nil { @@ -71,15 +72,22 @@ func ApplyResolver(s *schema.Schema, resolver interface{}) (*Schema, error) { } } + if t, ok := s.EntryPoints["subscription"]; ok { + if err := b.assignExec(&subscription, t, reflect.TypeOf(resolver)); err != nil { + return nil, err + } + } + if err := b.finish(); err != nil { return nil, err } return &Schema{ - Schema: *s, - Resolver: reflect.ValueOf(resolver), - Query: query, - Mutation: mutation, + Schema: *s, + Resolver: reflect.ValueOf(resolver), + Query: query, + Mutation: mutation, + Subscription: subscription, }, nil } @@ -284,14 +292,19 @@ func (b *execBuilder) makeFieldExec(typeName string, f *schema.Field, m reflect. return nil, fmt.Errorf("too many parameters") } - if m.Type.NumOut() > 2 { + maxNumOfReturns := 2 + if m.Type.NumOut() < maxNumOfReturns-1 { + return nil, fmt.Errorf("too few return values") + } + + if m.Type.NumOut() > maxNumOfReturns { return nil, fmt.Errorf("too many return values") } - hasError := m.Type.NumOut() == 2 + hasError := m.Type.NumOut() == maxNumOfReturns if hasError { - if m.Type.Out(1) != errorType { - return nil, fmt.Errorf(`must have "error" as its second return value`) + if m.Type.Out(maxNumOfReturns-1) != errorType { + return nil, fmt.Errorf(`must have "error" as its last return value`) } } @@ -304,7 +317,12 @@ func (b *execBuilder) makeFieldExec(typeName string, f *schema.Field, m reflect. HasError: hasError, TraceLabel: fmt.Sprintf("GraphQL field: %s.%s", typeName, f.Name), } - if err := b.assignExec(&fe.ValueExec, f.Type, m.Type.Out(0)); err != nil { + + out := m.Type.Out(0) + if typeName == "Subscription" && out.Kind() == reflect.Chan { + out = m.Type.Out(0).Elem() + } + if err := b.assignExec(&fe.ValueExec, f.Type, out); err != nil { return nil, err } return fe, nil diff --git a/internal/exec/selected/selected.go b/internal/exec/selected/selected.go index aed079b6..2a957f55 100644 --- a/internal/exec/selected/selected.go +++ b/internal/exec/selected/selected.go @@ -35,6 +35,8 @@ func ApplyOperation(r *Request, s *resolvable.Schema, op *query.Operation) []Sel obj = s.Query.(*resolvable.Object) case query.Mutation: obj = s.Mutation.(*resolvable.Object) + case query.Subscription: + obj = s.Subscription.(*resolvable.Object) } return applySelectionSet(r, obj, op.Selections) } diff --git a/internal/exec/subscribe.go b/internal/exec/subscribe.go new file mode 100644 index 00000000..03826f89 --- /dev/null +++ b/internal/exec/subscribe.go @@ -0,0 +1,147 @@ +package exec + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "reflect" + "time" + + "github.com/graph-gophers/graphql-go/errors" + "github.com/graph-gophers/graphql-go/internal/exec/resolvable" + "github.com/graph-gophers/graphql-go/internal/exec/selected" + "github.com/graph-gophers/graphql-go/internal/query" +) + +type Response struct { + Data json.RawMessage + Errors []*errors.QueryError +} + +func (r *Request) Subscribe(ctx context.Context, s *resolvable.Schema, op *query.Operation) <-chan *Response { + var result reflect.Value + var f *fieldToExec + var err *errors.QueryError + func() { + defer r.handlePanic(ctx) + + sels := selected.ApplyOperation(&r.Request, s, op) + var fields []*fieldToExec + collectFieldsToResolve(sels, s.Resolver, &fields, make(map[string]*fieldToExec)) + + // TODO: move this check into validation.Validate + if len(fields) != 1 { + err = errors.Errorf("%s", "can subscribe to at most one subscription at a time") + return + } + f = fields[0] + + var in []reflect.Value + if f.field.HasContext { + in = append(in, reflect.ValueOf(ctx)) + } + if f.field.ArgsPacker != nil { + in = append(in, f.field.PackedArgs) + } + callOut := f.resolver.Method(f.field.MethodIndex).Call(in) + result = callOut[0] + + if f.field.HasError && !callOut[1].IsNil() { + resolverErr := callOut[1].Interface().(error) + err = errors.Errorf("%s", resolverErr) + err.ResolverError = resolverErr + } + }() + + if err != nil { + return sendAndReturnClosed(&Response{Errors: []*errors.QueryError{err}}) + } + + if ctxErr := ctx.Err(); ctxErr != nil { + return sendAndReturnClosed(&Response{Errors: []*errors.QueryError{errors.Errorf("%s", ctxErr)}}) + } + + c := make(chan *Response) + // TODO: handle resolver nil channel better? + if result == reflect.Zero(result.Type()) { + close(c) + return c + } + + go func() { + for { + // Check subscription context + chosen, resp, ok := reflect.Select([]reflect.SelectCase{ + { + Dir: reflect.SelectRecv, + Chan: reflect.ValueOf(ctx.Done()), + }, + { + Dir: reflect.SelectRecv, + Chan: result, + }, + }) + switch chosen { + // subscription context done + case 0: + close(c) + return + // upstream received + case 1: + // upstream closed + if !ok { + close(c) + return + } + + subR := &Request{ + Request: selected.Request{ + Doc: r.Request.Doc, + Vars: r.Request.Vars, + Schema: r.Request.Schema, + }, + Limiter: r.Limiter, + Tracer: r.Tracer, + Logger: r.Logger, + } + var out bytes.Buffer + func() { + // TODO: configurable timeout + subCtx, cancel := context.WithTimeout(ctx, time.Second) + defer cancel() + + // resolve response + func() { + defer subR.handlePanic(subCtx) + + out.WriteString(fmt.Sprintf(`{"%s":`, f.field.Alias)) + subR.execSelectionSet(subCtx, f.sels, f.field.Type, &pathSegment{nil, f.field.Alias}, resp, &out) + out.WriteString(`}`) + }() + + if err := subCtx.Err(); err != nil { + c <- &Response{Errors: []*errors.QueryError{errors.Errorf("%s", err)}} + return + } + + // Send response within timeout + // TODO: maybe block until sent? + select { + case <-subCtx.Done(): + case c <- &Response{Data: out.Bytes(), Errors: subR.Errs}: + } + }() + } + } + }() + + return c +} + +func sendAndReturnClosed(resp *Response) chan *Response { + c := make(chan *Response, 1) + c <- resp + close(c) + return c +} diff --git a/subscription_test.go b/subscription_test.go new file mode 100644 index 00000000..95a63a2e --- /dev/null +++ b/subscription_test.go @@ -0,0 +1,196 @@ +package graphql_test + +import ( + "context" + "encoding/json" + stdErrors "errors" + "testing" + + graphql "github.com/graph-gophers/graphql-go" + "github.com/graph-gophers/graphql-go/errors" + "github.com/graph-gophers/graphql-go/gqltesting" +) + +type rootResolver struct { + *helloResolver + *helloSaidResolver +} + +type helloResolver struct{} + +func (r *helloResolver) Hello() string { + return "Hello world!" +} + +var resolverErr = stdErrors.New("resolver error") + +type helloSaidResolver struct { + err error + upstream <-chan *helloSaidEventResolver +} + +type helloSaidEventResolver struct { + msg string + err error +} + +func (r *helloSaidResolver) HelloSaid(ctx context.Context) (chan *helloSaidEventResolver, error) { + if r.err != nil { + return nil, r.err + } + + c := make(chan *helloSaidEventResolver) + go func() { + for r := range r.upstream { + select { + case <-ctx.Done(): + close(c) + return + case c <- r: + } + } + close(c) + }() + + return c, nil +} + +func (r *helloSaidEventResolver) Msg() (string, error) { + return r.msg, r.err +} + +func closedUpstream(rr ...*helloSaidEventResolver) <-chan *helloSaidEventResolver { + c := make(chan *helloSaidEventResolver, len(rr)) + for _, r := range rr { + c <- r + } + close(c) + return c +} + +func TestSchemaSubscribe(t *testing.T) { + gqltesting.RunSubscribes(t, []*gqltesting.TestSubscription{ + { + Name: "ok", + Schema: graphql.MustParseSchema(schema, &rootResolver{ + helloSaidResolver: &helloSaidResolver{ + upstream: closedUpstream( + &helloSaidEventResolver{msg: "Hello world!"}, + &helloSaidEventResolver{err: resolverErr}, + &helloSaidEventResolver{msg: "Hello again!"}, + ), + }, + }), + Query: ` + subscription onHelloSaid { + helloSaid { + msg + } + } + `, + ExpectedResults: []gqltesting.TestResponse{ + { + Data: json.RawMessage(` + { + "helloSaid": { + "msg": "Hello world!" + } + } + `), + }, + { + Data: json.RawMessage(` + { + "helloSaid": { + "msg":null + } + } + `), + Errors: []*errors.QueryError{errors.Errorf("%s", resolverErr)}, + }, + { + Data: json.RawMessage(` + { + "helloSaid": { + "msg": "Hello again!" + } + } + `), + }, + }, + }, + { + Name: "parse_errors", + Schema: graphql.MustParseSchema(schema, &rootResolver{}), + Query: `invalid graphQL query`, + ExpectedResults: []gqltesting.TestResponse{ + { + Errors: []*errors.QueryError{errors.Errorf("%s", `syntax error: unexpected "invalid", expecting "fragment" (line 1, column 9)`)}, + }, + }, + }, + { + Name: "subscribe_to_query_errors", + Schema: graphql.MustParseSchema(schema, &rootResolver{}), + Query: ` + query Hello { + hello + } + `, + ExpectedResults: []gqltesting.TestResponse{ + { + Errors: []*errors.QueryError{errors.Errorf("%s: %s", "subscription unavailable for operation of type", "QUERY")}, + }, + }, + }, + { + Name: "subscription_resolver_can_error", + Schema: graphql.MustParseSchema(schema, &rootResolver{ + helloSaidResolver: &helloSaidResolver{err: resolverErr}, + }), + Query: ` + subscription onHelloSaid { + helloSaid { + msg + } + } + `, + ExpectedResults: []gqltesting.TestResponse{ + { + Errors: []*errors.QueryError{errors.Errorf("%s", resolverErr)}, + }, + }, + }, + { + Name: "schema_without_resolver_errors", + Schema: &graphql.Schema{}, + Query: ` + subscription onHelloSaid { + helloSaid { + msg + } + } + `, + ExpectedErr: stdErrors.New("schema created without resolver, can not subscribe"), + }, + }) +} + +const schema = ` + schema { + subscription: Subscription, + query: Query + } + + type Subscription { + helloSaid: HelloSaidEvent! + } + + type HelloSaidEvent { + msg: String! + } + + type Query { + hello: String! + } +` diff --git a/subscriptions.go b/subscriptions.go new file mode 100644 index 00000000..4f7aa263 --- /dev/null +++ b/subscriptions.go @@ -0,0 +1,91 @@ +package graphql + +import ( + "context" + stdErrors "errors" + + "github.com/graph-gophers/graphql-go/errors" + "github.com/graph-gophers/graphql-go/internal/common" + "github.com/graph-gophers/graphql-go/internal/exec" + "github.com/graph-gophers/graphql-go/internal/exec/resolvable" + "github.com/graph-gophers/graphql-go/internal/exec/selected" + "github.com/graph-gophers/graphql-go/internal/query" + "github.com/graph-gophers/graphql-go/internal/validation" + "github.com/graph-gophers/graphql-go/introspection" +) + +// Subscribe returns a response channel for the given subscription with the schema's +// resolver. It returns an error if the schema was created without a resolver. +// If the context gets cancelled, the response channel will be closed and no +// 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 *Response, error) { + if s.res == nil { + return nil, stdErrors.New("schema created without resolver, can not subscribe") + } + return s.subscribe(ctx, queryString, operationName, variables, s.res), nil +} + +func (s *Schema) subscribe(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, res *resolvable.Schema) <-chan *Response { + doc, qErr := query.Parse(queryString) + if qErr != nil { + return sendAndReturnClosed(&Response{Errors: []*errors.QueryError{qErr}}) + } + + validationFinish := s.validationTracer.TraceValidation() + errs := validation.Validate(s.schema, doc, s.maxDepth) + validationFinish(errs) + if len(errs) != 0 { + return sendAndReturnClosed(&Response{Errors: errs}) + } + + op, err := getOperation(doc, operationName) + if err != nil { + return sendAndReturnClosed(&Response{Errors: []*errors.QueryError{errors.Errorf("%s", err)}}) + } + + // TODO: Move to validation.Validate? + if op.Type != query.Subscription { + return sendAndReturnClosed(&Response{Errors: []*errors.QueryError{errors.Errorf("%s: %s", "subscription unavailable for operation of type", op.Type)}}) + } + + r := &exec.Request{ + Request: selected.Request{ + Doc: doc, + Vars: variables, + Schema: s.schema, + }, + Limiter: make(chan struct{}, s.maxParallelism), + Tracer: s.tracer, + Logger: s.logger, + } + varTypes := make(map[string]*introspection.Type) + for _, v := range op.Vars { + t, err := common.ResolveType(v.Type, s.schema.Resolve) + if err != nil { + return sendAndReturnClosed(&Response{Errors: []*errors.QueryError{err}}) + } + varTypes[v.Name.Name] = introspection.WrapType(t) + } + + responses := r.Subscribe(ctx, res, op) + c := make(chan *Response) + go func() { + for resp := range responses { + c <- &Response{ + Data: resp.Data, + Errors: resp.Errors, + } + } + close(c) + }() + + return c +} + +func sendAndReturnClosed(resp *Response) chan *Response { + c := make(chan *Response, 1) + c <- resp + close(c) + return c +} From 3add01de2cb5556d662e49fcdae034f6d60a43c4 Mon Sep 17 00:00:00 2001 From: Matias Anaya Date: Mon, 7 May 2018 20:25:03 +1000 Subject: [PATCH 2/4] Remove stdErrors alias --- subscription_test.go | 16 ++++++++-------- subscriptions.go | 14 +++++++------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/subscription_test.go b/subscription_test.go index 95a63a2e..58e302da 100644 --- a/subscription_test.go +++ b/subscription_test.go @@ -3,11 +3,11 @@ package graphql_test import ( "context" "encoding/json" - stdErrors "errors" + "errors" "testing" graphql "github.com/graph-gophers/graphql-go" - "github.com/graph-gophers/graphql-go/errors" + qerrors "github.com/graph-gophers/graphql-go/errors" "github.com/graph-gophers/graphql-go/gqltesting" ) @@ -22,7 +22,7 @@ func (r *helloResolver) Hello() string { return "Hello world!" } -var resolverErr = stdErrors.New("resolver error") +var resolverErr = errors.New("resolver error") type helloSaidResolver struct { err error @@ -106,7 +106,7 @@ func TestSchemaSubscribe(t *testing.T) { } } `), - Errors: []*errors.QueryError{errors.Errorf("%s", resolverErr)}, + Errors: []*qerrors.QueryError{qerrors.Errorf("%s", resolverErr)}, }, { Data: json.RawMessage(` @@ -125,7 +125,7 @@ func TestSchemaSubscribe(t *testing.T) { Query: `invalid graphQL query`, ExpectedResults: []gqltesting.TestResponse{ { - Errors: []*errors.QueryError{errors.Errorf("%s", `syntax error: unexpected "invalid", expecting "fragment" (line 1, column 9)`)}, + Errors: []*qerrors.QueryError{qerrors.Errorf("%s", `syntax error: unexpected "invalid", expecting "fragment" (line 1, column 9)`)}, }, }, }, @@ -139,7 +139,7 @@ func TestSchemaSubscribe(t *testing.T) { `, ExpectedResults: []gqltesting.TestResponse{ { - Errors: []*errors.QueryError{errors.Errorf("%s: %s", "subscription unavailable for operation of type", "QUERY")}, + Errors: []*qerrors.QueryError{qerrors.Errorf("%s: %s", "subscription unavailable for operation of type", "QUERY")}, }, }, }, @@ -157,7 +157,7 @@ func TestSchemaSubscribe(t *testing.T) { `, ExpectedResults: []gqltesting.TestResponse{ { - Errors: []*errors.QueryError{errors.Errorf("%s", resolverErr)}, + Errors: []*qerrors.QueryError{qerrors.Errorf("%s", resolverErr)}, }, }, }, @@ -171,7 +171,7 @@ func TestSchemaSubscribe(t *testing.T) { } } `, - ExpectedErr: stdErrors.New("schema created without resolver, can not subscribe"), + ExpectedErr: errors.New("schema created without resolver, can not subscribe"), }, }) } diff --git a/subscriptions.go b/subscriptions.go index 4f7aa263..2c1731bd 100644 --- a/subscriptions.go +++ b/subscriptions.go @@ -2,9 +2,9 @@ package graphql import ( "context" - stdErrors "errors" + "errors" - "github.com/graph-gophers/graphql-go/errors" + qerrors "github.com/graph-gophers/graphql-go/errors" "github.com/graph-gophers/graphql-go/internal/common" "github.com/graph-gophers/graphql-go/internal/exec" "github.com/graph-gophers/graphql-go/internal/exec/resolvable" @@ -21,7 +21,7 @@ import ( // as possible (not immediately). func (s *Schema) Subscribe(ctx context.Context, queryString string, operationName string, variables map[string]interface{}) (<-chan *Response, error) { if s.res == nil { - return nil, stdErrors.New("schema created without resolver, can not subscribe") + return nil, errors.New("schema created without resolver, can not subscribe") } return s.subscribe(ctx, queryString, operationName, variables, s.res), nil } @@ -29,7 +29,7 @@ func (s *Schema) Subscribe(ctx context.Context, queryString string, operationNam func (s *Schema) subscribe(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, res *resolvable.Schema) <-chan *Response { doc, qErr := query.Parse(queryString) if qErr != nil { - return sendAndReturnClosed(&Response{Errors: []*errors.QueryError{qErr}}) + return sendAndReturnClosed(&Response{Errors: []*qerrors.QueryError{qErr}}) } validationFinish := s.validationTracer.TraceValidation() @@ -41,12 +41,12 @@ func (s *Schema) subscribe(ctx context.Context, queryString string, operationNam op, err := getOperation(doc, operationName) if err != nil { - return sendAndReturnClosed(&Response{Errors: []*errors.QueryError{errors.Errorf("%s", err)}}) + return sendAndReturnClosed(&Response{Errors: []*qerrors.QueryError{qerrors.Errorf("%s", err)}}) } // TODO: Move to validation.Validate? if op.Type != query.Subscription { - return sendAndReturnClosed(&Response{Errors: []*errors.QueryError{errors.Errorf("%s: %s", "subscription unavailable for operation of type", op.Type)}}) + return sendAndReturnClosed(&Response{Errors: []*qerrors.QueryError{qerrors.Errorf("%s: %s", "subscription unavailable for operation of type", op.Type)}}) } r := &exec.Request{ @@ -63,7 +63,7 @@ func (s *Schema) subscribe(ctx context.Context, queryString string, operationNam for _, v := range op.Vars { t, err := common.ResolveType(v.Type, s.schema.Resolve) if err != nil { - return sendAndReturnClosed(&Response{Errors: []*errors.QueryError{err}}) + return sendAndReturnClosed(&Response{Errors: []*qerrors.QueryError{err}}) } varTypes[v.Name.Name] = introspection.WrapType(t) } From a2b77e9f2dad2ced14b07fd2aae1b71059f159ef Mon Sep 17 00:00:00 2001 From: Matias Anaya Date: Mon, 7 May 2018 20:37:53 +1000 Subject: [PATCH 3/4] Fix graphql indentation --- subscription_test.go | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/subscription_test.go b/subscription_test.go index 58e302da..20a4d618 100644 --- a/subscription_test.go +++ b/subscription_test.go @@ -84,8 +84,8 @@ func TestSchemaSubscribe(t *testing.T) { Query: ` subscription onHelloSaid { helloSaid { - msg - } + msg + } } `, ExpectedResults: []gqltesting.TestResponse{ @@ -151,8 +151,8 @@ func TestSchemaSubscribe(t *testing.T) { Query: ` subscription onHelloSaid { helloSaid { - msg - } + msg + } } `, ExpectedResults: []gqltesting.TestResponse{ @@ -167,8 +167,8 @@ func TestSchemaSubscribe(t *testing.T) { Query: ` subscription onHelloSaid { helloSaid { - msg - } + msg + } } `, ExpectedErr: errors.New("schema created without resolver, can not subscribe"), @@ -177,18 +177,18 @@ func TestSchemaSubscribe(t *testing.T) { } const schema = ` - schema { - subscription: Subscription, + schema { + subscription: Subscription, query: Query - } + } - type Subscription { - helloSaid: HelloSaidEvent! - } + type Subscription { + helloSaid: HelloSaidEvent! + } - type HelloSaidEvent { - msg: String! - } + type HelloSaidEvent { + msg: String! + } type Query { hello: String! From cb1a8ec3ce857d5e8b77abae4c75942d473a716b Mon Sep 17 00:00:00 2001 From: Matias Anaya Date: Sat, 12 May 2018 07:14:16 +1000 Subject: [PATCH 4/4] Fix formatJSON() call after upstream change --- gqltesting/subscriptions.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/gqltesting/subscriptions.go b/gqltesting/subscriptions.go index 7a8ea43e..ea25514e 100644 --- a/gqltesting/subscriptions.go +++ b/gqltesting/subscriptions.go @@ -69,12 +69,19 @@ func RunSubscribe(t *testing.T, test *TestSubscription) { if err != nil { t.Fatal(err) } - got := formatJSON(t, resData) + got, err := formatJSON(resData) + if err != nil { + t.Fatalf("got: invalid JSON: %s", err) + } + expectedData, err := expected.Data.MarshalJSON() if err != nil { t.Fatal(err) } - want := formatJSON(t, expectedData) + want, err := formatJSON(expectedData) + if err != nil { + t.Fatalf("got: invalid JSON: %s", err) + } if !bytes.Equal(got, want) { t.Logf("got: %s", got)