Skip to content
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
55 changes: 54 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,59 @@ OUTPUT:
PASS
```

## GET Query Support

gqlcheck also supports GET requests for GraphQL queries, following the [GraphQL over HTTP specification](https://graphql.github.io/graphql-over-http/).

Use `TestWithMethod` to specify the HTTP method:

```go
func TestServerViaGet(t *testing.T) {
h := handler.New(
graph.NewExecutableSchema(
graph.Config{
Resolvers: &graph.Resolver{},
},
),
)
h.AddTransport(transport.GET{})

checker := gqlcheck.New(h, gqlcheck.Debug())
checker.TestWithMethod(t, http.MethodGet).
Query(`query {todos {text}}`).
Check().
HasStatusOK().
HasNoErrors().
HasData(map[string]any{
"todos": []any{},
})
}
```

OUTPUT:
```console
=== RUN TestServerViaGet
2024/02/05 11:31:12 == GET http://127.0.0.1:61506?query=query+%7Btodos+%7Btext%7D%7D
2024/02/05 11:31:12 >> header map[]
2024/02/05 11:31:12 >> body: nil
2024/02/05 11:31:12 << status: 200 OK
2024/02/05 11:31:12 << body: {"data":{"todos":[]}}
--- PASS: TestServerViaGet (0.00s)
PASS
```

You can also pass variables using `QueryWithVariables()`:

```go
checker.TestWithMethod(t, http.MethodGet).
QueryWithVariables(
`query GetUser($id: ID!) { user(id: $id) { name } }`,
map[string]any{"id": "123"},
).
Check().
HasStatusOK()
```

---

MIT
MIT
56 changes: 56 additions & 0 deletions testdata/gqlgen-todos/server_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"net/http"
"testing"

"github.com/99designs/gqlgen/graphql/handler"
Expand Down Expand Up @@ -30,3 +31,58 @@ func TestServer(t *testing.T) {
"todos": []any{},
})
}

func TestServerViaGet(t *testing.T) {
h := handler.New(
graph.NewExecutableSchema(
graph.Config{
Resolvers: &graph.Resolver{},
},
),
)
h.AddTransport(transport.GET{})

checker := gqlcheck.New(h, gqlcheck.Debug())
checker.TestWithMethod(t, http.MethodGet).
Query(`query {todos {text}}`).
Check().
HasStatusOK().
HasNoErrors().
HasData(map[string]any{
"todos": []any{},
})
}

func TestServerViaGetWithVariables(t *testing.T) {
h := handler.New(
graph.NewExecutableSchema(
graph.Config{
Resolvers: &graph.Resolver{},
},
),
)
h.AddTransport(transport.GET{})
h.AddTransport(transport.POST{})

checker := gqlcheck.New(h, gqlcheck.Debug())

// Create a todo via POST mutation with variables
checker.Test(t).
QueryWithVariables(
`mutation CreateTodo($input: NewTodo!) { createTodo(input: $input) { id text } }`,
map[string]any{"input": map[string]any{"text": "test todo", "userId": "user1"}},
).
Check().
HasStatusOK().
HasNoErrors()

// Query via GET (variables passed to demonstrate the feature)
checker.TestWithMethod(t, http.MethodGet).
QueryWithVariables(
`query { todos { text } }`,
map[string]any{},
).
Check().
HasStatusOK().
HasNoErrors()
}
63 changes: 60 additions & 3 deletions tester.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package gqlcheck

import (
"encoding/json"
"net/http"
"net/url"

"github.com/ikawaha/httpcheck"
)
Expand All @@ -14,19 +16,74 @@ type TestingT interface {

// Tester represents the GraphQL tester.
type Tester struct {
// For building request (before Check)
checker *Checker
t TestingT
method string
headers map[string]string
query string
variables map[string]any

// For response assertions (after Check)
client *httpcheck.Tester
}

// Test starts a new test with the given *testing.T.
// The default HTTP method is POST.
func (c *Checker) Test(t TestingT) *Tester {
return &Tester{
client: c.client.Test(t, http.MethodPost, "").
WithHeader("Content-Type", "application/graphql"),
checker: c,
t: t,
method: http.MethodPost, // default
headers: make(map[string]string),
}
}

// TestWithMethod starts a new test with the given *testing.T and HTTP method.
func (c *Checker) TestWithMethod(t TestingT, method string) *Tester {
return &Tester{
checker: c,
t: t,
method: method,
headers: make(map[string]string),
}
}

// Check makes request to built request object.
// After request is made, it saves response object for future assertions.
func (tt *Tester) Check() *Tester {
return &Tester{client: tt.client.Check()}
var client *httpcheck.Tester

switch tt.method {
case http.MethodGet:
// GET: query parameters in URL
params := url.Values{}
params.Set("query", tt.query)
if tt.variables != nil {
v, err := json.Marshal(tt.variables)
if err != nil {
tt.t.Errorf("failed to marshal variables: %v", err)
tt.t.FailNow()
}
params.Set("variables", string(v))
}
path := "?" + params.Encode()
client = tt.checker.client.Test(tt.t, http.MethodGet, path)
default:
// POST: JSON body
client = tt.checker.client.Test(tt.t, http.MethodPost, "").
WithHeader("Content-Type", "application/json")
body := map[string]any{"query": tt.query}
if tt.variables != nil {
body["variables"] = tt.variables
}
client = client.WithJSON(body)
}

// Apply headers
for k, v := range tt.headers {
client = client.WithHeader(k, v)
}

return &Tester{client: client.Check()}
}
9 changes: 7 additions & 2 deletions tester_auth.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package gqlcheck

import "encoding/base64"

// WithBasicAuth is an alias to set basic auth in the request header.
func (tt *Tester) WithBasicAuth(user, pass string) *Tester {
return &Tester{client: tt.client.WithBasicAuth(user, pass)}
auth := base64.StdEncoding.EncodeToString([]byte(user + ":" + pass))
tt.headers["Authorization"] = "Basic " + auth
return tt
}

// WithBearerAuth is an alias to set bearer auth in the request header.
func (tt *Tester) WithBearerAuth(token string) *Tester {
return &Tester{client: tt.client.WithHeader("Authorization", "Bearer: "+token)}
tt.headers["Authorization"] = "Bearer " + token
return tt
}
8 changes: 6 additions & 2 deletions tester_header.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ package gqlcheck

// WithHeader set header in the request.
func (tt *Tester) WithHeader(key, value string) *Tester {
return &Tester{client: tt.client.WithHeader(key, value)}
tt.headers[key] = value
return tt
}

// WithHeaders sets header in the request.
func (tt *Tester) WithHeaders(headers map[string]string) *Tester {
return &Tester{client: tt.client.WithHeaders(headers)}
for k, v := range headers {
tt.headers[k] = v
}
return tt
}
19 changes: 8 additions & 11 deletions tester_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,20 @@ func (q Query) String() string {

// Request sets the query and variables to the request.
func (tt *Tester) Request(q Query) *Tester {
return &Tester{client: tt.client.WithJSON(map[string]any{
"query": q.Query,
"variables": q.Variables,
})}
tt.query = q.Query
tt.variables = q.Variables
return tt
}

// Query sets the query to the request.
func (tt *Tester) Query(q string) *Tester {
return &Tester{client: tt.client.WithJSON(map[string]any{
"query": q,
})}
tt.query = q
return tt
}

// QueryWithVariables sets the query and variables to the request.
func (tt *Tester) QueryWithVariables(q string, variables map[string]any) *Tester {
return &Tester{client: tt.client.WithJSON(map[string]any{
"query": q,
"variables": variables,
})}
tt.query = q
tt.variables = variables
return tt
}
Loading