From 89f9f4166d67b5d551ff82ed3a00dd1cc7ef779e Mon Sep 17 00:00:00 2001 From: Chris Quigley Date: Thu, 5 Dec 2024 18:09:58 -0500 Subject: [PATCH] fix: Add support for operationName and variables in HTTP GET (#3292) ## Relevant issue(s) Resolves #3153 ## Description I have made it so that operationName and variables are supported by HTTP GET requests. I did this by modifying the `ExecRequest` function inside `http/handler_store.go` such that `operationName` and `variables` parameters are extracted if they are present. I added two new tests to `http/handler_store_test.go` which test this functionality. See: `TestExecRequest_WithValidQuery_HttpGet_WithOperationName_OmitsErrors` and `TestExecRequest_HttpGet_WithVariables_OmitsErrors` ## Tasks - [x] I made sure the code is well commented, particularly hard-to-understand areas. - [x] I made sure the repository-held documentation is changed accordingly. - [x] I made sure the pull request title adheres to the conventional commit style (the subset used in the project can be found in [tools/configs/chglog/config.yml](tools/configs/chglog/config.yml)). ## How has this been tested? The platform(s) on which this was tested: - Windows --- http/handler_ccip_test.go | 10 +++- http/handler_store.go | 15 +++++- http/handler_store_test.go | 98 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 3 deletions(-) diff --git a/http/handler_ccip_test.go b/http/handler_ccip_test.go index 43797b622d..1888effe37 100644 --- a/http/handler_ccip_test.go +++ b/http/handler_ccip_test.go @@ -67,7 +67,7 @@ func TestCCIPGet_WithValidData(t *testing.T) { resHex, err := hex.DecodeString(strings.TrimPrefix(ccipRes.Data, "0x")) require.NoError(t, err) - assert.JSONEq(t, `{"data": {"User": [{"name": "bob"}]}}`, string(resHex)) + assert.JSONEq(t, `{"data": {"User": [{"name": "bob"}, {"name": "adam"}]}}`, string(resHex)) } func TestCCIPGet_WithSubscription(t *testing.T) { @@ -153,7 +153,7 @@ func TestCCIPPost_WithValidData(t *testing.T) { resHex, err := hex.DecodeString(strings.TrimPrefix(ccipRes.Data, "0x")) require.NoError(t, err) - assert.JSONEq(t, `{"data": {"User": [{"name": "bob"}]}}`, string(resHex)) + assert.JSONEq(t, `{"data": {"User": [{"name": "bob"}, {"name": "adam"}]}}`, string(resHex)) } func TestCCIPPost_WithInvalidGraphQLRequest(t *testing.T) { @@ -210,5 +210,11 @@ func setupDatabase(t *testing.T) client.DB { err = col.Create(ctx, doc) require.NoError(t, err) + doc2, err := client.NewDocFromJSON([]byte(`{"name": "adam"}`), col.Definition()) + require.NoError(t, err) + + err = col.Create(ctx, doc2) + require.NoError(t, err) + return cdb } diff --git a/http/handler_store.go b/http/handler_store.go index 35436f3762..3d2cef63de 100644 --- a/http/handler_store.go +++ b/http/handler_store.go @@ -284,7 +284,21 @@ func (s *storeHandler) ExecRequest(rw http.ResponseWriter, req *http.Request) { var request GraphQLRequest switch { case req.URL.Query().Get("query") != "": + request.Query = req.URL.Query().Get("query") + + request.OperationName = req.URL.Query().Get("operationName") + + variablesFromQuery := req.URL.Query().Get("variables") + if variablesFromQuery != "" { + var variables map[string]any + if err := json.Unmarshal([]byte(variablesFromQuery), &variables); err != nil { + responseJSON(rw, http.StatusBadRequest, errorResponse{err}) + return + } + request.Variables = variables + } + case req.Body != nil: if err := requestJSON(req, &request); err != nil { responseJSON(rw, http.StatusBadRequest, errorResponse{err}) @@ -294,7 +308,6 @@ func (s *storeHandler) ExecRequest(rw http.ResponseWriter, req *http.Request) { responseJSON(rw, http.StatusBadRequest, errorResponse{ErrMissingRequest}) return } - var options []client.RequestOption if request.OperationName != "" { options = append(options, client.WithOperationName(request.OperationName)) diff --git a/http/handler_store_test.go b/http/handler_store_test.go index dabf9648bd..7d39c4be07 100644 --- a/http/handler_store_test.go +++ b/http/handler_store_test.go @@ -16,6 +16,7 @@ import ( "io" "net/http" "net/http/httptest" + "net/url" "testing" "github.com/stretchr/testify/assert" @@ -93,3 +94,100 @@ func TestExecRequest_WithInvalidQuery_HasSpecCompliantErrors(t *testing.T) { "message": "Cannot query field \"invalid\" on type \"User\".", }}) } + +func TestExecRequest_HttpGet_WithOperationName(t *testing.T) { + cdb := setupDatabase(t) + + query := ` + query UserQuery { + User { + name + } + } + query UserQueryWithDocID { + User { + _docID + name + } + } + ` + operationName := "UserQuery" + + encodedQuery := url.QueryEscape(query) + encodedOperationName := url.QueryEscape(operationName) + + endpointURL := "http://localhost:9181/api/v0/graphql?query=" + encodedQuery + "&operationName=" + encodedOperationName + + req := httptest.NewRequest(http.MethodGet, endpointURL, nil) + rec := httptest.NewRecorder() + + handler, err := NewHandler(cdb) + require.NoError(t, err) + handler.ServeHTTP(rec, req) + + res := rec.Result() + require.NotNil(t, res.Body) + + resData, err := io.ReadAll(res.Body) + require.NoError(t, err) + + var gqlResponse map[string]any + err = json.Unmarshal(resData, &gqlResponse) + require.NoError(t, err) + + // Ensure the response data contains names, but not the _docID field + expectedJSON := `{ + "data": { + "User": [ + {"name": "bob"}, + {"name": "adam"} + ] + } + }` + assert.JSONEq(t, expectedJSON, string(resData)) +} + +func TestExecRequest_HttpGet_WithVariables(t *testing.T) { + cdb := setupDatabase(t) + + query := `query getUser($filter: UserFilterArg) { + User(filter: $filter) { + name + } + }` + operationName := "getUser" + variables := `{"filter":{"name":{"_eq":"bob"}}}` + + encodedQuery := url.QueryEscape(query) + encodedOperationName := url.QueryEscape(operationName) + encodedVariables := url.QueryEscape(variables) + + endpointURL := "http://localhost:9181/api/v0/graphql?query=" + encodedQuery + "&operationName=" + encodedOperationName + "&variables=" + encodedVariables + + req := httptest.NewRequest(http.MethodGet, endpointURL, nil) + rec := httptest.NewRecorder() + + handler, err := NewHandler(cdb) + require.NoError(t, err) + handler.ServeHTTP(rec, req) + + res := rec.Result() + require.NotNil(t, res.Body) + + resData, err := io.ReadAll(res.Body) + require.NoError(t, err) + + var gqlResponse map[string]any + err = json.Unmarshal(resData, &gqlResponse) + require.NoError(t, err) + + // Ensure only bob is returned, because of the filter variable + expectedJSON := `{ + "data": { + "User": [ + {"name": "bob"} + ] + } + }` + assert.JSONEq(t, expectedJSON, string(resData)) +}