github.com/blend/go-sdk@v1.20220411.3/webutil/request_option_test.go (about)

     1  /*
     2  
     3  Copyright (c) 2022 - Present. Blend Labs, Inc. All rights reserved
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file.
     5  
     6  */
     7  
     8  package webutil
     9  
    10  import (
    11  	"bytes"
    12  	"context"
    13  	"encoding/json"
    14  	"fmt"
    15  	"io"
    16  	"net/http"
    17  	"net/http/httptest"
    18  	"net/url"
    19  	"strings"
    20  	"testing"
    21  
    22  	"github.com/blend/go-sdk/assert"
    23  )
    24  
    25  type xmlBody struct {
    26  	X []string `xml:"x"`
    27  	Y []string `xml:"y"`
    28  }
    29  
    30  func TestRequestOptions(t *testing.T) {
    31  	assert := assert.New(t)
    32  
    33  	req := &http.Request{}
    34  
    35  	assert.Empty(req.Method)
    36  	assert.Nil(OptMethod("POST")(req))
    37  	assert.Equal("POST", req.Method)
    38  
    39  	req.Method = ""
    40  	assert.Nil(OptGet()(req))
    41  	assert.Equal("GET", req.Method)
    42  
    43  	req.Method = ""
    44  	assert.Nil(OptPost()(req))
    45  	assert.Equal("POST", req.Method)
    46  
    47  	req.Method = ""
    48  	assert.Nil(OptPut()(req))
    49  	assert.Equal("PUT", req.Method)
    50  
    51  	req.Method = ""
    52  	assert.Nil(OptPatch()(req))
    53  	assert.Equal("PATCH", req.Method)
    54  
    55  	req.Method = ""
    56  	assert.Nil(OptDelete()(req))
    57  	assert.Equal("DELETE", req.Method)
    58  
    59  	type contextKey struct{}
    60  	assert.Nil(req.Context().Value(contextKey{}))
    61  	assert.Nil(OptContext(context.WithValue(context.Background(), contextKey{}, "foo"))(req))
    62  	assert.Equal("foo", req.Context().Value(contextKey{}))
    63  
    64  	assert.Nil(req.URL)
    65  	assert.Nil(OptQuery(url.Values{"foo": []string{"bar", "baz"}})(req))
    66  	assert.NotNil(req.URL)
    67  	assert.Equal("foo=bar&foo=baz", req.URL.RawQuery)
    68  
    69  	req.URL = &url.URL{}
    70  	assert.Nil(OptQueryValue("foo", "bar")(req))
    71  	assert.NotNil(req.URL)
    72  	assert.Equal("foo=bar", req.URL.RawQuery)
    73  
    74  	assert.Nil(req.Header)
    75  	assert.Nil(OptHeader(http.Header{"X-Foo": []string{"bar", "baz"}})(req))
    76  	assert.Equal("bar", req.Header.Get("X-Foo"))
    77  
    78  	req.Header = nil
    79  	assert.Nil(OptHeaderValue("X-Foo", "bar")(req))
    80  	assert.Equal("bar", req.Header.Get("X-Foo"))
    81  
    82  	assert.Nil(req.PostForm)
    83  	assert.Nil(OptPostForm(url.Values{"foo": []string{"bar", "baz"}})(req))
    84  	assert.Equal("bar", req.PostForm.Get("foo"))
    85  
    86  	req.PostForm = nil
    87  	assert.Nil(OptPostFormValue("buzz", "fuzz")(req))
    88  	assert.Equal("fuzz", req.PostForm.Get("buzz"))
    89  
    90  	req.Header = nil
    91  	assert.Nil(OptCookie(&http.Cookie{Name: "sid", Value: "my value"})(req))
    92  	c, err := req.Cookie("sid")
    93  	assert.Nil(err)
    94  	assert.Equal("my value", c.Value)
    95  
    96  	req.Header = nil
    97  	assert.Nil(OptCookieValue("jsid", "another value")(req))
    98  	c, err = req.Cookie("jsid")
    99  	assert.Nil(err)
   100  	assert.Equal("another value", c.Value)
   101  
   102  	assert.Nil(req.Body)
   103  	assert.Nil(OptBody(io.NopCloser(bytes.NewReader([]byte("foo bar"))))(req))
   104  	assert.NotNil(req.Body)
   105  	read, err := io.ReadAll(req.Body)
   106  	assert.Nil(err)
   107  	assert.Equal([]byte("foo bar"), read)
   108  
   109  	req.Body = nil
   110  	assert.Nil(OptBodyBytes([]byte("bar foo"))(req))
   111  	assert.NotNil(req.Body)
   112  	read, err = io.ReadAll(req.Body)
   113  	assert.Nil(err)
   114  	assert.Equal([]byte("bar foo"), read)
   115  
   116  	postedFiles := []PostedFile{
   117  		{Key: "file0", FileName: "file.txt", Contents: []byte("foo bar baz")},
   118  		{Key: "file1", FileName: "file_1.txt", Contents: []byte("fuzzy wuzzy was a bear")},
   119  	}
   120  	req.Header = nil
   121  	req.Body = nil
   122  	assert.Nil(OptPostedFiles(postedFiles...)(req))
   123  	assert.NotEmpty(req.Header)
   124  	assert.NotNil(req.Body)
   125  
   126  	req.Header = nil
   127  	req.Body = nil
   128  	assert.Nil(OptJSONBody([]string{"foo", "bar"})(req))
   129  	assert.Equal(ContentTypeApplicationJSON, req.Header.Get(HeaderContentType))
   130  	assert.NotNil(req.Body)
   131  
   132  	req.Header = nil
   133  	req.Body = nil
   134  	assert.Nil(OptXMLBody([]string{"foo", "bar"})(req))
   135  	assert.Equal(ContentTypeApplicationXML, req.Header.Get(HeaderContentType))
   136  	assert.NotNil(req.Body)
   137  }
   138  
   139  func TestOptBodyBytes(t *testing.T) {
   140  	assert := assert.New(t)
   141  	body := []byte("hello\n")
   142  	opt := OptBodyBytes(body)
   143  
   144  	r := &http.Request{}
   145  	err := opt(r)
   146  	assert.Nil(err)
   147  
   148  	bodyBytes, err := io.ReadAll(r.Body)
   149  	assert.Nil(err)
   150  	assert.Equal(body, bodyBytes)
   151  	assert.Equal(r.ContentLength, 6)
   152  	validateRequestGetBody(assert, r, body)
   153  }
   154  
   155  func TestOptPostedFiles(t *testing.T) {
   156  	assert := assert.New(t)
   157  	file1 := PostedFile{Key: "a", FileName: "b.txt", Contents: []byte("hey")}
   158  	file2 := PostedFile{Key: "c", FileName: "d.txt", Contents: []byte("bye")}
   159  	file3 := PostedFile{Key: "d", FileName: "e.txt", Contents: []byte("bytes"), ContentType: "application/pdf"}
   160  	opt := OptPostedFiles(file1, file2, file3)
   161  
   162  	r := &http.Request{}
   163  	err := opt(r)
   164  	assert.Nil(err)
   165  
   166  	boundary := getBoundary(assert, r.Header)
   167  	ct := fmt.Sprintf("multipart/form-data; boundary=%s", boundary)
   168  	assert.Equal(r.Header, http.Header{HeaderContentType: []string{ct}})
   169  	bodyBytes, err := io.ReadAll(r.Body)
   170  	defer r.Body.Close()
   171  	assert.Nil(err)
   172  	expected := fmt.Sprintf(
   173  		"--%[1]s\r\nContent-Disposition: form-data; name=%[2]q; filename=%[3]q\r\n"+
   174  			"Content-Type: application/octet-stream\r\n\r\n%[4]s\r\n"+
   175  			"--%[1]s\r\nContent-Disposition: form-data; name=%[5]q; filename=%[6]q\r\n"+
   176  			"Content-Type: application/octet-stream\r\n\r\n%[7]s\r\n"+
   177  			"--%[1]s\r\nContent-Disposition: form-data; name=%[8]q; filename=%[9]q\r\n"+
   178  			"Content-Type: %[10]s\r\n\r\n%[11]s\r\n--%[1]s--\r\n",
   179  		boundary,
   180  		file1.Key,
   181  		file1.FileName,
   182  		file1.Contents,
   183  		file2.Key,
   184  		file2.FileName,
   185  		file2.Contents,
   186  		file3.Key,
   187  		file3.FileName,
   188  		file3.ContentType,
   189  		file3.Contents,
   190  	)
   191  	assert.Equal(expected, string(bodyBytes))
   192  	assert.Equal(r.ContentLength, len(expected))
   193  	validateRequestGetBody(assert, r, []byte(expected))
   194  }
   195  
   196  func TestOptPostedFiles_postForm(t *testing.T) {
   197  	assert := assert.New(t)
   198  
   199  	testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
   200  		if err := req.ParseMultipartForm(1 << 20); err != nil {
   201  			http.Error(w, err.Error(), http.StatusBadRequest)
   202  			return
   203  		}
   204  		if req.FormValue("foo") != "foo-value" {
   205  			http.Error(w, "post form value `foo` missing or incorrect", http.StatusBadRequest)
   206  			return
   207  		}
   208  		if req.FormValue("bar") != "bar-value" {
   209  			http.Error(w, "post form value `bar` missing or incorrect", http.StatusBadRequest)
   210  			return
   211  		}
   212  
   213  		files, err := PostedFiles(req)
   214  		if err != nil {
   215  			http.Error(w, err.Error(), http.StatusBadRequest)
   216  			return
   217  		}
   218  		if len(files) < 2 {
   219  			http.Error(w, "posted files missing", http.StatusBadRequest)
   220  			return
   221  		}
   222  
   223  		w.WriteHeader(http.StatusOK)
   224  		fmt.Fprintf(w, "OK!")
   225  	}))
   226  	defer testServer.Close()
   227  
   228  	file1 := PostedFile{Key: "a", FileName: "b.txt", Contents: []byte("hey")}
   229  	file2 := PostedFile{Key: "c", FileName: "d.txt", Contents: []byte("bye")}
   230  
   231  	postFormOpt0 := OptPostFormValue("foo", "foo-value")
   232  	postFormOpt1 := OptPostFormValue("bar", "bar-value")
   233  	opt := OptPostedFiles(file1, file2)
   234  
   235  	r, err := http.NewRequest(http.MethodPost, testServer.URL, nil)
   236  	assert.Nil(err)
   237  	err = postFormOpt0(r)
   238  	assert.Nil(err)
   239  	err = postFormOpt1(r)
   240  	assert.Nil(err)
   241  	err = opt(r)
   242  	assert.Nil(err)
   243  
   244  	res, err := http.DefaultClient.Do(r)
   245  	assert.Nil(err)
   246  	defer res.Body.Close()
   247  	message, _ := io.ReadAll(res.Body)
   248  	assert.Equal(http.StatusOK, res.StatusCode, string(message))
   249  }
   250  
   251  func TestOptJSONBody(t *testing.T) {
   252  	assert := assert.New(t)
   253  	payload := map[string]float64{"x": 1.25, "y": -5.75}
   254  	opt := OptJSONBody(payload)
   255  
   256  	r := &http.Request{}
   257  	err := opt(r)
   258  	assert.Nil(err)
   259  
   260  	assert.NotNil(r.Body)
   261  	assert.NotNil(r.GetBody)
   262  
   263  	assert.Equal(r.Header, http.Header{HeaderContentType: []string{ContentTypeApplicationJSON}})
   264  	bodyBytes, err := io.ReadAll(r.Body)
   265  	defer r.Body.Close()
   266  	assert.Nil(err)
   267  	expected := []byte(`{"x":1.25,"y":-5.75}`)
   268  	assert.Equal(expected, bodyBytes)
   269  	assert.Equal(r.ContentLength, 20)
   270  
   271  	validateRequestGetBody(assert, r, expected)
   272  
   273  	verifyContents, _ := json.Marshal(payload)
   274  	verify, err := http.NewRequest("POST", "https://example.com/test", bytes.NewReader(verifyContents))
   275  	assert.Nil(err)
   276  	assert.Equal(verify.ContentLength, r.ContentLength)
   277  }
   278  
   279  func TestOptXMLBody(t *testing.T) {
   280  	assert := assert.New(t)
   281  	payload := xmlBody{X: []string{"hello"}, Y: []string{"goodbye"}}
   282  	opt := OptXMLBody(payload)
   283  
   284  	r := &http.Request{}
   285  	err := opt(r)
   286  	assert.Nil(err)
   287  
   288  	assert.Equal(r.Header, http.Header{HeaderContentType: []string{ContentTypeApplicationXML}})
   289  	bodyBytes, err := io.ReadAll(r.Body)
   290  	assert.Nil(err)
   291  	expected := []byte("<xmlBody><x>hello</x><y>goodbye</y></xmlBody>")
   292  	assert.Equal(expected, bodyBytes)
   293  	assert.Equal(r.ContentLength, 45)
   294  	validateRequestGetBody(assert, r, expected)
   295  }
   296  
   297  func getBoundary(assert *assert.Assertions, h http.Header) string {
   298  	boundaryPrefix := "multipart/form-data; boundary="
   299  	ct := h.Get(HeaderContentType)
   300  	assert.True(strings.HasPrefix(ct, boundaryPrefix))
   301  	return strings.TrimPrefix(ct, boundaryPrefix)
   302  }
   303  
   304  func validateRequestGetBody(assert *assert.Assertions, r *http.Request, expected []byte) {
   305  	assert.NotNil(r.GetBody)
   306  	bodyRC, err := r.GetBody()
   307  	assert.Nil(err)
   308  	defer bodyRC.Close()
   309  	bodyBytes, err := io.ReadAll(bodyRC)
   310  	assert.Nil(err)
   311  	assert.Equal(expected, bodyBytes)
   312  }