diff --git a/README.md b/README.md index 8b86282..3ddbb86 100644 --- a/README.md +++ b/README.md @@ -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 \ 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..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" @@ -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() +} diff --git a/tester.go b/tester.go index 9147dc7..8af07de 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,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()} } 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..86e9248 100644 --- a/tester_query.go +++ b/tester_query.go @@ -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 }