Skip to content

Commit

Permalink
extract shared handler test server stubs
Browse files Browse the repository at this point in the history
  • Loading branch information
vektah committed Oct 31, 2019
1 parent a70e93b commit 64cfc9a
Show file tree
Hide file tree
Showing 8 changed files with 214 additions and 203 deletions.
64 changes: 7 additions & 57 deletions graphql/handler/apollotracing/tracer_test.go
Original file line number Diff line number Diff line change
@@ -1,76 +1,26 @@
package apollotracing_test

import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"

"github.com/99designs/gqlgen/graphql"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/handler/apollotracing"
"github.com/99designs/gqlgen/graphql/handler/testserver"
"github.com/99designs/gqlgen/graphql/handler/transport"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/vektah/gqlparser"
"github.com/vektah/gqlparser/ast"
)

// todo: extract out common code for testing handler plugins without requiring a codegenned server.
func TestApolloTracing(t *testing.T) {
now := time.Unix(0, 0)

graphql.Now = func() time.Time {
defer func() {
now = now.Add(100 * time.Nanosecond)
}()
return now
}

schema := gqlparser.MustLoadSchema(&ast.Source{Input: `
schema { query: Query }
type Query {
me: User!
user(id: Int): User!
}
type User { name: String! }
`})

es := &graphql.ExecutableSchemaMock{
QueryFunc: func(ctx context.Context, op *ast.OperationDefinition) *graphql.Response {
// Field execution happens inside the generated code, we want just enough to test against right now.
ctx = graphql.WithResolverContext(ctx, &graphql.ResolverContext{
Object: "Query",
Field: graphql.CollectedField{
Field: &ast.Field{
Name: "me",
Alias: "me",
Definition: schema.Types["Query"].Fields.ForName("me"),
},
},
})
res, err := graphql.GetRequestContext(ctx).ResolverMiddleware(ctx, func(ctx context.Context) (interface{}, error) {
return &graphql.Response{Data: []byte(`{"name":"test"}`)}, nil
})
require.NoError(t, err)
return res.(*graphql.Response)
},
MutationFunc: func(ctx context.Context, op *ast.OperationDefinition) *graphql.Response {
return graphql.ErrorResponse(ctx, "mutations are not supported")
},
SchemaFunc: func() *ast.Schema {
return schema
},
}
h := handler.New(es)
h := testserver.New()
h.AddTransport(transport.POST{})
h.Use(apollotracing.New())

resp := doRequest(h, "POST", "/graphql", `{"query":"{ me { name } }"}`)
assert.Equal(t, http.StatusOK, resp.Code)
resp := doRequest(h, "POST", "/graphql", `{"query":"{ name }"}`)
assert.Equal(t, http.StatusOK, resp.Code, resp.Body.String())
var respData struct {
Extensions struct {
Tracing apollotracing.TracingExtension `json:"tracing"`
Expand All @@ -94,10 +44,10 @@ func TestApolloTracing(t *testing.T) {

require.EqualValues(t, 500, tracing.Execution.Resolvers[0].StartOffset)
require.EqualValues(t, 100, tracing.Execution.Resolvers[0].Duration)
require.EqualValues(t, []interface{}{"me"}, tracing.Execution.Resolvers[0].Path)
require.EqualValues(t, []interface{}{"name"}, tracing.Execution.Resolvers[0].Path)
require.EqualValues(t, "Query", tracing.Execution.Resolvers[0].ParentType)
require.EqualValues(t, "me", tracing.Execution.Resolvers[0].FieldName)
require.EqualValues(t, "User!", tracing.Execution.Resolvers[0].ReturnType)
require.EqualValues(t, "name", tracing.Execution.Resolvers[0].FieldName)
require.EqualValues(t, "String!", tracing.Execution.Resolvers[0].ReturnType)

}

Expand Down
54 changes: 13 additions & 41 deletions graphql/handler/server_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package handler
package handler_test

import (
"context"
Expand All @@ -8,41 +8,13 @@ import (
"testing"

"github.com/99designs/gqlgen/graphql"
"github.com/99designs/gqlgen/graphql/handler/testserver"
"github.com/99designs/gqlgen/graphql/handler/transport"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/vektah/gqlparser/ast"
)

func TestServer(t *testing.T) {
es := &graphql.ExecutableSchemaMock{
QueryFunc: func(ctx context.Context, op *ast.OperationDefinition) *graphql.Response {
// Field execution happens inside the generated code, we want just enough to test against right now.
res, err := graphql.GetRequestContext(ctx).ResolverMiddleware(ctx, func(ctx context.Context) (interface{}, error) {
return &graphql.Response{Data: []byte(`"query resp"`)}, nil
})
require.NoError(t, err)

return res.(*graphql.Response)
},
MutationFunc: func(ctx context.Context, op *ast.OperationDefinition) *graphql.Response {
return &graphql.Response{Data: []byte(`"mutation resp"`)}
},
SubscriptionFunc: func(ctx context.Context, op *ast.OperationDefinition) func() *graphql.Response {
called := 0
return func() *graphql.Response {
called++
if called > 2 {
return nil
}
return &graphql.Response{Data: []byte(`"subscription resp"`)}
}
},
SchemaFunc: func() *ast.Schema {
return &ast.Schema{}
},
}
srv := New(es)
srv := testserver.New()
srv.AddTransport(&transport.GET{})

t.Run("returns an error if no transport matches", func(t *testing.T) {
Expand All @@ -52,20 +24,20 @@ func TestServer(t *testing.T) {
})

t.Run("calls query on executable schema", func(t *testing.T) {
resp := get(srv, "/foo?query={a}")
resp := get(srv, "/foo?query={name}")
assert.Equal(t, http.StatusOK, resp.Code)
assert.Equal(t, `{"data":"query resp"}`, resp.Body.String())
assert.Equal(t, `{"data":{"name":"test"}}`, resp.Body.String())
})

t.Run("mutations are forbidden", func(t *testing.T) {
resp := get(srv, "/foo?query=mutation{a}")
assert.Equal(t, http.StatusOK, resp.Code)
resp := get(srv, "/foo?query=mutation{name}")
assert.Equal(t, http.StatusNotAcceptable, resp.Code)
assert.Equal(t, `{"errors":[{"message":"GET requests only allow query operations"}],"data":null}`, resp.Body.String())
})

t.Run("subscriptions are forbidden", func(t *testing.T) {
resp := get(srv, "/foo?query=subscription{a}")
assert.Equal(t, http.StatusOK, resp.Code)
resp := get(srv, "/foo?query=subscription{name}")
assert.Equal(t, http.StatusNotAcceptable, resp.Code)
assert.Equal(t, `{"errors":[{"message":"GET requests only allow query operations"}],"data":null}`, resp.Body.String())
})

Expand All @@ -80,8 +52,8 @@ func TestServer(t *testing.T) {
next(ctx, writer)
}))

resp := get(srv, "/foo?query={a}")
assert.Equal(t, http.StatusOK, resp.Code)
resp := get(srv, "/foo?query={name}")
assert.Equal(t, http.StatusOK, resp.Code, resp.Body.String())
assert.Equal(t, []string{"first", "second"}, calls)
})

Expand All @@ -98,8 +70,8 @@ func TestServer(t *testing.T) {
return next(ctx)
}))

