From e8be948beea117b08ade68111fc1ccc6a541258f Mon Sep 17 00:00:00 2001 From: Ken'ichiro Oyama Date: Mon, 15 Dec 2025 17:59:39 +0900 Subject: [PATCH 1/3] feat: support query via GET --- README.md | 53 ++++++++++++++++++++++++++- testdata/gqlgen-todos/server_test.go | 55 ++++++++++++++++++++++++++++ tester.go | 48 ++++++++++++++++++++++-- tester_auth.go | 9 ++++- tester_header.go | 8 +++- tester_query.go | 52 +++++++++++++++++++------- 6 files changed, 203 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 8b86282..0caefe1 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,57 @@ 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/). + +```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.Test(t). + QueryViaGet(`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 `QueryViaGetWithVariables()`: + +```go +checker.Test(t). + QueryViaGetWithVariables( + `query GetUser($id: ID!) { user(id: $id) { name } }`, + map[string]any{"id": "123"}, + ). + Check(). + HasStatusOK() +``` + --- -MIT \ No newline at end of file +MIT diff --git a/testdata/gqlgen-todos/server_test.go b/testdata/gqlgen-todos/server_test.go index 46f193d..d8e3f5f 100644 --- a/testdata/gqlgen-todos/server_test.go +++ b/testdata/gqlgen-todos/server_test.go @@ -30,3 +30,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.Test(t). + QueryViaGet(`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.Test(t). + QueryViaGetWithVariables( + `query { todos { text } }`, + map[string]any{}, + ). + Check(). + HasStatusOK(). + HasNoErrors() +} diff --git a/tester.go b/tester.go index 9147dc7..7c4bbcd 100644 --- a/tester.go +++ b/tester.go @@ -1,7 +1,9 @@ package gqlcheck import ( + "encoding/json" "net/http" + "net/url" "github.com/ikawaha/httpcheck" ) @@ -14,19 +16,59 @@ 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. 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), } } // 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, _ := json.Marshal(tt.variables) + 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()} } diff --git a/tester_auth.go b/tester_auth.go index 88cb572..1027100 100644 --- a/tester_auth.go +++ b/tester_auth.go @@ -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 } diff --git a/tester_header.go b/tester_header.go index 6004c3e..4bd7f8f 100644 --- a/tester_header.go +++ b/tester_header.go @@ -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 } diff --git a/tester_query.go b/tester_query.go index b5e914f..0bdb1b6 100644 --- a/tester_query.go +++ b/tester_query.go @@ -2,6 +2,7 @@ package gqlcheck import ( "encoding/json" + "net/http" ) // Query is a struct to represent a query. @@ -16,25 +17,48 @@ func (q Query) String() string { return string(b) } -// Request sets the query and variables to the request. +// Request sets the query and variables to the request (POST). func (tt *Tester) Request(q Query) *Tester { - return &Tester{client: tt.client.WithJSON(map[string]any{ - "query": q.Query, - "variables": q.Variables, - })} + tt.method = http.MethodPost + tt.query = q.Query + tt.variables = q.Variables + return tt } -// Query sets the query to the request. +// Query sets the query to the request (POST). func (tt *Tester) Query(q string) *Tester { - return &Tester{client: tt.client.WithJSON(map[string]any{ - "query": q, - })} + tt.method = http.MethodPost + tt.query = q + return tt } -// QueryWithVariables sets the query and variables to the request. +// QueryWithVariables sets the query and variables to the request (POST). 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.method = http.MethodPost + tt.query = q + tt.variables = variables + return tt +} + +// RequestViaGet sets the query and variables to the request (GET). +func (tt *Tester) RequestViaGet(q Query) *Tester { + tt.method = http.MethodGet + tt.query = q.Query + tt.variables = q.Variables + return tt +} + +// QueryViaGet sets the query to the request (GET). +func (tt *Tester) QueryViaGet(q string) *Tester { + tt.method = http.MethodGet + tt.query = q + return tt +} + +// QueryViaGetWithVariables sets the query and variables to the request (GET). +func (tt *Tester) QueryViaGetWithVariables(q string, variables map[string]any) *Tester { + tt.method = http.MethodGet + tt.query = q + tt.variables = variables + return tt } From eec467cba8960ab08710faa9c6eff0f1f9d48b06 Mon Sep 17 00:00:00 2001 From: Ken'ichiro Oyama Date: Mon, 15 Dec 2025 18:08:43 +0900 Subject: [PATCH 2/3] chore: fix lint warn --- tester.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tester.go b/tester.go index 7c4bbcd..edefe15 100644 --- a/tester.go +++ b/tester.go @@ -49,7 +49,11 @@ func (tt *Tester) Check() *Tester { params := url.Values{} params.Set("query", tt.query) if tt.variables != nil { - v, _ := json.Marshal(tt.variables) + 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() From d942eed96bcd6ac19eb6450bdda64d8db2bc8c3a Mon Sep 17 00:00:00 2001 From: Ken'ichiro Oyama Date: Mon, 15 Dec 2025 21:32:55 +0900 Subject: [PATCH 3/3] refactor: replace QueryViaGet methods with TestWithMethod Instead of separate methods like `QueryViaGet()` and `QueryViaGetWithVariables()`, introduce `TestWithMethod(t, method)` to specify the HTTP method at test start. This simplifies the API: - Before: `checker.Test(t).QueryViaGet(q)` - After: `checker.TestWithMethod(t, http.MethodGet).Query(q)` --- README.md | 12 +++++----- testdata/gqlgen-todos/server_test.go | 9 ++++---- tester.go | 11 ++++++++++ tester_query.go | 33 +++------------------------- 4 files changed, 26 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 0caefe1..3ddbb86 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,8 @@ PASS 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( @@ -62,8 +64,8 @@ func TestServerViaGet(t *testing.T) { h.AddTransport(transport.GET{}) checker := gqlcheck.New(h, gqlcheck.Debug()) - checker.Test(t). - QueryViaGet(`query {todos {text}}`). + checker.TestWithMethod(t, http.MethodGet). + Query(`query {todos {text}}`). Check(). HasStatusOK(). HasNoErrors(). @@ -85,11 +87,11 @@ OUTPUT: PASS ``` -You can also pass variables using `QueryViaGetWithVariables()`: +You can also pass variables using `QueryWithVariables()`: ```go -checker.Test(t). - QueryViaGetWithVariables( +checker.TestWithMethod(t, http.MethodGet). + QueryWithVariables( `query GetUser($id: ID!) { user(id: $id) { name } }`, map[string]any{"id": "123"}, ). diff --git a/testdata/gqlgen-todos/server_test.go b/testdata/gqlgen-todos/server_test.go index d8e3f5f..8b72034 100644 --- a/testdata/gqlgen-todos/server_test.go +++ b/testdata/gqlgen-todos/server_test.go @@ -1,6 +1,7 @@ package main import ( + "net/http" "testing" "github.com/99designs/gqlgen/graphql/handler" @@ -42,8 +43,8 @@ func TestServerViaGet(t *testing.T) { h.AddTransport(transport.GET{}) checker := gqlcheck.New(h, gqlcheck.Debug()) - checker.Test(t). - QueryViaGet(`query {todos {text}}`). + checker.TestWithMethod(t, http.MethodGet). + Query(`query {todos {text}}`). Check(). HasStatusOK(). HasNoErrors(). @@ -76,8 +77,8 @@ func TestServerViaGetWithVariables(t *testing.T) { HasNoErrors() // Query via GET (variables passed to demonstrate the feature) - checker.Test(t). - QueryViaGetWithVariables( + checker.TestWithMethod(t, http.MethodGet). + QueryWithVariables( `query { todos { text } }`, map[string]any{}, ). diff --git a/tester.go b/tester.go index edefe15..8af07de 100644 --- a/tester.go +++ b/tester.go @@ -29,6 +29,7 @@ type Tester struct { } // 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{ checker: c, @@ -38,6 +39,16 @@ func (c *Checker) Test(t TestingT) *Tester { } } +// 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 { diff --git a/tester_query.go b/tester_query.go index 0bdb1b6..86e9248 100644 --- a/tester_query.go +++ b/tester_query.go @@ -2,7 +2,6 @@ package gqlcheck import ( "encoding/json" - "net/http" ) // Query is a struct to represent a query. @@ -17,47 +16,21 @@ func (q Query) String() string { return string(b) } -// Request sets the query and variables to the request (POST). +// Request sets the query and variables to the request. func (tt *Tester) Request(q Query) *Tester { - tt.method = http.MethodPost tt.query = q.Query tt.variables = q.Variables return tt } -// Query sets the query to the request (POST). +// Query sets the query to the request. func (tt *Tester) Query(q string) *Tester { - tt.method = http.MethodPost tt.query = q return tt } -// QueryWithVariables sets the query and variables to the request (POST). +// QueryWithVariables sets the query and variables to the request. func (tt *Tester) QueryWithVariables(q string, variables map[string]any) *Tester { - tt.method = http.MethodPost - tt.query = q - tt.variables = variables - return tt -} - -// RequestViaGet sets the query and variables to the request (GET). -func (tt *Tester) RequestViaGet(q Query) *Tester { - tt.method = http.MethodGet - tt.query = q.Query - tt.variables = q.Variables - return tt -} - -// QueryViaGet sets the query to the request (GET). -func (tt *Tester) QueryViaGet(q string) *Tester { - tt.method = http.MethodGet - tt.query = q - return tt -} - -// QueryViaGetWithVariables sets the query and variables to the request (GET). -func (tt *Tester) QueryViaGetWithVariables(q string, variables map[string]any) *Tester { - tt.method = http.MethodGet tt.query = q tt.variables = variables return tt