github.com/ethersphere/bee/v2@v2.2.0/pkg/jsonhttp/jsonhttptest/jsonhttptest.go (about)

     1  // Copyright 2020 The Swarm Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package jsonhttptest
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"encoding/json"
    11  	"fmt"
    12  	"io"
    13  	"mime/multipart"
    14  	"net/http"
    15  	"net/textproto"
    16  	"reflect"
    17  	"sort"
    18  	"strconv"
    19  	"testing"
    20  
    21  	"github.com/ethersphere/bee/v2/pkg/jsonhttp"
    22  )
    23  
    24  // Request is a testing helper function that makes an HTTP request using
    25  // provided client with provided method and url. It performs a validation on
    26  // expected response code and additional options. It returns response headers if
    27  // the request and all validation are successful. In case of any error, testing
    28  // Errorf or Fatal functions will be called.
    29  func Request(tb testing.TB, client *http.Client, method, url string, responseCode int, opts ...Option) http.Header {
    30  	tb.Helper()
    31  
    32  	o := new(options)
    33  	for _, opt := range opts {
    34  		if err := opt.apply(o); err != nil {
    35  			tb.Fatal(err)
    36  		}
    37  	}
    38  
    39  	req, err := http.NewRequest(method, url, o.requestBody)
    40  	if err != nil {
    41  		tb.Fatal(err)
    42  	}
    43  	req.Header = o.requestHeaders
    44  	if o.ctx != nil {
    45  		req = req.WithContext(o.ctx)
    46  	}
    47  	resp, err := client.Do(req)
    48  	if err != nil {
    49  		tb.Fatal(err)
    50  	}
    51  	defer resp.Body.Close()
    52  
    53  	if resp.StatusCode != responseCode {
    54  		tb.Errorf("got response status %s, want %v %s", resp.Status, responseCode, http.StatusText(responseCode))
    55  	}
    56  
    57  	for _, key := range o.nonEmptyResponseHeaders {
    58  		if val := resp.Header.Get(key); val == "" {
    59  			tb.Errorf("header key=[%s] should be set", key)
    60  		}
    61  	}
    62  
    63  	if headers := o.expectedResponseHeaders; headers != nil {
    64  		for key, values := range headers {
    65  			got := sort.StringSlice(resp.Header.Values(key))
    66  			want := sort.StringSlice(values)
    67  			if !reflect.DeepEqual(got, want) {
    68  				tb.Errorf("header values for key=[%s] not as expected, got: %v, want %v", key, got, want)
    69  			}
    70  		}
    71  
    72  		// When "Content-Length" header is set additionally assert
    73  		// that resp.ContentLength has the same value.
    74  		if want := headers.Get("Content-Length"); want != "" {
    75  			got := strconv.FormatInt(resp.ContentLength, 10)
    76  			if want != got {
    77  				tb.Errorf("http.Response.ContentLength not as expected, got %v, want %v", got, want)
    78  			}
    79  		}
    80  	}
    81  
    82  	if o.expectedResponse != nil {
    83  		got, err := io.ReadAll(resp.Body)
    84  		if err != nil {
    85  			tb.Fatal(err)
    86  		}
    87  
    88  		if !bytes.Equal(got, o.expectedResponse) {
    89  			tb.Errorf("got response %q, want %q", string(got), string(o.expectedResponse))
    90  		}
    91  		return resp.Header
    92  	}
    93  
    94  	if o.expectedJSONResponse != nil {
    95  		if v := resp.Header.Get("Content-Type"); v != jsonhttp.DefaultContentTypeHeader {
    96  			tb.Errorf("got content type %q, want %q", v, jsonhttp.DefaultContentTypeHeader)
    97  		}
    98  		got, err := io.ReadAll(resp.Body)
    99  		if err != nil {
   100  			tb.Fatal(err)
   101  		}
   102  		got = bytes.TrimSpace(got)
   103  
   104  		want, err := json.Marshal(o.expectedJSONResponse)
   105  		if err != nil {
   106  			tb.Fatal(err)
   107  		}
   108  
   109  		if !bytes.Equal(got, want) {
   110  			tb.Errorf("got json response %q, want %q", string(got), string(want))
   111  		}
   112  		return resp.Header
   113  	}
   114  
   115  	if o.unmarshalResponse != nil {
   116  		if err := json.NewDecoder(resp.Body).Decode(&o.unmarshalResponse); err != nil {
   117  			tb.Fatal(err)
   118  		}
   119  		return resp.Header
   120  	}
   121  	if o.responseBody != nil {
   122  		got, err := io.ReadAll(resp.Body)
   123  		if err != nil {
   124  			tb.Fatal(err)
   125  		}
   126  		*o.responseBody = got
   127  	}
   128  	if o.noResponseBody {
   129  		got, err := io.ReadAll(resp.Body)
   130  		if err != nil {
   131  			tb.Fatal(err)
   132  		}
   133  		if len(got) > 0 {
   134  			tb.Errorf("got response body %q, want none", string(got))
   135  		}
   136  	}
   137  	return resp.Header
   138  }
   139  
   140  // WithContext sets a context to the request made by the Request function.
   141  func WithContext(ctx context.Context) Option {
   142  	return optionFunc(func(o *options) error {
   143  		o.ctx = ctx
   144  		return nil
   145  	})
   146  }
   147  
   148  // WithRequestBody writes a request body to the request made by the Request
   149  // function.
   150  func WithRequestBody(body io.Reader) Option {
   151  	return optionFunc(func(o *options) error {
   152  		o.requestBody = body
   153  		return nil
   154  	})
   155  }
   156  
   157  // WithJSONRequestBody writes a request JSON-encoded body to the request made by
   158  // the Request function.
   159  func WithJSONRequestBody(r interface{}) Option {
   160  	return optionFunc(func(o *options) error {
   161  		b, err := json.Marshal(r)
   162  		if err != nil {
   163  			return fmt.Errorf("json encode request body: %w", err)
   164  		}
   165  		o.requestBody = bytes.NewReader(b)
   166  		return nil
   167  	})
   168  }
   169  
   170  // WithMultipartRequest writes a multipart request with a single file in it to
   171  // the request made by the Request function.
   172  func WithMultipartRequest(body io.Reader, length int, filename, contentType string) Option {
   173  	return optionFunc(func(o *options) error {
   174  		buf := bytes.NewBuffer(nil)
   175  		mw := multipart.NewWriter(buf)
   176  		hdr := make(textproto.MIMEHeader)
   177  		if filename != "" {
   178  			hdr.Set("Content-Disposition", fmt.Sprintf("form-data; name=%q", filename))
   179  		}
   180  		if contentType != "" {
   181  			hdr.Set("Content-Type", contentType)
   182  		}
   183  		if length > 0 {
   184  			hdr.Set("Content-Length", strconv.Itoa(length))
   185  		}
   186  		part, err := mw.CreatePart(hdr)
   187  		if err != nil {
   188  			return fmt.Errorf("create multipart part: %w", err)
   189  		}
   190  		if _, err = io.Copy(part, body); err != nil {
   191  			return fmt.Errorf("copy file data to multipart part: %w", err)
   192  		}
   193  		if err := mw.Close(); err != nil {
   194  			return fmt.Errorf("close multipart writer: %w", err)
   195  		}
   196  		o.requestBody = buf
   197  		if o.requestHeaders == nil {
   198  			o.requestHeaders = make(http.Header)
   199  		}
   200  		o.requestHeaders.Set("Content-Type", fmt.Sprintf("multipart/form-data; boundary=%q", mw.Boundary()))
   201  		return nil
   202  	})
   203  }
   204  
   205  // WithRequestHeader adds a single header to the request made by the Request
   206  // function. To add multiple headers call multiple times this option when as
   207  // arguments to the Request function.
   208  func WithRequestHeader(key, value string) Option {
   209  	return optionFunc(func(o *options) error {
   210  		if o.requestHeaders == nil {
   211  			o.requestHeaders = make(http.Header)
   212  		}
   213  		o.requestHeaders.Add(key, value)
   214  		return nil
   215  	})
   216  }
   217  
   218  // WithExpectedResponse validates that the response from the request in the
   219  // Request function matches completely bytes provided here.
   220  func WithExpectedResponse(response []byte) Option {
   221  	return optionFunc(func(o *options) error {
   222  		o.expectedResponse = response
   223  		return nil
   224  	})
   225  }
   226  
   227  // WithExpectedResponseHeader validates that the response from the request
   228  // has header with specified value
   229  func WithExpectedResponseHeader(key, value string) Option {
   230  	return optionFunc(func(o *options) error {
   231  		if o.expectedResponseHeaders == nil {
   232  			o.expectedResponseHeaders = make(http.Header)
   233  		}
   234  		o.expectedResponseHeaders.Add(key, value)
   235  		return nil
   236  	})
   237  }
   238  
   239  // WithExpectedContentLength is shorthand for creating "Content-Length" header check.
   240  func WithExpectedContentLength(value int) Option {
   241  	return WithExpectedResponseHeader("Content-Length", strconv.Itoa(value))
   242  }
   243  
   244  // WithNonEmptyResponseHeader validates that the response from the request
   245  // has header with non empty value.
   246  func WithNonEmptyResponseHeader(key string) Option {
   247  	return optionFunc(func(o *options) error {
   248  		if o.nonEmptyResponseHeaders == nil {
   249  			o.nonEmptyResponseHeaders = make([]string, 0, 1)
   250  		}
   251  		o.nonEmptyResponseHeaders = append(o.nonEmptyResponseHeaders, key)
   252  		return nil
   253  	})
   254  }
   255  
   256  // WithExpectedJSONResponse validates that the response from the request in the
   257  // Request function matches JSON-encoded body provided here.
   258  func WithExpectedJSONResponse(response interface{}) Option {
   259  	return optionFunc(func(o *options) error {
   260  		o.expectedJSONResponse = response
   261  		return nil
   262  	})
   263  }
   264  
   265  // WithUnmarshalJSONResponse unmarshals response body from the request in the
   266  // Request function to the provided response. Response must be a pointer.
   267  func WithUnmarshalJSONResponse(response interface{}) Option {
   268  	return optionFunc(func(o *options) error {
   269  		o.unmarshalResponse = response
   270  		return nil
   271  	})
   272  }
   273  
   274  // WithPutResponseBody replaces the data in the provided byte slice with the
   275  // data from the response body of the request in the Request function.
   276  //
   277  // Example:
   278  //
   279  //	var respBytes []byte
   280  //	options := []jsonhttptest.Option{
   281  //		jsonhttptest.WithPutResponseBody(&respBytes),
   282  //	}
   283  func WithPutResponseBody(b *[]byte) Option {
   284  	return optionFunc(func(o *options) error {
   285  		o.responseBody = b
   286  		return nil
   287  	})
   288  }
   289  
   290  // WithNoResponseBody ensures that there is no data sent by the response of the
   291  // request in the Request function.
   292  func WithNoResponseBody() Option {
   293  	return optionFunc(func(o *options) error {
   294  		o.noResponseBody = true
   295  		return nil
   296  	})
   297  }
   298  
   299  type options struct {
   300  	ctx                     context.Context
   301  	requestBody             io.Reader
   302  	requestHeaders          http.Header
   303  	expectedResponseHeaders http.Header
   304  	nonEmptyResponseHeaders []string
   305  	expectedResponse        []byte
   306  	expectedJSONResponse    interface{}
   307  	unmarshalResponse       interface{}
   308  	responseBody            *[]byte
   309  	noResponseBody          bool
   310  }
   311  
   312  type Option interface {
   313  	apply(*options) error
   314  }
   315  type optionFunc func(*options) error
   316  
   317  func (f optionFunc) apply(r *options) error { return f(r) }