diff --git a/middleware_test.go b/middleware_test.go index a9bac8c7..b071f29e 100644 --- a/middleware_test.go +++ b/middleware_test.go @@ -1,9 +1,15 @@ package resty import ( + "bytes" + "encoding/json" + "io" + "mime" + "mime/multipart" "net/http" "net/url" "reflect" + "strings" "testing" ) @@ -343,3 +349,557 @@ func Benchmark_parseRequestHeader(b *testing.B) { } } } + +func Test_parseRequestBody(t *testing.T) { + for _, tt := range []struct { + name string + init func(c *Client, r *Request) + expectedBodyBuf []byte + expectedContentLength string + expectedContentType string + }{ + { + name: "empty body", + init: func(c *Client, r *Request) {}, + }, + { + name: "empty body with SetContentLength by request", + init: func(c *Client, r *Request) { + r.SetContentLength(true) + }, + expectedContentLength: "0", + }, + { + name: "empty body with SetContentLength by client", + init: func(c *Client, r *Request) { + c.SetContentLength(true) + }, + expectedContentLength: "0", + }, + { + name: "string body", + init: func(c *Client, r *Request) { + r.SetBody("foo") + }, + expectedBodyBuf: []byte("foo"), + expectedContentType: plainTextType, + }, + { + name: "string body with GET method", + init: func(c *Client, r *Request) { + r.SetBody("foo") + r.Method = http.MethodGet + }, + }, + { + name: "string body with GET method and AllowGetMethodPayload", + init: func(c *Client, r *Request) { + c.SetAllowGetMethodPayload(true) + r.SetBody("foo") + r.Method = http.MethodGet + }, + expectedBodyBuf: []byte("foo"), + expectedContentType: plainTextType, + }, + { + name: "string body with HEAD method", + init: func(c *Client, r *Request) { + r.SetBody("foo") + r.Method = http.MethodHead + }, + }, + { + name: "string body with OPTIONS method", + init: func(c *Client, r *Request) { + r.SetBody("foo") + r.Method = http.MethodOptions + }, + }, + { + name: "string body with POST method", + init: func(c *Client, r *Request) { + r.SetBody("foo") + r.Method = http.MethodPost + }, + expectedBodyBuf: []byte("foo"), + expectedContentType: plainTextType, + }, + { + name: "string body with PATCH method", + init: func(c *Client, r *Request) { + r.SetBody("foo") + r.Method = http.MethodPatch + }, + expectedBodyBuf: []byte("foo"), + expectedContentType: plainTextType, + }, + { + name: "string body with PUT method", + init: func(c *Client, r *Request) { + r.SetBody("foo") + r.Method = http.MethodPut + }, + expectedBodyBuf: []byte("foo"), + expectedContentType: plainTextType, + }, + { + name: "string body with DELETE method", + init: func(c *Client, r *Request) { + r.SetBody("foo") + r.Method = http.MethodDelete + }, + expectedBodyBuf: []byte("foo"), + expectedContentType: plainTextType, + }, + { + name: "string body with CONNECT method", + init: func(c *Client, r *Request) { + r.SetBody("foo") + r.Method = http.MethodConnect + }, + expectedBodyBuf: []byte("foo"), + expectedContentType: plainTextType, + }, + { + name: "string body with TRACE method", + init: func(c *Client, r *Request) { + r.SetBody("foo") + r.Method = http.MethodTrace + }, + expectedBodyBuf: []byte("foo"), + expectedContentType: plainTextType, + }, + { + name: "string body with BAR method", + init: func(c *Client, r *Request) { + r.SetBody("foo") + r.Method = "BAR" + }, + expectedBodyBuf: []byte("foo"), + expectedContentType: plainTextType, + }, + { + name: "byte body", + init: func(c *Client, r *Request) { + r.SetBody([]byte("foo")) + }, + expectedBodyBuf: []byte("foo"), + expectedContentType: plainTextType, + }, + { + name: "io.Reader body, no bodyBuf", + init: func(c *Client, r *Request) { + r.SetBody(bytes.NewBufferString("foo")) + }, + expectedContentType: jsonContentType, + }, + { + name: "io.Reader body with SetContentLength by request", + init: func(c *Client, r *Request) { + r.SetBody(bytes.NewBufferString("foo")). + SetContentLength(true) + }, + expectedBodyBuf: []byte("foo"), + expectedContentLength: "3", + expectedContentType: jsonContentType, + }, + { + name: "io.Reader body with SetContentLength by client", + init: func(c *Client, r *Request) { + c.SetContentLength(true) + r.SetBody(bytes.NewBufferString("foo")) + }, + expectedBodyBuf: []byte("foo"), + expectedContentLength: "3", + expectedContentType: jsonContentType, + }, + { + name: "form data by request", + init: func(c *Client, r *Request) { + r.SetFormData(map[string]string{ + "foo": "1", + "bar": "2", + }) + }, + expectedBodyBuf: []byte("foo=1&bar=2"), + expectedContentType: formContentType, + }, + { + name: "form data by client", + init: func(c *Client, r *Request) { + c.SetFormData(map[string]string{ + "foo": "1", + "bar": "2", + }) + }, + expectedBodyBuf: []byte("foo=1&bar=2"), + expectedContentType: formContentType, + }, + { + name: "form data by client and request", + init: func(c *Client, r *Request) { + c.SetFormData(map[string]string{ + "foo": "1", + "bar": "2", + }) + r.SetFormData(map[string]string{ + "foo": "3", + "baz": "4", + }) + }, + expectedBodyBuf: []byte("foo=3&bar=2&baz=4"), + expectedContentType: formContentType, + }, + { + name: "json from struct", + init: func(c *Client, r *Request) { + r.SetBody(struct { + Foo string `json:"foo"` + Bar string `json:"bar"` + }{ + Foo: "1", + Bar: "2", + }).SetContentLength(true) + }, + expectedBodyBuf: []byte(`{"foo":"1","bar":"2"}`), + expectedContentType: jsonContentType, + expectedContentLength: "21", + }, + { + name: "json from slice", + init: func(c *Client, r *Request) { + r.SetBody([]string{"foo", "bar"}).SetContentLength(true) + }, + expectedBodyBuf: []byte(`["foo","bar"]`), + expectedContentType: jsonContentType, + expectedContentLength: "13", + }, + { + name: "json from map", + init: func(c *Client, r *Request) { + r.SetBody(map[string]interface{}{ + "foo": "1", + "bar": []int{1, 2, 3}, + "baz": map[string]string{ + "qux": "4", + }, + "xyz": nil, + }).SetContentLength(true) + }, + expectedBodyBuf: []byte(`{"bar":[1,2,3],"baz":{"qux":"4"},"foo":"1","xyz":null}`), + expectedContentType: jsonContentType, + expectedContentLength: "54", + }, + { + name: "json from map", + init: func(c *Client, r *Request) { + r.SetBody(map[string]interface{}{ + "foo": "1", + "bar": []int{1, 2, 3}, + "baz": map[string]string{ + "qux": "4", + }, + "xyz": nil, + }).SetContentLength(true) + }, + expectedBodyBuf: []byte(`{"bar":[1,2,3],"baz":{"qux":"4"},"foo":"1","xyz":null}`), + expectedContentType: jsonContentType, + expectedContentLength: "54", + }, + { + name: "json from map", + init: func(c *Client, r *Request) { + r.SetBody(map[string]interface{}{ + "foo": "1", + "bar": []int{1, 2, 3}, + "baz": map[string]string{ + "qux": "4", + }, + "xyz": nil, + }).SetContentLength(true) + }, + expectedBodyBuf: []byte(`{"bar":[1,2,3],"baz":{"qux":"4"},"foo":"1","xyz":null}`), + expectedContentType: jsonContentType, + expectedContentLength: "54", + }, + { + name: "xml from struct", + init: func(c *Client, r *Request) { + type FooBar struct { + Foo string `xml:"foo"` + Bar string `xml:"bar"` + } + r.SetBody(FooBar{ + Foo: "1", + Bar: "2", + }). + SetContentLength(true). + SetHeader(hdrContentTypeKey, "text/xml") + }, + expectedBodyBuf: []byte(`12`), + expectedContentType: "text/xml", + expectedContentLength: "41", + }, + { + name: "mulipart form data", + init: func(c *Client, r *Request) { + c.SetFormData(map[string]string{ + "foo": "1", + "bar": "2", + }) + r.SetFormData(map[string]string{ + "foo": "3", + "baz": "4", + }) + r.SetMultipartFormData(map[string]string{ + "foo": "5", + "xyz": "6", + }).SetContentLength(true) + }, + expectedBodyBuf: []byte(`{"bar":"2", "baz":"4", "foo":"5", "xyz":"6"}`), + expectedContentType: "multipart/form-data; boundary=", + expectedContentLength: "744", + }, + { + name: "mulipart fields", + init: func(c *Client, r *Request) { + r.SetMultipartFields( + &MultipartField{ + Param: "foo", + ContentType: "text/plain", + Reader: strings.NewReader("1"), + }, + &MultipartField{ + Param: "bar", + ContentType: "text/plain", + Reader: strings.NewReader("2"), + }, + ).SetContentLength(true) + }, + expectedBodyBuf: []byte(`{"bar":"2","foo":"1"}`), + expectedContentType: "multipart/form-data; boundary=", + expectedContentLength: "344", + }, + { + name: "mulipart files", + init: func(c *Client, r *Request) { + r.SetFileReader("foo", "foo.txt", strings.NewReader("1")). + SetFileReader("bar", "bar.txt", strings.NewReader("2")). + SetContentLength(true) + }, + expectedBodyBuf: []byte(`{"bar":"2","foo":"1"}`), + expectedContentType: "multipart/form-data; boundary=", + expectedContentLength: "412", + }, + } { + t.Run(tt.name, func(t *testing.T) { + c := New() + r := c.R() + tt.init(c, r) + if err := parseRequestBody(c, r); err != nil { + t.Errorf("parseRequestBody() error = %v", err) + } + switch { + case r.bodyBuf == nil && tt.expectedBodyBuf != nil: + t.Errorf("bodyBuf is nil, but expected: %s", string(tt.expectedBodyBuf)) + case r.bodyBuf != nil && tt.expectedBodyBuf == nil: + t.Errorf("bodyBuf is not nil, but expected nil: %s", r.bodyBuf.String()) + case r.bodyBuf != nil && tt.expectedBodyBuf != nil: + var actual, expected interface{} = r.bodyBuf.Bytes(), tt.expectedBodyBuf + if r.isFormData { + var err error + actual, err = url.ParseQuery(r.bodyBuf.String()) + if err != nil { + t.Errorf("ParseQuery(r.bodyBuf) error = %v", err) + } + expected, err = url.ParseQuery(string(tt.expectedBodyBuf)) + if err != nil { + t.Errorf("ParseQuery(tt.expectedBodyBuf) error = %v", err) + } + } else if r.isMultiPart { + _, params, err := mime.ParseMediaType(r.Header.Get(hdrContentTypeKey)) + if err != nil { + t.Errorf("ParseMediaType(hdrContentTypeKey) error = %v", err) + } + boundary, ok := params["boundary"] + if !ok { + t.Errorf("boundary not found in Content-Type header") + } + reader := multipart.NewReader(r.bodyBuf, boundary) + body := make(map[string]interface{}) + for part, perr := reader.NextPart(); perr != io.EOF; part, perr = reader.NextPart() { + if perr != nil { + t.Errorf("NextPart() error = %v", perr) + } + name := part.FormName() + if name == "" { + name = part.FileName() + } + data, err := io.ReadAll(part) + if err != nil { + t.Errorf("ReadAll(part) error = %v", err) + } + body[name] = string(data) + } + actual = body + expected = nil + if err := json.Unmarshal(tt.expectedBodyBuf, &expected); err != nil { + t.Errorf("json.Unmarshal(tt.expectedBodyBuf) error = %v", err) + } + t.Logf(`in case of an error, the expected body should be set as json for object: %#+v`, actual) + } + if !reflect.DeepEqual(actual, expected) { + t.Errorf("bodyBuf = %q does not match expected %q", r.bodyBuf.String(), string(tt.expectedBodyBuf)) + } + } + if tt.expectedContentLength != r.Header.Get(hdrContentLengthKey) { + t.Errorf("Content-Length header = %q does not match expected %q", r.Header.Get(hdrContentLengthKey), tt.expectedContentLength) + } + if ct := r.Header.Get(hdrContentTypeKey); !((tt.expectedContentType == "" && ct != "") || strings.Contains(ct, tt.expectedContentType)) { + t.Errorf("Content-Type header = %q does not match expected %q", r.Header.Get(hdrContentTypeKey), tt.expectedContentType) + } + }) + } +} + +func Benchmark_parseRequestBody_string(b *testing.B) { + c := New() + r := c.R() + r.SetBody("foo").SetContentLength(true) + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := parseRequestBody(c, r); err != nil { + b.Errorf("parseRequestBody() error = %v", err) + } + } +} + +func Benchmark_parseRequestBody_byte(b *testing.B) { + c := New() + r := c.R() + r.SetBody([]byte("foo")).SetContentLength(true) + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := parseRequestBody(c, r); err != nil { + b.Errorf("parseRequestBody() error = %v", err) + } + } +} + +func Benchmark_parseRequestBody_reader_with_SetContentLength(b *testing.B) { + c := New() + r := c.R() + r.SetBody(bytes.NewBufferString("foo")).SetContentLength(true) + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := parseRequestBody(c, r); err != nil { + b.Errorf("parseRequestBody() error = %v", err) + } + } +} +func Benchmark_parseRequestBody_reader_without_SetContentLength(b *testing.B) { + c := New() + r := c.R() + r.SetBody(bytes.NewBufferString("foo")) + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := parseRequestBody(c, r); err != nil { + b.Errorf("parseRequestBody() error = %v", err) + } + } +} + +func Benchmark_parseRequestBody_struct(b *testing.B) { + type FooBar struct { + Foo string `json:"foo"` + Bar string `json:"bar"` + } + c := New() + r := c.R() + r.SetBody(FooBar{Foo: "1", Bar: "2"}).SetContentLength(true).SetHeader(hdrContentTypeKey, jsonContentType) + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := parseRequestBody(c, r); err != nil { + b.Errorf("parseRequestBody() error = %v", err) + } + } +} + +func Benchmark_parseRequestBody_struct_xml(b *testing.B) { + type FooBar struct { + Foo string `xml:"foo"` + Bar string `xml:"bar"` + } + c := New() + r := c.R() + r.SetBody(FooBar{Foo: "1", Bar: "2"}).SetContentLength(true).SetHeader(hdrContentTypeKey, "text/xml") + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := parseRequestBody(c, r); err != nil { + b.Errorf("parseRequestBody() error = %v", err) + } + } +} + +func Benchmark_parseRequestBody_map(b *testing.B) { + c := New() + r := c.R() + r.SetBody(map[string]string{ + "foo": "1", + "bar": "2", + }).SetContentLength(true).SetHeader(hdrContentTypeKey, jsonContentType) + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := parseRequestBody(c, r); err != nil { + b.Errorf("parseRequestBody() error = %v", err) + } + } +} + +func Benchmark_parseRequestBody_slice(b *testing.B) { + c := New() + r := c.R() + r.SetBody([]string{"1", "2"}).SetContentLength(true).SetHeader(hdrContentTypeKey, jsonContentType) + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := parseRequestBody(c, r); err != nil { + b.Errorf("parseRequestBody() error = %v", err) + } + } +} + +func Benchmark_parseRequestBody_FormData(b *testing.B) { + c := New() + r := c.R() + c.SetFormData(map[string]string{"foo": "1", "bar": "2"}) + r.SetFormData(map[string]string{"foo": "3", "baz": "4"}).SetContentLength(true) + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := parseRequestBody(c, r); err != nil { + b.Errorf("parseRequestBody() error = %v", err) + } + } +} + +func Benchmark_parseRequestBody_MultiPart(b *testing.B) { + c := New() + r := c.R() + c.SetFormData(map[string]string{"foo": "1", "bar": "2"}) + r.SetFormData(map[string]string{"foo": "3", "baz": "4"}). + SetMultipartFormData(map[string]string{"foo": "5", "xyz": "6"}). + SetFileReader("qwe", "qwe.txt", strings.NewReader("7")). + SetMultipartFields( + &MultipartField{ + Param: "sdj", + ContentType: "text/plain", + Reader: strings.NewReader("8"), + }, + ). + SetContentLength(true) + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := parseRequestBody(c, r); err != nil { + b.Errorf("parseRequestBody() error = %v", err) + } + } +}