resp := get(srv, "/foo?query={a}")
assert.Equal(t, http.StatusOK, resp.Code)
resp := get(srv, "/foo?query={name}")
assert.Equal(t, http.StatusOK, resp.Code, resp.Body.String())
assert.Equal(t, []string{"first", "second"}, calls)
})
}
Expand Down
99 changes: 99 additions & 0 deletions graphql/handler/testserver/testserver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package testserver

import (
"context"
"fmt"
"time"

"github.com/99designs/gqlgen/graphql"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/vektah/gqlparser"
"github.com/vektah/gqlparser/ast"
)

// New provides a server for use in tests that isn't relying on generated code. It isnt a perfect reproduction of
// a generated server, but it aims to be good enough to test the handler package without relying on codegen.
func New() *TestServer {
next := make(chan struct{})
now := time.Unix(0, 0)

graphql.Now = func() time.Time {
defer func() {
now = now.Add(100 * time.Nanosecond)
}()
return now
}

schema := gqlparser.MustLoadSchema(&ast.Source{Input: `
schema { query: Query }
type Query {
name: String!
find(id: Int!): String!
}
type Mutation {
name: String!
}
type Subscription {
name: String!
}
`})

es := &graphql.ExecutableSchemaMock{
QueryFunc: func(ctx context.Context, op *ast.OperationDefinition) *graphql.Response {
// Field execution happens inside the generated code, lets simulate some of it.
ctx = graphql.WithResolverContext(ctx, &graphql.ResolverContext{
Object: "Query",
Field: graphql.CollectedField{
Field: &ast.Field{
Name: "name",
Alias: "name",
Definition: schema.Types["Query"].Fields.ForName("name"),
},
},
})
res, err := graphql.GetRequestContext(ctx).ResolverMiddleware(ctx, func(ctx context.Context) (interface{}, error) {
return &graphql.Response{Data: []byte(`{"name":"test"}`)}, nil
})
if err != nil {
panic(err)
}
return res.(*graphql.Response)
},
MutationFunc: func(ctx context.Context, op *ast.OperationDefinition) *graphql.Response {
return graphql.ErrorResponse(ctx, "mutations are not supported")
},
SubscriptionFunc: func(ctx context.Context, op *ast.OperationDefinition) func() *graphql.Response {
return func() *graphql.Response {
select {
case <-ctx.Done():
return nil
case <-next:
return &graphql.Response{
Data: []byte(`{"name":"test"}`),
}
}
}
},
SchemaFunc: func() *ast.Schema {
return schema
},
}
return &TestServer{
Server: handler.New(es),
next: next,
}
}

