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