├── go.mod ├── go.sum ├── .gitignore ├── .circleci └── config.yml ├── LICENSE ├── README.md ├── graphiql_test.go ├── graphcoolPlayground_test.go ├── graphcoolPlayground.go ├── graphiql.go ├── handler.go ├── handler_test.go └── request_options_test.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/graphql-go/handler 2 | 3 | go 1.14 4 | 5 | require github.com/graphql-go/graphql v0.8.1 // indirect 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/graphql-go/graphql v0.8.1 h1:p7/Ou/WpmulocJeEx7wjQy611rtXGQaAcXGqanuMMgc= 2 | github.com/graphql-go/graphql v0.8.1/go.mod h1:nKiHzRM0qopJEwCITUuIsxk9PlVlwIiiI8pnJEhordQ= 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | *.swp 26 | .idea 27 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | defaults: &defaults 2 | working_directory: /go/src/github.com/graphql-go/handler 3 | steps: 4 | - checkout 5 | - run: go get -v -t -d ./... 6 | - run: go test ./... 7 | 8 | version: 2 9 | jobs: 10 | golang:1.8.7: 11 | <<: *defaults 12 | docker: 13 | - image: circleci/golang:1.8.7 14 | golang:1.9.7: 15 | <<: *defaults 16 | docker: 17 | - image: circleci/golang:1.9.7 18 | golang:latest: 19 | <<: *defaults 20 | docker: 21 | - image: circleci/golang:latest 22 | coveralls: 23 | working_directory: /go/src/github.com/graphql-go/handler 24 | docker: 25 | - image: circleci/golang:latest 26 | steps: 27 | - checkout 28 | - run: go get -v -t -d ./... 29 | - run: go get github.com/mattn/goveralls 30 | - run: go test -v -cover -race -coverprofile=coverage.out 31 | - run: /go/bin/goveralls -coverprofile=coverage.out -service=circle-ci -repotoken $COVERALLS_TOKEN 32 | 33 | workflows: 34 | version: 2 35 | build: 36 | jobs: 37 | - golang:1.8.7 38 | - golang:1.9.7 39 | - golang:latest 40 | - coveralls 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Hafiz Ismail 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-go-handler [![CircleCI](https://circleci.com/gh/graphql-go/handler.svg?style=svg)](https://circleci.com/gh/graphql-go/handler) [![GoDoc](https://godoc.org/graphql-go/handler?status.svg)](https://godoc.org/github.com/graphql-go/handler) [![Coverage Status](https://coveralls.io/repos/graphql-go/handler/badge.svg?branch=master&service=github)](https://coveralls.io/github/graphql-go/handler?branch=master) [![Join the chat at https://gitter.im/graphql-go/graphql](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/graphql-go/graphql?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 2 | 3 | 4 | Golang HTTP.Handler for [graphl-go](https://github.com/graphql-go/graphql) 5 | 6 | ### Usage 7 | 8 | ```go 9 | package main 10 | 11 | import ( 12 | "net/http" 13 | "github.com/graphql-go/handler" 14 | ) 15 | 16 | func main() { 17 | schema, _ := graphql.NewSchema(...) 18 | 19 | h := handler.New(&handler.Config{ 20 | Schema: &schema, 21 | Pretty: true, 22 | GraphiQL: true, 23 | }) 24 | 25 | http.Handle("/graphql", h) 26 | http.ListenAndServe(":8080", nil) 27 | } 28 | ``` 29 | 30 | ### Using Playground 31 | ```go 32 | h := handler.New(&handler.Config{ 33 | Schema: &schema, 34 | Pretty: true, 35 | GraphiQL: false, 36 | Playground: true, 37 | }) 38 | ``` 39 | 40 | ### Details 41 | 42 | The handler will accept requests with 43 | the parameters: 44 | 45 | * **`query`**: A string GraphQL document to be executed. 46 | 47 | * **`variables`**: The runtime values to use for any GraphQL query variables 48 | as a JSON object. 49 | 50 | * **`operationName`**: If the provided `query` contains multiple named 51 | operations, this specifies which operation should be executed. If not 52 | provided, an 400 error will be returned if the `query` contains multiple 53 | named operations. 54 | 55 | GraphQL will first look for each parameter in the URL's query-string: 56 | 57 | ``` 58 | /graphql?query=query+getUser($id:ID){user(id:$id){name}}&variables={"id":"4"} 59 | ``` 60 | 61 | If not found in the query-string, it will look in the POST request body. 62 | The `handler` will interpret it 63 | depending on the provided `Content-Type` header. 64 | 65 | * **`application/json`**: the POST body will be parsed as a JSON 66 | object of parameters. 67 | 68 | * **`application/x-www-form-urlencoded`**: this POST body will be 69 | parsed as a url-encoded string of key-value pairs. 70 | 71 | * **`application/graphql`**: The POST body will be parsed as GraphQL 72 | query string, which provides the `query` parameter. 73 | 74 | 75 | ### Examples 76 | - [golang-graphql-playground](https://github.com/graphql-go/playground) 77 | - [golang-relay-starter-kit](https://github.com/sogko/golang-relay-starter-kit) 78 | - [todomvc-relay-go](https://github.com/sogko/todomvc-relay-go) 79 | 80 | ### Test 81 | ```bash 82 | $ go get github.com/graphql-go/handler 83 | $ go build && go test ./... 84 | -------------------------------------------------------------------------------- /graphiql_test.go: -------------------------------------------------------------------------------- 1 | package handler_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/graphql-go/graphql/testutil" 10 | "github.com/graphql-go/handler" 11 | ) 12 | 13 | func TestRenderGraphiQL(t *testing.T) { 14 | cases := map[string]struct { 15 | graphiqlEnabled bool 16 | accept string 17 | url string 18 | expectedStatusCode int 19 | expectedContentType string 20 | expectedBodyContains string 21 | }{ 22 | "renders GraphiQL": { 23 | graphiqlEnabled: true, 24 | accept: "text/html", 25 | expectedStatusCode: http.StatusOK, 26 | expectedContentType: "text/html; charset=utf-8", 27 | expectedBodyContains: "", 28 | }, 29 | "doesn't render graphiQL if turned off": { 30 | graphiqlEnabled: false, 31 | accept: "text/html", 32 | expectedStatusCode: http.StatusOK, 33 | expectedContentType: "application/json; charset=utf-8", 34 | }, 35 | "doesn't render GraphiQL if Content-Type application/json is present": { 36 | graphiqlEnabled: true, 37 | accept: "application/json,text/html", 38 | expectedStatusCode: http.StatusOK, 39 | expectedContentType: "application/json; charset=utf-8", 40 | }, 41 | "doesn't render GraphiQL if Content-Type text/html is not present": { 42 | graphiqlEnabled: true, 43 | expectedStatusCode: http.StatusOK, 44 | expectedContentType: "application/json; charset=utf-8", 45 | }, 46 | "doesn't render GraphiQL if 'raw' query is present": { 47 | graphiqlEnabled: true, 48 | accept: "text/html", 49 | url: "?raw", 50 | expectedStatusCode: http.StatusOK, 51 | expectedContentType: "application/json; charset=utf-8", 52 | }, 53 | } 54 | 55 | for tcID, tc := range cases { 56 | t.Run(tcID, func(t *testing.T) { 57 | req, err := http.NewRequest(http.MethodGet, tc.url, nil) 58 | if err != nil { 59 | t.Error(err) 60 | } 61 | 62 | req.Header.Set("Accept", tc.accept) 63 | 64 | h := handler.New(&handler.Config{ 65 | Schema: &testutil.StarWarsSchema, 66 | GraphiQL: tc.graphiqlEnabled, 67 | }) 68 | 69 | rr := httptest.NewRecorder() 70 | 71 | h.ServeHTTP(rr, req) 72 | resp := rr.Result() 73 | 74 | statusCode := resp.StatusCode 75 | if statusCode != tc.expectedStatusCode { 76 | t.Fatalf("%s: wrong status code, expected %v, got %v", tcID, tc.expectedStatusCode, statusCode) 77 | } 78 | 79 | contentType := resp.Header.Get("Content-Type") 80 | if contentType != tc.expectedContentType { 81 | t.Fatalf("%s: wrong content type, expected %s, got %s", tcID, tc.expectedContentType, contentType) 82 | } 83 | 84 | body := rr.Body.String() 85 | if !strings.Contains(body, tc.expectedBodyContains) { 86 | t.Fatalf("%s: wrong body, expected %s to contain %s", tcID, body, tc.expectedBodyContains) 87 | } 88 | }) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /graphcoolPlayground_test.go: -------------------------------------------------------------------------------- 1 | package handler_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/graphql-go/graphql/testutil" 10 | "github.com/graphql-go/handler" 11 | ) 12 | 13 | func TestRenderPlayground(t *testing.T) { 14 | cases := map[string]struct { 15 | playgroundEnabled bool 16 | accept string 17 | url string 18 | expectedStatusCode int 19 | expectedContentType string 20 | expectedBodyContains string 21 | }{ 22 | "renders Playground": { 23 | playgroundEnabled: true, 24 | accept: "text/html", 25 | expectedStatusCode: http.StatusOK, 26 | expectedContentType: "text/html; charset=utf-8", 27 | expectedBodyContains: "", 28 | }, 29 | "doesn't render Playground if turned off": { 30 | playgroundEnabled: false, 31 | accept: "text/html", 32 | expectedStatusCode: http.StatusOK, 33 | expectedContentType: "application/json; charset=utf-8", 34 | }, 35 | "doesn't render Playground if Content-Type application/json is present": { 36 | playgroundEnabled: true, 37 | accept: "application/json,text/html", 38 | expectedStatusCode: http.StatusOK, 39 | expectedContentType: "application/json; charset=utf-8", 40 | }, 41 | "doesn't render Playground if Content-Type text/html is not present": { 42 | playgroundEnabled: true, 43 | expectedStatusCode: http.StatusOK, 44 | expectedContentType: "application/json; charset=utf-8", 45 | }, 46 | "doesn't render Playground if 'raw' query is present": { 47 | playgroundEnabled: true, 48 | accept: "text/html", 49 | url: "?raw", 50 | expectedStatusCode: http.StatusOK, 51 | expectedContentType: "application/json; charset=utf-8", 52 | }, 53 | } 54 | 55 | for tcID, tc := range cases { 56 | t.Run(tcID, func(t *testing.T) { 57 | req, err := http.NewRequest(http.MethodGet, tc.url, nil) 58 | if err != nil { 59 | t.Error(err) 60 | } 61 | 62 | req.Header.Set("Accept", tc.accept) 63 | 64 | h := handler.New(&handler.Config{ 65 | Schema: &testutil.StarWarsSchema, 66 | GraphiQL: false, 67 | Playground: tc.playgroundEnabled, 68 | }) 69 | 70 | rr := httptest.NewRecorder() 71 | 72 | h.ServeHTTP(rr, req) 73 | resp := rr.Result() 74 | 75 | statusCode := resp.StatusCode 76 | if statusCode != tc.expectedStatusCode { 77 | t.Fatalf("%s: wrong status code, expected %v, got %v", tcID, tc.expectedStatusCode, statusCode) 78 | } 79 | 80 | contentType := resp.Header.Get("Content-Type") 81 | if contentType != tc.expectedContentType { 82 | t.Fatalf("%s: wrong content type, expected %s, got %s", tcID, tc.expectedContentType, contentType) 83 | } 84 | 85 | body := rr.Body.String() 86 | if !strings.Contains(body, tc.expectedBodyContains) { 87 | t.Fatalf("%s: wrong body, expected %s to contain %s", tcID, body, tc.expectedBodyContains) 88 | } 89 | }) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /graphcoolPlayground.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "html/template" 5 | "net/http" 6 | ) 7 | 8 | type playgroundData struct { 9 | PlaygroundVersion string 10 | Endpoint string 11 | SubscriptionEndpoint string 12 | SetTitle bool 13 | } 14 | 15 | // renderPlayground renders the Playground GUI 16 | func renderPlayground(w http.ResponseWriter, r *http.Request, endpoint string, subscriptionEndpoint string) { 17 | t := template.New("Playground") 18 | t, err := t.Parse(graphcoolPlaygroundTemplate) 19 | if err != nil { 20 | http.Error(w, err.Error(), http.StatusInternalServerError) 21 | return 22 | } 23 | 24 | d := playgroundData{ 25 | PlaygroundVersion: graphcoolPlaygroundVersion, 26 | Endpoint: endpoint, 27 | SubscriptionEndpoint: subscriptionEndpoint, 28 | SetTitle: true, 29 | } 30 | err = t.ExecuteTemplate(w, "index", d) 31 | if err != nil { 32 | http.Error(w, err.Error(), http.StatusInternalServerError) 33 | } 34 | } 35 | 36 | const graphcoolPlaygroundVersion = "1.5.2" 37 | 38 | const graphcoolPlaygroundTemplate = ` 39 | {{ define "index" }} 40 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | GraphQL Playground 55 | 56 | 57 | 58 | 59 | 60 | 61 |
62 | 89 | 90 |
Loading 91 | GraphQL Playground 92 |
93 |
94 | 102 | 103 | 104 | 105 | {{ end }} 106 | ` 107 | -------------------------------------------------------------------------------- /graphiql.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "html/template" 6 | "net/http" 7 | 8 | "github.com/graphql-go/graphql" 9 | ) 10 | 11 | // graphiqlData is the page data structure of the rendered GraphiQL page 12 | type graphiqlData struct { 13 | GraphiqlVersion string 14 | QueryString string 15 | VariablesString string 16 | OperationName string 17 | ResultString string 18 | } 19 | 20 | // renderGraphiQL renders the GraphiQL GUI 21 | func renderGraphiQL(w http.ResponseWriter, params graphql.Params) { 22 | t := template.New("GraphiQL") 23 | t, err := t.Parse(graphiqlTemplate) 24 | if err != nil { 25 | http.Error(w, err.Error(), http.StatusInternalServerError) 26 | return 27 | } 28 | 29 | // Create variables string 30 | vars, err := json.MarshalIndent(params.VariableValues, "", " ") 31 | if err != nil { 32 | http.Error(w, err.Error(), http.StatusInternalServerError) 33 | return 34 | } 35 | varsString := string(vars) 36 | if varsString == "null" { 37 | varsString = "" 38 | } 39 | 40 | // Create result string 41 | var resString string 42 | if params.RequestString == "" { 43 | resString = "" 44 | } else { 45 | result, err := json.MarshalIndent(graphql.Do(params), "", " ") 46 | if err != nil { 47 | http.Error(w, err.Error(), http.StatusInternalServerError) 48 | return 49 | } 50 | resString = string(result) 51 | } 52 | 53 | d := graphiqlData{ 54 | GraphiqlVersion: graphiqlVersion, 55 | QueryString: params.RequestString, 56 | ResultString: resString, 57 | VariablesString: varsString, 58 | OperationName: params.OperationName, 59 | } 60 | err = t.ExecuteTemplate(w, "index", d) 61 | if err != nil { 62 | http.Error(w, err.Error(), http.StatusInternalServerError) 63 | } 64 | 65 | return 66 | } 67 | 68 | // graphiqlVersion is the current version of GraphiQL 69 | const graphiqlVersion = "0.11.11" 70 | 71 | // tmpl is the page template to render GraphiQL 72 | const graphiqlTemplate = ` 73 | {{ define "index" }} 74 | 82 | 83 | 84 | 85 | 86 | GraphiQL 87 | 88 | 89 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 |
Loading...
109 | 202 | 203 | 204 | {{ end }} 205 | ` 206 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/url" 9 | "strings" 10 | 11 | "github.com/graphql-go/graphql" 12 | 13 | "context" 14 | 15 | "github.com/graphql-go/graphql/gqlerrors" 16 | ) 17 | 18 | const ( 19 | ContentTypeJSON = "application/json" 20 | ContentTypeGraphQL = "application/graphql" 21 | ContentTypeFormURLEncoded = "application/x-www-form-urlencoded" 22 | ) 23 | 24 | type ResultCallbackFn func(ctx context.Context, params *graphql.Params, result *graphql.Result, responseBody []byte) 25 | 26 | type Handler struct { 27 | Schema *graphql.Schema 28 | pretty bool 29 | graphiql bool 30 | playground bool 31 | playgroundConfig *PlaygroundConfig 32 | rootObjectFn RootObjectFn 33 | resultCallbackFn ResultCallbackFn 34 | formatErrorFn func(err error) gqlerrors.FormattedError 35 | } 36 | 37 | type RequestOptions struct { 38 | Query string `json:"query" url:"query" schema:"query"` 39 | Variables map[string]interface{} `json:"variables" url:"variables" schema:"variables"` 40 | OperationName string `json:"operationName" url:"operationName" schema:"operationName"` 41 | } 42 | 43 | // a workaround for getting`variables` as a JSON string 44 | type requestOptionsCompatibility struct { 45 | Query string `json:"query" url:"query" schema:"query"` 46 | Variables string `json:"variables" url:"variables" schema:"variables"` 47 | OperationName string `json:"operationName" url:"operationName" schema:"operationName"` 48 | } 49 | 50 | func getFromForm(values url.Values) *RequestOptions { 51 | query := values.Get("query") 52 | if query != "" { 53 | // get variables map 54 | variables := make(map[string]interface{}, len(values)) 55 | variablesStr := values.Get("variables") 56 | json.Unmarshal([]byte(variablesStr), &variables) 57 | 58 | return &RequestOptions{ 59 | Query: query, 60 | Variables: variables, 61 | OperationName: values.Get("operationName"), 62 | } 63 | } 64 | 65 | return nil 66 | } 67 | 68 | // RequestOptions Parses a http.Request into GraphQL request options struct 69 | func NewRequestOptions(r *http.Request) *RequestOptions { 70 | if reqOpt := getFromForm(r.URL.Query()); reqOpt != nil { 71 | return reqOpt 72 | } 73 | 74 | if r.Method != http.MethodPost { 75 | return &RequestOptions{} 76 | } 77 | 78 | if r.Body == nil { 79 | return &RequestOptions{} 80 | } 81 | 82 | // TODO: improve Content-Type handling 83 | contentTypeStr := r.Header.Get("Content-Type") 84 | contentTypeTokens := strings.Split(contentTypeStr, ";") 85 | contentType := contentTypeTokens[0] 86 | 87 | switch contentType { 88 | case ContentTypeGraphQL: 89 | body, err := ioutil.ReadAll(r.Body) 90 | if err != nil { 91 | return &RequestOptions{} 92 | } 93 | return &RequestOptions{ 94 | Query: string(body), 95 | } 96 | case ContentTypeFormURLEncoded: 97 | if err := r.ParseForm(); err != nil { 98 | return &RequestOptions{} 99 | } 100 | 101 | if reqOpt := getFromForm(r.PostForm); reqOpt != nil { 102 | return reqOpt 103 | } 104 | 105 | return &RequestOptions{} 106 | 107 | case ContentTypeJSON: 108 | fallthrough 109 | default: 110 | var opts RequestOptions 111 | body, err := ioutil.ReadAll(r.Body) 112 | if err != nil { 113 | return &opts 114 | } 115 | err = json.Unmarshal(body, &opts) 116 | if err != nil { 117 | // Probably `variables` was sent as a string instead of an object. 118 | // So, we try to be polite and try to parse that as a JSON string 119 | var optsCompatible requestOptionsCompatibility 120 | json.Unmarshal(body, &optsCompatible) 121 | json.Unmarshal([]byte(optsCompatible.Variables), &opts.Variables) 122 | } 123 | return &opts 124 | } 125 | } 126 | 127 | // ContextHandler provides an entrypoint into executing graphQL queries with a 128 | // user-provided context. 129 | func (h *Handler) ContextHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) { 130 | // get query 131 | opts := NewRequestOptions(r) 132 | 133 | // execute graphql query 134 | params := graphql.Params{ 135 | Schema: *h.Schema, 136 | RequestString: opts.Query, 137 | VariableValues: opts.Variables, 138 | OperationName: opts.OperationName, 139 | Context: ctx, 140 | } 141 | if h.rootObjectFn != nil { 142 | params.RootObject = h.rootObjectFn(ctx, r) 143 | } 144 | result := graphql.Do(params) 145 | 146 | if formatErrorFn := h.formatErrorFn; formatErrorFn != nil && len(result.Errors) > 0 { 147 | formatted := make([]gqlerrors.FormattedError, len(result.Errors)) 148 | for i, formattedError := range result.Errors { 149 | formatted[i] = formatErrorFn(formattedError.OriginalError()) 150 | } 151 | result.Errors = formatted 152 | } 153 | 154 | if h.graphiql { 155 | acceptHeader := r.Header.Get("Accept") 156 | _, raw := r.URL.Query()["raw"] 157 | if !raw && !strings.Contains(acceptHeader, "application/json") && strings.Contains(acceptHeader, "text/html") { 158 | renderGraphiQL(w, params) 159 | return 160 | } 161 | } 162 | 163 | if h.playground { 164 | acceptHeader := r.Header.Get("Accept") 165 | _, raw := r.URL.Query()["raw"] 166 | if !raw && !strings.Contains(acceptHeader, "application/json") && strings.Contains(acceptHeader, "text/html") { 167 | 168 | endpoint := r.URL.Path 169 | subscriptionEndpoint := fmt.Sprintf("ws://%v/subscriptions", r.Host) 170 | if h.playgroundConfig != nil { 171 | endpoint = h.playgroundConfig.Endpoint 172 | subscriptionEndpoint = h.playgroundConfig.SubscriptionEndpoint 173 | } 174 | 175 | renderPlayground(w, r, endpoint, subscriptionEndpoint) 176 | return 177 | } 178 | } 179 | 180 | // use proper JSON Header 181 | w.Header().Add("Content-Type", "application/json; charset=utf-8") 182 | 183 | var buff []byte 184 | if h.pretty { 185 | w.WriteHeader(http.StatusOK) 186 | buff, _ = json.MarshalIndent(result, "", "\t") 187 | 188 | w.Write(buff) 189 | } else { 190 | w.WriteHeader(http.StatusOK) 191 | buff, _ = json.Marshal(result) 192 | 193 | w.Write(buff) 194 | } 195 | 196 | if h.resultCallbackFn != nil { 197 | h.resultCallbackFn(ctx, ¶ms, result, buff) 198 | } 199 | } 200 | 201 | // ServeHTTP provides an entrypoint into executing graphQL queries. 202 | func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 203 | h.ContextHandler(r.Context(), w, r) 204 | } 205 | 206 | // RootObjectFn allows a user to generate a RootObject per request 207 | type RootObjectFn func(ctx context.Context, r *http.Request) map[string]interface{} 208 | 209 | type PlaygroundConfig struct { 210 | Endpoint string 211 | SubscriptionEndpoint string 212 | } 213 | 214 | type Config struct { 215 | Schema *graphql.Schema 216 | Pretty bool 217 | GraphiQL bool 218 | Playground bool 219 | PlaygroundConfig *PlaygroundConfig 220 | RootObjectFn RootObjectFn 221 | ResultCallbackFn ResultCallbackFn 222 | FormatErrorFn func(err error) gqlerrors.FormattedError 223 | } 224 | 225 | func NewConfig() *Config { 226 | return &Config{ 227 | Schema: nil, 228 | Pretty: true, 229 | GraphiQL: true, 230 | Playground: false, 231 | PlaygroundConfig: nil, 232 | } 233 | } 234 | 235 | func New(p *Config) *Handler { 236 | if p == nil { 237 | p = NewConfig() 238 | } 239 | 240 | if p.Schema == nil { 241 | panic("undefined GraphQL schema") 242 | } 243 | 244 | return &Handler{ 245 | Schema: p.Schema, 246 | pretty: p.Pretty, 247 | graphiql: p.GraphiQL, 248 | playground: p.Playground, 249 | playgroundConfig: p.PlaygroundConfig, 250 | rootObjectFn: p.RootObjectFn, 251 | resultCallbackFn: p.ResultCallbackFn, 252 | formatErrorFn: p.FormatErrorFn, 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /handler_test.go: -------------------------------------------------------------------------------- 1 | package handler_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/httptest" 9 | "reflect" 10 | "strings" 11 | "testing" 12 | 13 | "context" 14 | 15 | "github.com/graphql-go/graphql" 16 | "github.com/graphql-go/graphql/gqlerrors" 17 | "github.com/graphql-go/graphql/language/location" 18 | "github.com/graphql-go/graphql/testutil" 19 | "github.com/graphql-go/handler" 20 | ) 21 | 22 | func decodeResponse(t *testing.T, recorder *httptest.ResponseRecorder) *graphql.Result { 23 | // clone request body reader so that we can have a nicer error message 24 | bodyString := "" 25 | var target graphql.Result 26 | if b, err := ioutil.ReadAll(recorder.Body); err == nil { 27 | bodyString = string(b) 28 | } 29 | readerClone := strings.NewReader(bodyString) 30 | 31 | decoder := json.NewDecoder(readerClone) 32 | err := decoder.Decode(&target) 33 | if err != nil { 34 | t.Fatalf("DecodeResponseToType(): %v \n%v", err.Error(), bodyString) 35 | } 36 | return &target 37 | } 38 | func executeTest(t *testing.T, h *handler.Handler, req *http.Request) (*graphql.Result, *httptest.ResponseRecorder) { 39 | resp := httptest.NewRecorder() 40 | h.ServeHTTP(resp, req) 41 | result := decodeResponse(t, resp) 42 | return result, resp 43 | } 44 | 45 | func TestContextPropagated(t *testing.T) { 46 | myNameQuery := graphql.NewObject(graphql.ObjectConfig{ 47 | Name: "Query", 48 | Fields: graphql.Fields{ 49 | "name": &graphql.Field{ 50 | Name: "name", 51 | Type: graphql.String, 52 | Resolve: func(p graphql.ResolveParams) (interface{}, error) { 53 | return p.Context.Value("name"), nil 54 | }, 55 | }, 56 | }, 57 | }) 58 | myNameSchema, err := graphql.NewSchema(graphql.SchemaConfig{ 59 | Query: myNameQuery, 60 | }) 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | 65 | expected := &graphql.Result{ 66 | Data: map[string]interface{}{ 67 | "name": "context-data", 68 | }, 69 | } 70 | queryString := `query={name}` 71 | req, _ := http.NewRequest("GET", fmt.Sprintf("/graphql?%v", queryString), nil) 72 | 73 | h := handler.New(&handler.Config{ 74 | Schema: &myNameSchema, 75 | Pretty: true, 76 | }) 77 | 78 | ctx := context.WithValue(context.Background(), "name", "context-data") 79 | resp := httptest.NewRecorder() 80 | h.ContextHandler(ctx, resp, req) 81 | result := decodeResponse(t, resp) 82 | if resp.Code != http.StatusOK { 83 | t.Fatalf("unexpected server response %v", resp.Code) 84 | } 85 | if !reflect.DeepEqual(result, expected) { 86 | t.Fatalf("wrong result, graphql result diff: %v", testutil.Diff(expected, result)) 87 | } 88 | } 89 | 90 | func TestHandler_BasicQuery_Pretty(t *testing.T) { 91 | expected := &graphql.Result{ 92 | Data: map[string]interface{}{ 93 | "hero": map[string]interface{}{ 94 | "name": "R2-D2", 95 | }, 96 | }, 97 | } 98 | queryString := `query=query HeroNameQuery { hero { name } }&operationName=HeroNameQuery` 99 | req, _ := http.NewRequest("GET", fmt.Sprintf("/graphql?%v", queryString), nil) 100 | 101 | callbackCalled := false 102 | h := handler.New(&handler.Config{ 103 | Schema: &testutil.StarWarsSchema, 104 | Pretty: true, 105 | ResultCallbackFn: func(ctx context.Context, params *graphql.Params, result *graphql.Result, responseBody []byte) { 106 | callbackCalled = true 107 | if params.OperationName != "HeroNameQuery" { 108 | t.Fatalf("OperationName passed to callback was not HeroNameQuery: %v", params.OperationName) 109 | } 110 | 111 | if result.HasErrors() { 112 | t.Fatalf("unexpected graphql result errors") 113 | } 114 | }, 115 | }) 116 | result, resp := executeTest(t, h, req) 117 | if resp.Code != http.StatusOK { 118 | t.Fatalf("unexpected server response %v", resp.Code) 119 | } 120 | if !reflect.DeepEqual(result, expected) { 121 | t.Fatalf("wrong result, graphql result diff: %v", testutil.Diff(expected, result)) 122 | } 123 | if !callbackCalled { 124 | t.Fatalf("ResultCallbackFn was not called when it should have been") 125 | } 126 | } 127 | 128 | func TestHandler_BasicQuery_Ugly(t *testing.T) { 129 | expected := &graphql.Result{ 130 | Data: map[string]interface{}{ 131 | "hero": map[string]interface{}{ 132 | "name": "R2-D2", 133 | }, 134 | }, 135 | } 136 | queryString := `query=query HeroNameQuery { hero { name } }` 137 | req, _ := http.NewRequest("GET", fmt.Sprintf("/graphql?%v", queryString), nil) 138 | 139 | h := handler.New(&handler.Config{ 140 | Schema: &testutil.StarWarsSchema, 141 | Pretty: false, 142 | }) 143 | result, resp := executeTest(t, h, req) 144 | if resp.Code != http.StatusOK { 145 | t.Fatalf("unexpected server response %v", resp.Code) 146 | } 147 | if !reflect.DeepEqual(result, expected) { 148 | t.Fatalf("wrong result, graphql result diff: %v", testutil.Diff(expected, result)) 149 | } 150 | } 151 | 152 | func TestHandler_Params_NilParams(t *testing.T) { 153 | defer func() { 154 | if r := recover(); r != nil { 155 | if str, ok := r.(string); ok { 156 | if str != "undefined GraphQL schema" { 157 | t.Fatalf("unexpected error, got %v", r) 158 | } 159 | // test passed 160 | return 161 | } 162 | t.Fatalf("unexpected error, got %v", r) 163 | 164 | } 165 | t.Fatalf("expected to panic, did not panic") 166 | }() 167 | _ = handler.New(nil) 168 | 169 | } 170 | 171 | func TestHandler_BasicQuery_WithRootObjFn(t *testing.T) { 172 | myNameQuery := graphql.NewObject(graphql.ObjectConfig{ 173 | Name: "Query", 174 | Fields: graphql.Fields{ 175 | "name": &graphql.Field{ 176 | Name: "name", 177 | Type: graphql.String, 178 | Resolve: func(p graphql.ResolveParams) (interface{}, error) { 179 | rv := p.Info.RootValue.(map[string]interface{}) 180 | return rv["rootValue"], nil 181 | }, 182 | }, 183 | }, 184 | }) 185 | myNameSchema, err := graphql.NewSchema(graphql.SchemaConfig{ 186 | Query: myNameQuery, 187 | }) 188 | if err != nil { 189 | t.Fatal(err) 190 | } 191 | 192 | expected := &graphql.Result{ 193 | Data: map[string]interface{}{ 194 | "name": "foo", 195 | }, 196 | } 197 | queryString := `query={name}` 198 | req, _ := http.NewRequest("GET", fmt.Sprintf("/graphql?%v", queryString), nil) 199 | 200 | h := handler.New(&handler.Config{ 201 | Schema: &myNameSchema, 202 | Pretty: true, 203 | RootObjectFn: func(ctx context.Context, r *http.Request) map[string]interface{} { 204 | return map[string]interface{}{"rootValue": "foo"} 205 | }, 206 | }) 207 | result, resp := executeTest(t, h, req) 208 | if resp.Code != http.StatusOK { 209 | t.Fatalf("unexpected server response %v", resp.Code) 210 | } 211 | if !reflect.DeepEqual(result, expected) { 212 | t.Fatalf("wrong result, graphql result diff: %v", testutil.Diff(expected, result)) 213 | } 214 | } 215 | 216 | type customError struct { 217 | message string 218 | } 219 | 220 | func (e customError) Error() string { 221 | return fmt.Sprintf("%s", e.message) 222 | } 223 | 224 | func TestHandler_BasicQuery_WithFormatErrorFn(t *testing.T) { 225 | resolverError := customError{message: "resolver error"} 226 | myNameQuery := graphql.NewObject(graphql.ObjectConfig{ 227 | Name: "Query", 228 | Fields: graphql.Fields{ 229 | "name": &graphql.Field{ 230 | Name: "name", 231 | Type: graphql.String, 232 | Resolve: func(p graphql.ResolveParams) (interface{}, error) { 233 | return nil, resolverError 234 | }, 235 | }, 236 | }, 237 | }) 238 | myNameSchema, err := graphql.NewSchema(graphql.SchemaConfig{ 239 | Query: myNameQuery, 240 | }) 241 | if err != nil { 242 | t.Fatal(err) 243 | } 244 | 245 | customFormattedError := gqlerrors.FormattedError{ 246 | Message: resolverError.Error(), 247 | Locations: []location.SourceLocation{ 248 | location.SourceLocation{ 249 | Line: 1, 250 | Column: 2, 251 | }, 252 | }, 253 | Path: []interface{}{"name"}, 254 | } 255 | 256 | expected := &graphql.Result{ 257 | Data: map[string]interface{}{ 258 | "name": nil, 259 | }, 260 | Errors: []gqlerrors.FormattedError{customFormattedError}, 261 | } 262 | 263 | queryString := `query={name}` 264 | req, _ := http.NewRequest("GET", fmt.Sprintf("/graphql?%v", queryString), nil) 265 | 266 | formatErrorFnCalled := false 267 | h := handler.New(&handler.Config{ 268 | Schema: &myNameSchema, 269 | Pretty: true, 270 | FormatErrorFn: func(err error) gqlerrors.FormattedError { 271 | formatErrorFnCalled = true 272 | var formatted gqlerrors.FormattedError 273 | switch err := err.(type) { 274 | case *gqlerrors.Error: 275 | formatted = gqlerrors.FormatError(err) 276 | default: 277 | t.Fatalf("unexpected error type: %v", reflect.TypeOf(err)) 278 | } 279 | return formatted 280 | }, 281 | }) 282 | result, resp := executeTest(t, h, req) 283 | if resp.Code != http.StatusOK { 284 | t.Fatalf("unexpected server response %v", resp.Code) 285 | } 286 | if !formatErrorFnCalled { 287 | t.Fatalf("FormatErrorFn was not called when it should have been") 288 | } 289 | if !reflect.DeepEqual(result, expected) { 290 | t.Fatalf("wrong result, graphql result diff: %v", testutil.Diff(expected, result)) 291 | } 292 | } 293 | 294 | func TestPlaygroundWithDefaultConfig(t *testing.T) { 295 | query := graphql.NewObject(graphql.ObjectConfig{ 296 | Name: "Query", 297 | Fields: graphql.Fields{ 298 | "ping": &graphql.Field{ 299 | Name: "ping", 300 | Type: graphql.String, 301 | Resolve: func(p graphql.ResolveParams) (interface{}, error) { 302 | return "OK", nil 303 | }, 304 | }, 305 | }, 306 | }) 307 | 308 | schema, err := graphql.NewSchema(graphql.SchemaConfig{ 309 | Query: query, 310 | }) 311 | if err != nil { 312 | t.Fatal(err) 313 | } 314 | 315 | req, err := http.NewRequest("GET", "/graphql", nil) 316 | req.Header.Set("Accept", "text/html") 317 | if err != nil { 318 | t.Fatal(err) 319 | } 320 | 321 | h := handler.New(&handler.Config{ 322 | Schema: &schema, 323 | Playground: true, 324 | }) 325 | 326 | resp := httptest.NewRecorder() 327 | h.ContextHandler(context.Background(), resp, req) 328 | 329 | if resp.Code != http.StatusOK { 330 | t.Fatalf("unexpected server response %v", resp.Code) 331 | } 332 | 333 | expectedBodyContains := []string{ 334 | "GraphQL Playground", 335 | `endpoint: "/graphql"`, 336 | `subscriptionEndpoint: "ws:///subscriptions"`, 337 | } 338 | respBody := resp.Body.String() 339 | 340 | for _, e := range expectedBodyContains { 341 | if !strings.Contains(respBody, e) { 342 | t.Fatalf("wrong body, expected %s to contain %s", respBody, e) 343 | } 344 | } 345 | } 346 | 347 | func TestPlaygroundWithCustomConfig(t *testing.T) { 348 | query := graphql.NewObject(graphql.ObjectConfig{ 349 | Name: "Query", 350 | Fields: graphql.Fields{ 351 | "ping": &graphql.Field{ 352 | Name: "ping", 353 | Type: graphql.String, 354 | Resolve: func(p graphql.ResolveParams) (interface{}, error) { 355 | return "OK", nil 356 | }, 357 | }, 358 | }, 359 | }) 360 | 361 | schema, err := graphql.NewSchema(graphql.SchemaConfig{ 362 | Query: query, 363 | }) 364 | if err != nil { 365 | t.Fatal(err) 366 | } 367 | 368 | req, err := http.NewRequest("GET", "/custom-path/graphql", nil) 369 | req.Header.Set("Accept", "text/html") 370 | if err != nil { 371 | t.Fatal(err) 372 | } 373 | 374 | h := handler.New(&handler.Config{ 375 | Schema: &schema, 376 | Playground: true, 377 | PlaygroundConfig: &handler.PlaygroundConfig{ 378 | Endpoint: "/custom-path/graphql", 379 | SubscriptionEndpoint: "/custom-path/ws", 380 | }, 381 | }) 382 | 383 | resp := httptest.NewRecorder() 384 | h.ContextHandler(context.Background(), resp, req) 385 | 386 | if resp.Code != http.StatusOK { 387 | t.Fatalf("unexpected server response %v", resp.Code) 388 | } 389 | 390 | expectedBodyContains := []string{ 391 | "GraphQL Playground", 392 | `endpoint: "/custom-path/graphql"`, 393 | `subscriptionEndpoint: "/custom-path/ws"`, 394 | } 395 | respBody := resp.Body.String() 396 | 397 | for _, e := range expectedBodyContains { 398 | if !strings.Contains(respBody, e) { 399 | t.Fatalf("wrong body, expected %s to contain %s", respBody, e) 400 | } 401 | } 402 | } 403 | -------------------------------------------------------------------------------- /request_options_test.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "reflect" 9 | "testing" 10 | 11 | "github.com/graphql-go/graphql/testutil" 12 | ) 13 | 14 | func TestRequestOptions_GET_BasicQueryString(t *testing.T) { 15 | queryString := "query=query RebelsShipsQuery { rebels { name } }" 16 | expected := &RequestOptions{ 17 | Query: "query RebelsShipsQuery { rebels { name } }", 18 | Variables: make(map[string]interface{}), 19 | } 20 | 21 | req, _ := http.NewRequest("GET", fmt.Sprintf("/graphql?%v", queryString), nil) 22 | result := NewRequestOptions(req) 23 | 24 | if !reflect.DeepEqual(result, expected) { 25 | t.Fatalf("wrong result, graphql result diff: %v", testutil.Diff(expected, result)) 26 | } 27 | } 28 | func TestRequestOptions_GET_ContentTypeApplicationGraphQL(t *testing.T) { 29 | body := []byte(`query RebelsShipsQuery { rebels { name } }`) 30 | expected := &RequestOptions{} 31 | 32 | req, _ := http.NewRequest("GET", "/graphql", bytes.NewBuffer(body)) 33 | req.Header.Add("Content-Type", "application/graphql") 34 | result := NewRequestOptions(req) 35 | 36 | if !reflect.DeepEqual(result, expected) { 37 | t.Fatalf("wrong result, graphql result diff: %v", testutil.Diff(expected, result)) 38 | } 39 | } 40 | func TestRequestOptions_GET_ContentTypeApplicationJSON(t *testing.T) { 41 | body := ` 42 | { 43 | "query": "query RebelsShipsQuery { rebels { name } }" 44 | }` 45 | expected := &RequestOptions{} 46 | 47 | req, _ := http.NewRequest("GET", "/graphql", bytes.NewBufferString(body)) 48 | req.Header.Add("Content-Type", "application/json") 49 | result := NewRequestOptions(req) 50 | 51 | if !reflect.DeepEqual(result, expected) { 52 | t.Fatalf("wrong result, graphql result diff: %v", testutil.Diff(expected, result)) 53 | } 54 | } 55 | func TestRequestOptions_GET_ContentTypeApplicationUrlEncoded(t *testing.T) { 56 | data := url.Values{} 57 | data.Add("query", "query RebelsShipsQuery { rebels { name } }") 58 | 59 | expected := &RequestOptions{} 60 | 61 | req, _ := http.NewRequest("GET", "/graphql", bytes.NewBufferString(data.Encode())) 62 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 63 | result := NewRequestOptions(req) 64 | 65 | if !reflect.DeepEqual(result, expected) { 66 | t.Fatalf("wrong result, graphql result diff: %v", testutil.Diff(expected, result)) 67 | } 68 | } 69 | 70 | func TestRequestOptions_POST_BasicQueryString_WithNoBody(t *testing.T) { 71 | queryString := "query=query RebelsShipsQuery { rebels { name } }" 72 | expected := &RequestOptions{ 73 | Query: "query RebelsShipsQuery { rebels { name } }", 74 | Variables: make(map[string]interface{}), 75 | } 76 | 77 | req, _ := http.NewRequest("POST", fmt.Sprintf("/graphql?%v", queryString), nil) 78 | result := NewRequestOptions(req) 79 | 80 | if !reflect.DeepEqual(result, expected) { 81 | t.Fatalf("wrong result, graphql result diff: %v", testutil.Diff(expected, result)) 82 | } 83 | } 84 | func TestRequestOptions_POST_ContentTypeApplicationGraphQL(t *testing.T) { 85 | body := []byte(`query RebelsShipsQuery { rebels { name } }`) 86 | expected := &RequestOptions{ 87 | Query: "query RebelsShipsQuery { rebels { name } }", 88 | } 89 | 90 | req, _ := http.NewRequest("POST", "/graphql", bytes.NewBuffer(body)) 91 | req.Header.Add("Content-Type", "application/graphql") 92 | result := NewRequestOptions(req) 93 | 94 | if !reflect.DeepEqual(result, expected) { 95 | t.Fatalf("wrong result, graphql result diff: %v", testutil.Diff(expected, result)) 96 | } 97 | } 98 | func TestRequestOptions_POST_ContentTypeApplicationGraphQL_WithNonGraphQLQueryContent(t *testing.T) { 99 | body := []byte(`not a graphql query`) 100 | expected := &RequestOptions{ 101 | Query: "not a graphql query", 102 | } 103 | 104 | req, _ := http.NewRequest("POST", "/graphql", bytes.NewBuffer(body)) 105 | req.Header.Add("Content-Type", "application/graphql") 106 | result := NewRequestOptions(req) 107 | 108 | if !reflect.DeepEqual(result, expected) { 109 | t.Fatalf("wrong result, graphql result diff: %v", testutil.Diff(expected, result)) 110 | } 111 | } 112 | func TestRequestOptions_POST_ContentTypeApplicationGraphQL_EmptyBody(t *testing.T) { 113 | body := []byte(``) 114 | expected := &RequestOptions{ 115 | Query: "", 116 | } 117 | 118 | req, _ := http.NewRequest("POST", "/graphql", bytes.NewBuffer(body)) 119 | req.Header.Add("Content-Type", "application/graphql") 120 | result := NewRequestOptions(req) 121 | 122 | if !reflect.DeepEqual(result, expected) { 123 | t.Fatalf("wrong result, graphql result diff: %v", testutil.Diff(expected, result)) 124 | } 125 | } 126 | func TestRequestOptions_POST_ContentTypeApplicationGraphQL_NilBody(t *testing.T) { 127 | expected := &RequestOptions{} 128 | 129 | req, _ := http.NewRequest("POST", "/graphql", nil) 130 | req.Header.Add("Content-Type", "application/graphql") 131 | result := NewRequestOptions(req) 132 | 133 | if !reflect.DeepEqual(result, expected) { 134 | t.Fatalf("wrong result, graphql result diff: %v", testutil.Diff(expected, result)) 135 | } 136 | } 137 | 138 | func TestRequestOptions_POST_ContentTypeApplicationJSON(t *testing.T) { 139 | body := ` 140 | { 141 | "query": "query RebelsShipsQuery { rebels { name } }" 142 | }` 143 | expected := &RequestOptions{ 144 | Query: "query RebelsShipsQuery { rebels { name } }", 145 | } 146 | 147 | req, _ := http.NewRequest("POST", "/graphql", bytes.NewBufferString(body)) 148 | req.Header.Add("Content-Type", "application/json") 149 | result := NewRequestOptions(req) 150 | 151 | if !reflect.DeepEqual(result, expected) { 152 | t.Fatalf("wrong result, graphql result diff: %v", testutil.Diff(expected, result)) 153 | } 154 | } 155 | 156 | func TestRequestOptions_GET_WithVariablesAsObject(t *testing.T) { 157 | variables := url.QueryEscape(`{ "a": 1, "b": "2" }`) 158 | query := url.QueryEscape("query RebelsShipsQuery { rebels { name } }") 159 | queryString := fmt.Sprintf("query=%s&variables=%s", query, variables) 160 | expected := &RequestOptions{ 161 | Query: "query RebelsShipsQuery { rebels { name } }", 162 | Variables: map[string]interface{}{ 163 | "a": float64(1), 164 | "b": "2", 165 | }, 166 | } 167 | 168 | req, _ := http.NewRequest("GET", fmt.Sprintf("/graphql?%v", queryString), nil) 169 | result := NewRequestOptions(req) 170 | 171 | if !reflect.DeepEqual(result, expected) { 172 | t.Fatalf("wrong result, graphql result diff: %v", testutil.Diff(expected, result)) 173 | } 174 | } 175 | 176 | func TestRequestOptions_POST_ContentTypeApplicationJSON_WithVariablesAsObject(t *testing.T) { 177 | body := ` 178 | { 179 | "query": "query RebelsShipsQuery { rebels { name } }", 180 | "variables": { "a": 1, "b": "2" } 181 | }` 182 | expected := &RequestOptions{ 183 | Query: "query RebelsShipsQuery { rebels { name } }", 184 | Variables: map[string]interface{}{ 185 | "a": float64(1), 186 | "b": "2", 187 | }, 188 | } 189 | 190 | req, _ := http.NewRequest("POST", "/graphql", bytes.NewBufferString(body)) 191 | req.Header.Add("Content-Type", "application/json") 192 | result := NewRequestOptions(req) 193 | 194 | if !reflect.DeepEqual(result, expected) { 195 | t.Fatalf("wrong result, graphql result diff: %v", testutil.Diff(expected, result)) 196 | } 197 | } 198 | func TestRequestOptions_POST_ContentTypeApplicationJSON_WithVariablesAsString(t *testing.T) { 199 | body := ` 200 | { 201 | "query": "query RebelsShipsQuery { rebels { name } }", 202 | "variables": "{ \"a\": 1, \"b\": \"2\" }" 203 | }` 204 | expected := &RequestOptions{ 205 | Query: "query RebelsShipsQuery { rebels { name } }", 206 | Variables: map[string]interface{}{ 207 | "a": float64(1), 208 | "b": "2", 209 | }, 210 | } 211 | 212 | req, _ := http.NewRequest("POST", "/graphql", bytes.NewBufferString(body)) 213 | req.Header.Add("Content-Type", "application/json") 214 | result := NewRequestOptions(req) 215 | 216 | if !reflect.DeepEqual(result, expected) { 217 | t.Fatalf("wrong result, graphql result diff: %v", testutil.Diff(expected, result)) 218 | } 219 | } 220 | func TestRequestOptions_POST_ContentTypeApplicationJSON_WithInvalidJSON(t *testing.T) { 221 | body := `INVALIDJSON{}` 222 | expected := &RequestOptions{} 223 | 224 | req, _ := http.NewRequest("POST", "/graphql", bytes.NewBufferString(body)) 225 | req.Header.Add("Content-Type", "application/json") 226 | result := NewRequestOptions(req) 227 | 228 | if !reflect.DeepEqual(result, expected) { 229 | t.Fatalf("wrong result, graphql result diff: %v", testutil.Diff(expected, result)) 230 | } 231 | } 232 | func TestRequestOptions_POST_ContentTypeApplicationJSON_WithNilBody(t *testing.T) { 233 | expected := &RequestOptions{} 234 | 235 | req, _ := http.NewRequest("POST", "/graphql", nil) 236 | req.Header.Add("Content-Type", "application/json") 237 | result := NewRequestOptions(req) 238 | 239 | if !reflect.DeepEqual(result, expected) { 240 | t.Fatalf("wrong result, graphql result diff: %v", testutil.Diff(expected, result)) 241 | } 242 | } 243 | 244 | func TestRequestOptions_POST_ContentTypeApplicationUrlEncoded(t *testing.T) { 245 | data := url.Values{} 246 | data.Add("query", "query RebelsShipsQuery { rebels { name } }") 247 | 248 | expected := &RequestOptions{ 249 | Query: "query RebelsShipsQuery { rebels { name } }", 250 | Variables: make(map[string]interface{}), 251 | } 252 | 253 | req, _ := http.NewRequest("POST", "/graphql", bytes.NewBufferString(data.Encode())) 254 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 255 | result := NewRequestOptions(req) 256 | 257 | if !reflect.DeepEqual(result, expected) { 258 | t.Fatalf("wrong result, graphql result diff: %v", testutil.Diff(expected, result)) 259 | } 260 | } 261 | func TestRequestOptions_POST_ContentTypeApplicationUrlEncoded_WithInvalidData(t *testing.T) { 262 | data := "Invalid Data" 263 | 264 | expected := &RequestOptions{} 265 | 266 | req, _ := http.NewRequest("POST", "/graphql", bytes.NewBufferString(data)) 267 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 268 | result := NewRequestOptions(req) 269 | 270 | if !reflect.DeepEqual(result, expected) { 271 | t.Fatalf("wrong result, graphql result diff: %v", testutil.Diff(expected, result)) 272 | } 273 | } 274 | func TestRequestOptions_POST_ContentTypeApplicationUrlEncoded_WithNilBody(t *testing.T) { 275 | 276 | expected := &RequestOptions{} 277 | 278 | req, _ := http.NewRequest("POST", "/graphql", nil) 279 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 280 | result := NewRequestOptions(req) 281 | 282 | if !reflect.DeepEqual(result, expected) { 283 | t.Fatalf("wrong result, graphql result diff: %v", testutil.Diff(expected, result)) 284 | } 285 | } 286 | 287 | func TestRequestOptions_PUT_BasicQueryString(t *testing.T) { 288 | queryString := "query=query RebelsShipsQuery { rebels { name } }" 289 | expected := &RequestOptions{ 290 | Query: "query RebelsShipsQuery { rebels { name } }", 291 | Variables: make(map[string]interface{}), 292 | } 293 | 294 | req, _ := http.NewRequest("PUT", fmt.Sprintf("/graphql?%v", queryString), nil) 295 | result := NewRequestOptions(req) 296 | 297 | if !reflect.DeepEqual(result, expected) { 298 | t.Fatalf("wrong result, graphql result diff: %v", testutil.Diff(expected, result)) 299 | } 300 | } 301 | func TestRequestOptions_PUT_ContentTypeApplicationGraphQL(t *testing.T) { 302 | body := []byte(`query RebelsShipsQuery { rebels { name } }`) 303 | expected := &RequestOptions{} 304 | 305 | req, _ := http.NewRequest("PUT", "/graphql", bytes.NewBuffer(body)) 306 | req.Header.Add("Content-Type", "application/graphql") 307 | result := NewRequestOptions(req) 308 | 309 | if !reflect.DeepEqual(result, expected) { 310 | t.Fatalf("wrong result, graphql result diff: %v", testutil.Diff(expected, result)) 311 | } 312 | } 313 | func TestRequestOptions_PUT_ContentTypeApplicationJSON(t *testing.T) { 314 | body := ` 315 | { 316 | "query": "query RebelsShipsQuery { rebels { name } }" 317 | }` 318 | expected := &RequestOptions{} 319 | 320 | req, _ := http.NewRequest("PUT", "/graphql", bytes.NewBufferString(body)) 321 | req.Header.Add("Content-Type", "application/json") 322 | result := NewRequestOptions(req) 323 | 324 | if !reflect.DeepEqual(result, expected) { 325 | t.Fatalf("wrong result, graphql result diff: %v", testutil.Diff(expected, result)) 326 | } 327 | } 328 | func TestRequestOptions_PUT_ContentTypeApplicationUrlEncoded(t *testing.T) { 329 | data := url.Values{} 330 | data.Add("query", "query RebelsShipsQuery { rebels { name } }") 331 | 332 | expected := &RequestOptions{} 333 | 334 | req, _ := http.NewRequest("PUT", "/graphql", bytes.NewBufferString(data.Encode())) 335 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 336 | result := NewRequestOptions(req) 337 | 338 | if !reflect.DeepEqual(result, expected) { 339 | t.Fatalf("wrong result, graphql result diff: %v", testutil.Diff(expected, result)) 340 | } 341 | } 342 | 343 | func TestRequestOptions_DELETE_BasicQueryString(t *testing.T) { 344 | queryString := "query=query RebelsShipsQuery { rebels { name } }" 345 | expected := &RequestOptions{ 346 | Query: "query RebelsShipsQuery { rebels { name } }", 347 | Variables: make(map[string]interface{}), 348 | } 349 | 350 | req, _ := http.NewRequest("DELETE", fmt.Sprintf("/graphql?%v", queryString), nil) 351 | result := NewRequestOptions(req) 352 | 353 | if !reflect.DeepEqual(result, expected) { 354 | t.Fatalf("wrong result, graphql result diff: %v", testutil.Diff(expected, result)) 355 | } 356 | } 357 | func TestRequestOptions_DELETE_ContentTypeApplicationGraphQL(t *testing.T) { 358 | body := []byte(`query RebelsShipsQuery { rebels { name } }`) 359 | expected := &RequestOptions{} 360 | 361 | req, _ := http.NewRequest("DELETE", "/graphql", bytes.NewBuffer(body)) 362 | req.Header.Add("Content-Type", "application/graphql") 363 | result := NewRequestOptions(req) 364 | 365 | if !reflect.DeepEqual(result, expected) { 366 | t.Fatalf("wrong result, graphql result diff: %v", testutil.Diff(expected, result)) 367 | } 368 | } 369 | func TestRequestOptions_DELETE_ContentTypeApplicationJSON(t *testing.T) { 370 | body := ` 371 | { 372 | "query": "query RebelsShipsQuery { rebels { name } }" 373 | }` 374 | expected := &RequestOptions{} 375 | 376 | req, _ := http.NewRequest("DELETE", "/graphql", bytes.NewBufferString(body)) 377 | req.Header.Add("Content-Type", "application/json") 378 | result := NewRequestOptions(req) 379 | 380 | if !reflect.DeepEqual(result, expected) { 381 | t.Fatalf("wrong result, graphql result diff: %v", testutil.Diff(expected, result)) 382 | } 383 | } 384 | func TestRequestOptions_DELETE_ContentTypeApplicationUrlEncoded(t *testing.T) { 385 | data := url.Values{} 386 | data.Add("query", "query RebelsShipsQuery { rebels { name } }") 387 | 388 | expected := &RequestOptions{} 389 | 390 | req, _ := http.NewRequest("DELETE", "/graphql", bytes.NewBufferString(data.Encode())) 391 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 392 | result := NewRequestOptions(req) 393 | 394 | if !reflect.DeepEqual(result, expected) { 395 | t.Fatalf("wrong result, graphql result diff: %v", testutil.Diff(expected, result)) 396 | } 397 | } 398 | 399 | func TestRequestOptions_POST_UnsupportedContentType(t *testing.T) { 400 | body := `query{}` 401 | expected := &RequestOptions{} 402 | 403 | req, _ := http.NewRequest("POST", "/graphql", bytes.NewBufferString(body)) 404 | req.Header.Add("Content-Type", "application/xml") 405 | result := NewRequestOptions(req) 406 | 407 | if !reflect.DeepEqual(result, expected) { 408 | t.Fatalf("wrong result, graphql result diff: %v", testutil.Diff(expected, result)) 409 | } 410 | } 411 | --------------------------------------------------------------------------------