github.com/niko0xdev/gqlgen@v0.17.55-0.20240120102243-2ecff98c3e37/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  }