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) }