// Copyright 2015 go-swagger maintainers // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package middleware import ( "bytes" "io" "mime/multipart" "net/http" "net/url" "strings" "testing" "time" "github.com/stretchr/testify/require" "github.com/go-openapi/spec" "github.com/go-openapi/strfmt" "github.com/stretchr/testify/assert" "github.com/go-openapi/runtime" ) type stubConsumer struct { } func (s *stubConsumer) Consume(_ io.Reader, _ interface{}) error { return nil } type friend struct { Name string `json:"name"` Age int `json:"age"` } type jsonRequestParams struct { ID int64 // path Name string // query Friend friend // body RequestID int64 // header Tags []string // csv } type jsonRequestPtr struct { ID int64 // path Name string // query RequestID int64 // header Tags []string // csv Friend *friend } type jsonRequestSlice struct { ID int64 // path Name string // query RequestID int64 // header Tags []string // csv Friend []friend } func parametersForAllTypes(fmt string) map[string]spec.Parameter { if fmt == "" { fmt = "csv" } nameParam := spec.QueryParam("name").Typed("string", "") idParam := spec.PathParam("id").Typed("integer", "int64") ageParam := spec.QueryParam("age").Typed("integer", "int32") scoreParam := spec.QueryParam("score").Typed("number", "float") factorParam := spec.QueryParam("factor").Typed("number", "double") friendSchema := new(spec.Schema).Typed("object", "") friendParam := spec.BodyParam("friend", friendSchema) requestIDParam := spec.HeaderParam("X-Request-Id").Typed("integer", "int64") requestIDParam.Extensions = spec.Extensions(map[string]interface{}{}) requestIDParam.Extensions.Add("go-name", "RequestID") items := new(spec.Items) items.Type = "string" tagsParam := spec.QueryParam("tags").CollectionOf(items, fmt) confirmedParam := spec.QueryParam("confirmed").Typed("boolean", "") plannedParam := spec.QueryParam("planned").Typed("string", "date") deliveredParam := spec.QueryParam("delivered").Typed("string", "date-time") pictureParam := spec.QueryParam("picture").Typed("string", "byte") // base64 encoded during transport return map[string]spec.Parameter{ "ID": *idParam, "Name": *nameParam, "RequestID": *requestIDParam, "Friend": *friendParam, "Tags": *tagsParam, "Age": *ageParam, "Score": *scoreParam, "Factor": *factorParam, "Confirmed": *confirmedParam, "Planned": *plannedParam, "Delivered": *deliveredParam, "Picture": *pictureParam, } } func parametersForJSONRequestParams(fmt string) map[string]spec.Parameter { if fmt == "" { fmt = "csv" } nameParam := spec.QueryParam("name").Typed("string", "") idParam := spec.PathParam("id").Typed("integer", "int64") friendSchema := new(spec.Schema).Typed("object", "") friendParam := spec.BodyParam("friend", friendSchema) requestIDParam := spec.HeaderParam("X-Request-Id").Typed("integer", "int64") requestIDParam.Extensions = spec.Extensions(map[string]interface{}{}) requestIDParam.Extensions.Add("go-name", "RequestID") items := new(spec.Items) items.Type = "string" tagsParam := spec.QueryParam("tags").CollectionOf(items, fmt) return map[string]spec.Parameter{ "ID": *idParam, "Name": *nameParam, "RequestID": *requestIDParam, "Friend": *friendParam, "Tags": *tagsParam, } } func parametersForJSONRequestSliceParams(fmt string) map[string]spec.Parameter { if fmt == "" { fmt = "csv" } nameParam := spec.QueryParam("name").Typed("string", "") idParam := spec.PathParam("id").Typed("integer", "int64") friendSchema := new(spec.Schema).Typed("object", "") friendParam := spec.BodyParam("friend", spec.ArrayProperty(friendSchema)) requestIDParam := spec.HeaderParam("X-Request-Id").Typed("integer", "int64") requestIDParam.Extensions = spec.Extensions(map[string]interface{}{}) requestIDParam.Extensions.Add("go-name", "RequestID") items := new(spec.Items) items.Type = "string" tagsParam := spec.QueryParam("tags").CollectionOf(items, fmt) return map[string]spec.Parameter{ "ID": *idParam, "Name": *nameParam, "RequestID": *requestIDParam, "Friend": *friendParam, "Tags": *tagsParam, } } func TestRequestBindingDefaultValue(t *testing.T) { confirmed := true name := "thomas" friend := map[string]interface{}{"name": "toby", "age": float64(32)} id, age, score, factor := int64(7575), int32(348), float32(5.309), float64(37.403) requestID := 19394858 tags := []string{"one", "two", "three"} dt1 := time.Date(2014, 8, 9, 0, 0, 0, 0, time.UTC) planned := strfmt.Date(dt1) dt2 := time.Date(2014, 10, 12, 8, 5, 5, 0, time.UTC) delivered := strfmt.DateTime(dt2) uri, _ := url.Parse("http://localhost:8002/hello") defaults := map[string]interface{}{ "id": id, "age": age, "score": score, "factor": factor, "name": name, "friend": friend, "X-Request-Id": requestID, "tags": tags, "confirmed": confirmed, "planned": planned, "delivered": delivered, "picture": []byte("hello"), } op2 := parametersForAllTypes("") op3 := make(map[string]spec.Parameter) for k, p := range op2 { p.Default = defaults[p.Name] op3[k] = p } req, _ := http.NewRequest("POST", uri.String(), bytes.NewBuffer(nil)) req.Header.Set("Content-Type", "application/json") binder := NewUntypedRequestBinder(op3, new(spec.Swagger), strfmt.Default) data := make(map[string]interface{}) err := binder.Bind(req, RouteParams(nil), runtime.JSONConsumer(), &data) assert.NoError(t, err) assert.Equal(t, defaults["id"], data["id"]) assert.Equal(t, name, data["name"]) assert.Equal(t, friend, data["friend"]) assert.EqualValues(t, requestID, data["X-Request-Id"]) assert.Equal(t, tags, data["tags"]) assert.Equal(t, planned, data["planned"]) assert.Equal(t, delivered, data["delivered"]) assert.Equal(t, confirmed, data["confirmed"]) assert.Equal(t, age, data["age"]) assert.Equal(t, factor, data["factor"]) assert.Equal(t, score, data["score"]) assert.Equal(t, "hello", string(data["picture"].(strfmt.Base64))) } func TestRequestBindingForInvalid(t *testing.T) { invalidParam := spec.QueryParam("some") op1 := map[string]spec.Parameter{"Some": *invalidParam} binder := NewUntypedRequestBinder(op1, new(spec.Swagger), strfmt.Default) req, _ := http.NewRequest("GET", "http://localhost:8002/hello?name=the-name", nil) err := binder.Bind(req, nil, new(stubConsumer), new(jsonRequestParams)) assert.Error(t, err) op2 := parametersForJSONRequestParams("") binder = NewUntypedRequestBinder(op2, new(spec.Swagger), strfmt.Default) req, _ = http.NewRequest("POST", "http://localhost:8002/hello/1?name=the-name", bytes.NewBuffer([]byte(`{"name":"toby","age":32}`))) req.Header.Set("Content-Type", "application(") data := jsonRequestParams{} err = binder.Bind(req, RouteParams([]RouteParam{{"id", "1"}}), runtime.JSONConsumer(), &data) assert.Error(t, err) req, _ = http.NewRequest("POST", "http://localhost:8002/hello/1?name=the-name", bytes.NewBuffer([]byte(`{]`))) req.Header.Set("Content-Type", "application/json") data = jsonRequestParams{} err = binder.Bind(req, RouteParams([]RouteParam{{"id", "1"}}), runtime.JSONConsumer(), &data) assert.Error(t, err) invalidMultiParam := spec.HeaderParam("tags").CollectionOf(new(spec.Items), "multi") op3 := map[string]spec.Parameter{"Tags": *invalidMultiParam} binder = NewUntypedRequestBinder(op3, new(spec.Swagger), strfmt.Default) req, _ = http.NewRequest("POST", "http://localhost:8002/hello/1?name=the-name", bytes.NewBuffer([]byte(`{}`))) req.Header.Set("Content-Type", "application/json") data = jsonRequestParams{} err = binder.Bind(req, RouteParams([]RouteParam{{"id", "1"}}), runtime.JSONConsumer(), &data) assert.Error(t, err) invalidMultiParam = spec.PathParam("").CollectionOf(new(spec.Items), "multi") op4 := map[string]spec.Parameter{"Tags": *invalidMultiParam} binder = NewUntypedRequestBinder(op4, new(spec.Swagger), strfmt.Default) req, _ = http.NewRequest("POST", "http://localhost:8002/hello/1?name=the-name", bytes.NewBuffer([]byte(`{}`))) req.Header.Set("Content-Type", "application/json") data = jsonRequestParams{} err = binder.Bind(req, RouteParams([]RouteParam{{"id", "1"}}), runtime.JSONConsumer(), &data) assert.Error(t, err) invalidInParam := spec.HeaderParam("tags").Typed("string", "") invalidInParam.In = "invalid" op5 := map[string]spec.Parameter{"Tags": *invalidInParam} binder = NewUntypedRequestBinder(op5, new(spec.Swagger), strfmt.Default) req, _ = http.NewRequest("POST", "http://localhost:8002/hello/1?name=the-name", bytes.NewBuffer([]byte(`{}`))) req.Header.Set("Content-Type", "application/json") data = jsonRequestParams{} err = binder.Bind(req, RouteParams([]RouteParam{{"id", "1"}}), runtime.JSONConsumer(), &data) assert.Error(t, err) } func TestRequestBindingForValid(t *testing.T) { for _, fmt := range []string{"csv", "pipes", "tsv", "ssv", "multi"} { op1 := parametersForJSONRequestParams(fmt) binder := NewUntypedRequestBinder(op1, new(spec.Swagger), strfmt.Default) lval := []string{"one", "two", "three"} var queryString string var skipEscape bool switch fmt { case "multi": skipEscape = true queryString = strings.Join(lval, "&tags=") case "ssv": queryString = strings.Join(lval, " ") case "pipes": queryString = strings.Join(lval, "|") case "tsv": queryString = strings.Join(lval, "\t") default: queryString = strings.Join(lval, ",") } if !skipEscape { queryString = url.QueryEscape(queryString) } urlStr := "http://localhost:8002/hello/1?name=the-name&tags=" + queryString req, err := http.NewRequest("POST", urlStr, bytes.NewBuffer([]byte(`{"name":"toby","age":32}`))) require.NoError(t, err) req.Header.Set("Content-Type", "application/json;charset=utf-8") req.Header.Set("X-Request-Id", "1325959595") data := jsonRequestParams{} err = binder.Bind(req, RouteParams([]RouteParam{{"id", "1"}}), runtime.JSONConsumer(), &data) expected := jsonRequestParams{ ID: 1, Name: "the-name", Friend: friend{"toby", 32}, RequestID: 1325959595, Tags: []string{"one", "two", "three"}, } assert.NoError(t, err) assert.Equal(t, expected, data) } op1 := parametersForJSONRequestParams("") binder := NewUntypedRequestBinder(op1, new(spec.Swagger), strfmt.Default) urlStr := "http://localhost:8002/hello/1?name=the-name&tags=one,two,three" req, _ := http.NewRequest("POST", urlStr, bytes.NewBuffer([]byte(`{"name":"toby","age":32}`))) req.Header.Set("Content-Type", "application/json;charset=utf-8") req.Header.Set("X-Request-Id", "1325959595") data2 := jsonRequestPtr{} err := binder.Bind(req, []RouteParam{{"id", "1"}}, runtime.JSONConsumer(), &data2) expected2 := jsonRequestPtr{ Friend: &friend{"toby", 32}, Tags: []string{"one", "two", "three"}, } assert.NoError(t, err) if data2.Friend == nil { t.Fatal("friend is nil") } assert.Equal(t, *expected2.Friend, *data2.Friend) assert.Equal(t, expected2.Tags, data2.Tags) req, _ = http.NewRequest("POST", urlStr, bytes.NewBuffer([]byte(`[{"name":"toby","age":32}]`))) req.Header.Set("Content-Type", "application/json;charset=utf-8") req.Header.Set("X-Request-Id", "1325959595") op2 := parametersForJSONRequestSliceParams("") binder = NewUntypedRequestBinder(op2, new(spec.Swagger), strfmt.Default) data3 := jsonRequestSlice{} err = binder.Bind(req, []RouteParam{{"id", "1"}}, runtime.JSONConsumer(), &data3) expected3 := jsonRequestSlice{ Friend: []friend{{"toby", 32}}, Tags: []string{"one", "two", "three"}, } assert.NoError(t, err) assert.Equal(t, expected3.Friend, data3.Friend) assert.Equal(t, expected3.Tags, data3.Tags) } type formRequest struct { Name string Age int } func parametersForFormUpload() map[string]spec.Parameter { nameParam := spec.FormDataParam("name").Typed("string", "") ageParam := spec.FormDataParam("age").Typed("integer", "int32") return map[string]spec.Parameter{"Name": *nameParam, "Age": *ageParam} } func TestFormUpload(t *testing.T) { params := parametersForFormUpload() binder := NewUntypedRequestBinder(params, new(spec.Swagger), strfmt.Default) urlStr := "http://localhost:8002/hello" req, _ := http.NewRequest("POST", urlStr, bytes.NewBufferString(`name=the-name&age=32`)) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") data := formRequest{} res := binder.Bind(req, nil, runtime.JSONConsumer(), &data) assert.NoError(t, res) assert.Equal(t, "the-name", data.Name) assert.Equal(t, 32, data.Age) req, _ = http.NewRequest("POST", urlStr, bytes.NewBufferString(`name=%3&age=32`)) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") data = formRequest{} assert.Error(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data)) } type fileRequest struct { Name string // body File runtime.File // upload } func paramsForFileUpload() *UntypedRequestBinder { nameParam := spec.FormDataParam("name").Typed("string", "") fileParam := spec.FileParam("file").AsRequired() params := map[string]spec.Parameter{"Name": *nameParam, "File": *fileParam} return NewUntypedRequestBinder(params, new(spec.Swagger), strfmt.Default) } func TestBindingFileUpload(t *testing.T) { binder := paramsForFileUpload() body := bytes.NewBuffer(nil) writer := multipart.NewWriter(body) part, err := writer.CreateFormFile("file", "plain-jane.txt") assert.NoError(t, err) _, _ = part.Write([]byte("the file contents")) _ = writer.WriteField("name", "the-name") assert.NoError(t, writer.Close()) urlStr := "http://localhost:8002/hello" req, _ := http.NewRequest("POST", urlStr, body) req.Header.Set("Content-Type", writer.FormDataContentType()) data := fileRequest{} assert.NoError(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data)) assert.Equal(t, "the-name", data.Name) assert.NotNil(t, data.File) assert.NotNil(t, data.File.Header) assert.Equal(t, "plain-jane.txt", data.File.Header.Filename) bb, err := io.ReadAll(data.File.Data) assert.NoError(t, err) assert.Equal(t, []byte("the file contents"), bb) req, _ = http.NewRequest("POST", urlStr, body) req.Header.Set("Content-Type", "application/json") data = fileRequest{} assert.Error(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data)) req, _ = http.NewRequest("POST", urlStr, body) req.Header.Set("Content-Type", "application(") data = fileRequest{} assert.Error(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data)) body = bytes.NewBuffer(nil) writer = multipart.NewWriter(body) part, err = writer.CreateFormFile("bad-name", "plain-jane.txt") assert.NoError(t, err) _, _ = part.Write([]byte("the file contents")) _ = writer.WriteField("name", "the-name") assert.NoError(t, writer.Close()) req, _ = http.NewRequest("POST", urlStr, body) req.Header.Set("Content-Type", writer.FormDataContentType()) data = fileRequest{} assert.Error(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data)) req, _ = http.NewRequest("POST", urlStr, body) req.Header.Set("Content-Type", writer.FormDataContentType()) _, _ = req.MultipartReader() data = fileRequest{} assert.Error(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data)) writer = multipart.NewWriter(body) _ = writer.WriteField("name", "the-name") assert.NoError(t, writer.Close()) req, _ = http.NewRequest("POST", urlStr, body) req.Header.Set("Content-Type", writer.FormDataContentType()) data = fileRequest{} assert.Error(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data)) } func paramsForOptionalFileUpload() *UntypedRequestBinder { nameParam := spec.FormDataParam("name").Typed("string", "") fileParam := spec.FileParam("file").AsOptional() params := map[string]spec.Parameter{"Name": *nameParam, "File": *fileParam} return NewUntypedRequestBinder(params, new(spec.Swagger), strfmt.Default) } func TestBindingOptionalFileUpload(t *testing.T) { binder := paramsForOptionalFileUpload() body := bytes.NewBuffer(nil) writer := multipart.NewWriter(body) _ = writer.WriteField("name", "the-name") assert.NoError(t, writer.Close()) urlStr := "http://localhost:8002/hello" req, _ := http.NewRequest("POST", urlStr, body) req.Header.Set("Content-Type", writer.FormDataContentType()) data := fileRequest{} assert.NoError(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data)) assert.Equal(t, "the-name", data.Name) assert.Nil(t, data.File.Data) assert.Nil(t, data.File.Header) writer = multipart.NewWriter(body) part, err := writer.CreateFormFile("file", "plain-jane.txt") assert.NoError(t, err) _, _ = part.Write([]byte("the file contents")) _ = writer.WriteField("name", "the-name") assert.NoError(t, writer.Close()) req, _ = http.NewRequest("POST", urlStr, body) req.Header.Set("Content-Type", writer.FormDataContentType()) assert.NoError(t, writer.Close()) data = fileRequest{} assert.NoError(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data)) assert.Equal(t, "the-name", data.Name) assert.NotNil(t, data.File) assert.NotNil(t, data.File.Header) assert.Equal(t, "plain-jane.txt", data.File.Header.Filename) bb, err := io.ReadAll(data.File.Data) assert.NoError(t, err) assert.Equal(t, []byte("the file contents"), bb) }