github.com/animeshon/gqlgen@v0.13.1-0.20210304133704-3a770431bb6d/client/client.go (about) 1 // client is used internally for testing. See readme for alternatives 2 3 package client 4 5 import ( 6 "bytes" 7 "encoding/json" 8 "fmt" 9 "io/ioutil" 10 "net/http" 11 "net/http/httptest" 12 13 "github.com/mitchellh/mapstructure" 14 ) 15 16 type ( 17 // Client used for testing GraphQL servers. Not for production use. 18 Client struct { 19 h http.Handler 20 opts []Option 21 } 22 23 // Option implements a visitor that mutates an outgoing GraphQL request 24 // 25 // This is the Option pattern - https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis 26 Option func(bd *Request) 27 28 // Request represents an outgoing GraphQL request 29 Request struct { 30 Query string `json:"query"` 31 Variables map[string]interface{} `json:"variables,omitempty"` 32 OperationName string `json:"operationName,omitempty"` 33 HTTP *http.Request `json:"-"` 34 } 35 36 // Response is a GraphQL layer response from a handler. 37 Response struct { 38 Data interface{} 39 Errors json.RawMessage 40 Extensions map[string]interface{} 41 } 42 ) 43 44 // New creates a graphql client 45 // Options can be set that should be applied to all requests made with this client 46 func New(h http.Handler, opts ...Option) *Client { 47 p := &Client{ 48 h: h, 49 opts: opts, 50 } 51 52 return p 53 } 54 55 // MustPost is a convenience wrapper around Post that automatically panics on error 56 func (p *Client) MustPost(query string, response interface{}, options ...Option) { 57 if err := p.Post(query, response, options...); err != nil { 58 panic(err) 59 } 60 } 61 62 // Post sends a http POST request to the graphql endpoint with the given query then unpacks 63 // the response into the given object. 64 func (p *Client) Post(query string, response interface{}, options ...Option) error { 65 respDataRaw, err := p.RawPost(query, options...) 66 if err != nil { 67 return err 68 } 69 70 // we want to unpack even if there is an error, so we can see partial responses 71 unpackErr := unpack(respDataRaw.Data, response) 72 73 if respDataRaw.Errors != nil { 74 return RawJsonError{respDataRaw.Errors} 75 } 76 return unpackErr 77 } 78 79 // RawPost is similar to Post, except it skips decoding the raw json response 80 // unpacked onto Response. This is used to test extension keys which are not 81 // available when using Post. 82 func (p *Client) RawPost(query string, options ...Option) (*Response, error) { 83 r, err := p.newRequest(query, options...) 84 if err != nil { 85 return nil, fmt.Errorf("build: %s", err.Error()) 86 } 87 88 w := httptest.NewRecorder() 89 p.h.ServeHTTP(w, r) 90 91 if w.Code >= http.StatusBadRequest { 92 return nil, fmt.Errorf("http %d: %s", w.Code, w.Body.String()) 93 } 94 95 // decode it into map string first, let mapstructure do the final decode 96 // because it can be much stricter about unknown fields. 97 respDataRaw := &Response{} 98 err = json.Unmarshal(w.Body.Bytes(), &respDataRaw) 99 if err != nil { 100 return nil, fmt.Errorf("decode: %s", err.Error()) 101 } 102 103 return respDataRaw, nil 104 } 105 106 func (p *Client) newRequest(query string, options ...Option) (*http.Request, error) { 107 bd := &Request{ 108 Query: query, 109 HTTP: httptest.NewRequest(http.MethodPost, "/", nil), 110 } 111 bd.HTTP.Header.Set("Content-Type", "application/json") 112 113 // per client options from client.New apply first 114 for _, option := range p.opts { 115 option(bd) 116 } 117 // per request options 118 for _, option := range options { 119 option(bd) 120 } 121 122 switch bd.HTTP.Header.Get("Content-Type") { 123 case "application/json": 124 requestBody, err := json.Marshal(bd) 125 if err != nil { 126 return nil, fmt.Errorf("encode: %s", err.Error()) 127 } 128 bd.HTTP.Body = ioutil.NopCloser(bytes.NewBuffer(requestBody)) 129 default: 130 panic("unsupported encoding" + bd.HTTP.Header.Get("Content-Type")) 131 } 132 133 return bd.HTTP, nil 134 } 135 136 func unpack(data interface{}, into interface{}) error { 137 d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ 138 Result: into, 139 TagName: "json", 140 ErrorUnused: true, 141 ZeroFields: true, 142 }) 143 if err != nil { 144 return fmt.Errorf("mapstructure: %s", err.Error()) 145 } 146 147 return d.Decode(data) 148 }