type TestServer struct {
*handler.Server
next chan struct{}
}

func (s *TestServer) SendNextSubscriptionMessage() {
select {
case s.next <- struct{}{}:
case <-time.After(1 * time.Second):
fmt.Println("WARNING: no active subscription")
}

}
7 changes: 5 additions & 2 deletions graphql/handler/transport/http_get.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ import (
"net/http"
"strings"

"github.com/vektah/gqlparser/ast"

"github.com/99designs/gqlgen/graphql"
"github.com/vektah/gqlparser/ast"
)

// GET implements the GET side of the default HTTP transport
Expand Down Expand Up @@ -47,13 +46,15 @@ func (H GET) Do(w http.ResponseWriter, r *http.Request, exec graphql.GraphExecut

if variables := r.URL.Query().Get("variables"); variables != "" {
if err := jsonDecode(strings.NewReader(variables), &raw.Variables); err != nil {
w.WriteHeader(http.StatusBadRequest)
writer.Errorf("variables could not be decoded")
return
}
}

if extensions := r.URL.Query().Get("extensions"); extensions != "" {
if err := jsonDecode(strings.NewReader(extensions), &raw.Extensions); err != nil {
w.WriteHeader(http.StatusBadRequest)
writer.Errorf("extensions could not be decoded")
return
}
Expand All @@ -63,9 +64,11 @@ func (H GET) Do(w http.ResponseWriter, r *http.Request, exec graphql.GraphExecut
if err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writer.GraphqlErr(err...)
return
}
op := rc.Doc.Operations.ForName(rc.OperationName)
if op.Operation != ast.Query {
w.WriteHeader(http.StatusNotAcceptable)
writer.Errorf("GET requests only allow query operations")
return
}
Expand Down
45 changes: 45 additions & 0 deletions graphql/handler/transport/http_get_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package transport_test

import (
"net/http"
"testing"

"github.com/99designs/gqlgen/graphql/handler/testserver"
"github.com/99designs/gqlgen/graphql/handler/transport"
"github.com/stretchr/testify/assert"
)

func TestGET(t *testing.T) {
h := testserver.New()
h.AddTransport(transport.GET{})

t.Run("success", func(t *testing.T) {
resp := doRequest(h, "GET", "/graphql?query={name}", ``)
assert.Equal(t, http.StatusOK, resp.Code, resp.Body.String())
assert.Equal(t, `{"data":{"name":"test"}}`, resp.Body.String())
})

t.Run("decode failure", func(t *testing.T) {
resp := doRequest(h, "GET", "/graphql?query={name}&variables=notjson", "")
assert.Equal(t, http.StatusBadRequest, resp.Code, resp.Body.String())
assert.Equal(t, `{"errors":[{"message":"variables could not be decoded"}],"data":null}`, resp.Body.String())
})

t.Run("invalid variable", func(t *testing.T) {
resp := doRequest(h, "GET", `/graphql?query=query($id:Int!){find(id:$id)}&variables={"id":false}`, "")
assert.Equal(t, http.StatusUnprocessableEntity, resp.Code, resp.Body.String())
assert.Equal(t, `{"errors":[{"message":"cannot use bool as Int","path":["variable","id"]}],"data":null}`, resp.Body.String())
})

t.Run("parse failure", func(t *testing.T) {
resp := doRequest(h, "GET", "/graphql?query=!", "")
assert.Equal(t, http.StatusUnprocessableEntity, resp.Code, resp.Body.String())
assert.Equal(t, `{"errors":[{"message":"Unexpected !","locations":[{"line":1,"column":1}]}],"data":null}`, resp.Body.String())
})

t.Run("no mutations", func(t *testing.T) {
resp := doRequest(h, "GET", "/graphql?query=mutation{name}", "")
assert.Equal(t, http.StatusNotAcceptable, resp.Code, resp.Body.String())
assert.Equal(t, `{"errors":[{"message":"GET requests only allow query operations"}],"data":null}`, resp.Body.String())
})
}
Loading

0 comments on commit 64cfc9a

Please sign in to comment.