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