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  }