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  }