1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package middleware
16
17 import (
18 "bytes"
19 "context"
20 "io"
21 "mime/multipart"
22 "net/http"
23 "net/url"
24 "strings"
25 "testing"
26 "time"
27
28 "github.com/go-openapi/runtime"
29 "github.com/go-openapi/spec"
30 "github.com/go-openapi/strfmt"
31 "github.com/stretchr/testify/assert"
32 "github.com/stretchr/testify/require"
33 )
34
35 const (
36 csvFormat = "csv"
37 testURL = "http://localhost:8002/hello"
38 )
39
40 type stubConsumer struct {
41 }
42
43 func (s *stubConsumer) Consume(_ io.Reader, _ interface{}) error {
44 return nil
45 }
46
47 type friend struct {
48 Name string `json:"name"`
49 Age int `json:"age"`
50 }
51
52 type jsonRequestParams struct {
53 ID int64
54 Name string
55 Friend friend
56 RequestID int64
57 Tags []string
58 }
59
60 type jsonRequestPtr struct {
61 ID int64
62 Name string
63 RequestID int64
64 Tags []string
65 Friend *friend
66 }
67
68 type jsonRequestSlice struct {
69 ID int64
70 Name string
71 RequestID int64
72 Tags []string
73 Friend []friend
74 }
75
76 func parametersForAllTypes(fmt string) map[string]spec.Parameter {
77 if fmt == "" {
78 fmt = csvFormat
79 }
80 nameParam := spec.QueryParam("name").Typed(typeString, "")
81 idParam := spec.PathParam("id").Typed("integer", "int64")
82 ageParam := spec.QueryParam("age").Typed("integer", "int32")
83 scoreParam := spec.QueryParam("score").Typed("number", "float")
84 factorParam := spec.QueryParam("factor").Typed("number", "double")
85
86 friendSchema := new(spec.Schema).Typed("object", "")
87 friendParam := spec.BodyParam("friend", friendSchema)
88
89 requestIDParam := spec.HeaderParam("X-Request-Id").Typed("integer", "int64")
90 requestIDParam.Extensions = spec.Extensions(map[string]interface{}{})
91 requestIDParam.Extensions.Add("go-name", "RequestID")
92
93 items := new(spec.Items)
94 items.Type = typeString
95 tagsParam := spec.QueryParam("tags").CollectionOf(items, fmt)
96
97 confirmedParam := spec.QueryParam("confirmed").Typed("boolean", "")
98 plannedParam := spec.QueryParam("planned").Typed(typeString, "date")
99 deliveredParam := spec.QueryParam("delivered").Typed(typeString, "date-time")
100 pictureParam := spec.QueryParam("picture").Typed(typeString, "byte")
101
102 return map[string]spec.Parameter{
103 "ID": *idParam,
104 "Name": *nameParam,
105 "RequestID": *requestIDParam,
106 "Friend": *friendParam,
107 "Tags": *tagsParam,
108 "Age": *ageParam,
109 "Score": *scoreParam,
110 "Factor": *factorParam,
111 "Confirmed": *confirmedParam,
112 "Planned": *plannedParam,
113 "Delivered": *deliveredParam,
114 "Picture": *pictureParam,
115 }
116 }
117
118 func parametersForJSONRequestParams(fmt string) map[string]spec.Parameter {
119 if fmt == "" {
120 fmt = csvFormat
121 }
122 nameParam := spec.QueryParam("name").Typed(typeString, "")
123 idParam := spec.PathParam("id").Typed("integer", "int64")
124
125 friendSchema := new(spec.Schema).Typed("object", "")
126 friendParam := spec.BodyParam("friend", friendSchema)
127
128 requestIDParam := spec.HeaderParam("X-Request-Id").Typed("integer", "int64")
129 requestIDParam.Extensions = spec.Extensions(map[string]interface{}{})
130 requestIDParam.Extensions.Add("go-name", "RequestID")
131
132 items := new(spec.Items)
133 items.Type = typeString
134 tagsParam := spec.QueryParam("tags").CollectionOf(items, fmt)
135
136 return map[string]spec.Parameter{
137 "ID": *idParam,
138 "Name": *nameParam,
139 "RequestID": *requestIDParam,
140 "Friend": *friendParam,
141 "Tags": *tagsParam,
142 }
143 }
144 func parametersForJSONRequestSliceParams(fmt string) map[string]spec.Parameter {
145 if fmt == "" {
146 fmt = csvFormat
147 }
148 nameParam := spec.QueryParam("name").Typed(typeString, "")
149 idParam := spec.PathParam("id").Typed("integer", "int64")
150
151 friendSchema := new(spec.Schema).Typed("object", "")
152 friendParam := spec.BodyParam("friend", spec.ArrayProperty(friendSchema))
153
154 requestIDParam := spec.HeaderParam("X-Request-Id").Typed("integer", "int64")
155 requestIDParam.Extensions = spec.Extensions(map[string]interface{}{})
156 requestIDParam.Extensions.Add("go-name", "RequestID")
157
158 items := new(spec.Items)
159 items.Type = typeString
160 tagsParam := spec.QueryParam("tags").CollectionOf(items, fmt)
161
162 return map[string]spec.Parameter{
163 "ID": *idParam,
164 "Name": *nameParam,
165 "RequestID": *requestIDParam,
166 "Friend": *friendParam,
167 "Tags": *tagsParam,
168 }
169 }
170
171 func TestRequestBindingDefaultValue(t *testing.T) {
172 confirmed := true
173 name := "thomas"
174 friend := map[string]interface{}{"name": "toby", "age": float64(32)}
175 id, age, score, factor := int64(7575), int32(348), float32(5.309), float64(37.403)
176 requestID := 19394858
177 tags := []string{"one", "two", "three"}
178 dt1 := time.Date(2014, 8, 9, 0, 0, 0, 0, time.UTC)
179 planned := strfmt.Date(dt1)
180 dt2 := time.Date(2014, 10, 12, 8, 5, 5, 0, time.UTC)
181 delivered := strfmt.DateTime(dt2)
182 uri, err := url.Parse(testURL)
183 require.NoError(t, err)
184 defaults := map[string]interface{}{
185 "id": id,
186 "age": age,
187 "score": score,
188 "factor": factor,
189 "name": name,
190 "friend": friend,
191 "X-Request-Id": requestID,
192 "tags": tags,
193 "confirmed": confirmed,
194 "planned": planned,
195 "delivered": delivered,
196 "picture": []byte("hello"),
197 }
198 op2 := parametersForAllTypes("")
199 op3 := make(map[string]spec.Parameter)
200 for k, p := range op2 {
201 p.Default = defaults[p.Name]
202 op3[k] = p
203 }
204
205 req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, uri.String(), bytes.NewBuffer(nil))
206 require.NoError(t, err)
207 req.Header.Set("Content-Type", "application/json")
208 binder := NewUntypedRequestBinder(op3, new(spec.Swagger), strfmt.Default)
209
210 data := make(map[string]interface{})
211 err = binder.Bind(req, RouteParams(nil), runtime.JSONConsumer(), &data)
212 require.NoError(t, err)
213 assert.Equal(t, defaults["id"], data["id"])
214 assert.Equal(t, name, data["name"])
215 assert.Equal(t, friend, data["friend"])
216 assert.EqualValues(t, requestID, data["X-Request-Id"])
217 assert.Equal(t, tags, data["tags"])
218 assert.Equal(t, planned, data["planned"])
219 assert.Equal(t, delivered, data["delivered"])
220 assert.Equal(t, confirmed, data["confirmed"])
221 assert.Equal(t, age, data["age"])
222 assert.InDelta(t, factor, data["factor"], 1e-6)
223 assert.InDelta(t, score, data["score"], 1e-6)
224 assert.Equal(t, "hello", string(data["picture"].(strfmt.Base64)))
225 }
226
227 func TestRequestBindingForInvalid(t *testing.T) {
228 invalidParam := spec.QueryParam("some")
229
230 op1 := map[string]spec.Parameter{"Some": *invalidParam}
231
232 binder := NewUntypedRequestBinder(op1, new(spec.Swagger), strfmt.Default)
233 req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://localhost:8002/hello?name=the-name", nil)
234 require.NoError(t, err)
235
236 err = binder.Bind(req, nil, new(stubConsumer), new(jsonRequestParams))
237 require.Error(t, err)
238
239 op2 := parametersForJSONRequestParams("")
240 binder = NewUntypedRequestBinder(op2, new(spec.Swagger), strfmt.Default)
241
242 req, err = http.NewRequestWithContext(context.Background(), http.MethodPost, "http://localhost:8002/hello/1?name=the-name", bytes.NewBufferString(`{"name":"toby","age":32}`))
243 require.NoError(t, err)
244 req.Header.Set("Content-Type", "application(")
245 data := jsonRequestParams{}
246 err = binder.Bind(req, RouteParams([]RouteParam{{"id", "1"}}), runtime.JSONConsumer(), &data)
247 require.Error(t, err)
248
249 req, err = http.NewRequestWithContext(context.Background(), http.MethodPost, "http://localhost:8002/hello/1?name=the-name", bytes.NewBufferString(`{]`))
250 require.NoError(t, err)
251 req.Header.Set("Content-Type", "application/json")
252 data = jsonRequestParams{}
253 err = binder.Bind(req, RouteParams([]RouteParam{{"id", "1"}}), runtime.JSONConsumer(), &data)
254 require.Error(t, err)
255
256 invalidMultiParam := spec.HeaderParam("tags").CollectionOf(new(spec.Items), "multi")
257 op3 := map[string]spec.Parameter{"Tags": *invalidMultiParam}
258 binder = NewUntypedRequestBinder(op3, new(spec.Swagger), strfmt.Default)
259
260 req, err = http.NewRequestWithContext(context.Background(), http.MethodPost, "http://localhost:8002/hello/1?name=the-name", bytes.NewBufferString(`{}`))
261 require.NoError(t, err)
262 req.Header.Set("Content-Type", "application/json")
263 data = jsonRequestParams{}
264 err = binder.Bind(req, RouteParams([]RouteParam{{"id", "1"}}), runtime.JSONConsumer(), &data)
265 require.Error(t, err)
266
267 invalidMultiParam = spec.PathParam("").CollectionOf(new(spec.Items), "multi")
268
269 op4 := map[string]spec.Parameter{"Tags": *invalidMultiParam}
270 binder = NewUntypedRequestBinder(op4, new(spec.Swagger), strfmt.Default)
271
272 req, err = http.NewRequestWithContext(context.Background(), http.MethodPost, "http://localhost:8002/hello/1?name=the-name", bytes.NewBufferString(`{}`))
273 require.NoError(t, err)
274 req.Header.Set("Content-Type", "application/json")
275 data = jsonRequestParams{}
276 err = binder.Bind(req, RouteParams([]RouteParam{{"id", "1"}}), runtime.JSONConsumer(), &data)
277 require.Error(t, err)
278
279 invalidInParam := spec.HeaderParam("tags").Typed(typeString, "")
280 invalidInParam.In = "invalid"
281 op5 := map[string]spec.Parameter{"Tags": *invalidInParam}
282 binder = NewUntypedRequestBinder(op5, new(spec.Swagger), strfmt.Default)
283
284 req, err = http.NewRequestWithContext(context.Background(), http.MethodPost, "http://localhost:8002/hello/1?name=the-name", bytes.NewBufferString(`{}`))
285 require.NoError(t, err)
286 req.Header.Set("Content-Type", "application/json")
287 data = jsonRequestParams{}
288 err = binder.Bind(req, RouteParams([]RouteParam{{"id", "1"}}), runtime.JSONConsumer(), &data)
289 require.Error(t, err)
290 }
291
292 func TestRequestBindingForValid(t *testing.T) {
293 for _, fmt := range []string{csvFormat, "pipes", "tsv", "ssv", "multi"} {
294 op1 := parametersForJSONRequestParams(fmt)
295
296 binder := NewUntypedRequestBinder(op1, new(spec.Swagger), strfmt.Default)
297
298 lval := []string{"one", "two", "three"}
299 var queryString string
300 var skipEscape bool
301 switch fmt {
302 case "multi":
303 skipEscape = true
304 queryString = strings.Join(lval, "&tags=")
305 case "ssv":
306 queryString = strings.Join(lval, " ")
307 case "pipes":
308 queryString = strings.Join(lval, "|")
309 case "tsv":
310 queryString = strings.Join(lval, "\t")
311 default:
312 queryString = strings.Join(lval, ",")
313 }
314 if !skipEscape {
315 queryString = url.QueryEscape(queryString)
316 }
317
318 urlStr := "http://localhost:8002/hello/1?name=the-name&tags=" + queryString
319
320 req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, urlStr, bytes.NewBufferString(`{"name":"toby","age":32}`))
321 require.NoError(t, err)
322 req.Header.Set("Content-Type", "application/json;charset=utf-8")
323 req.Header.Set("X-Request-Id", "1325959595")
324
325 data := jsonRequestParams{}
326 err = binder.Bind(req, RouteParams([]RouteParam{{"id", "1"}}), runtime.JSONConsumer(), &data)
327
328 expected := jsonRequestParams{
329 ID: 1,
330 Name: "the-name",
331 Friend: friend{"toby", 32},
332 RequestID: 1325959595,
333 Tags: []string{"one", "two", "three"},
334 }
335 require.NoError(t, err)
336 assert.Equal(t, expected, data)
337 }
338
339 op1 := parametersForJSONRequestParams("")
340
341 binder := NewUntypedRequestBinder(op1, new(spec.Swagger), strfmt.Default)
342 urlStr := "http://localhost:8002/hello/1?name=the-name&tags=one,two,three"
343 req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, urlStr, bytes.NewBufferString(`{"name":"toby","age":32}`))
344 require.NoError(t, err)
345 req.Header.Set("Content-Type", "application/json;charset=utf-8")
346 req.Header.Set("X-Request-Id", "1325959595")
347
348 data2 := jsonRequestPtr{}
349 err = binder.Bind(req, []RouteParam{{"id", "1"}}, runtime.JSONConsumer(), &data2)
350
351 expected2 := jsonRequestPtr{
352 Friend: &friend{"toby", 32},
353 Tags: []string{"one", "two", "three"},
354 }
355 require.NoError(t, err)
356 if data2.Friend == nil {
357 t.Fatal("friend is nil")
358 }
359 assert.Equal(t, *expected2.Friend, *data2.Friend)
360 assert.Equal(t, expected2.Tags, data2.Tags)
361
362 req, err = http.NewRequestWithContext(context.Background(), http.MethodPost, urlStr, bytes.NewBufferString(`[{"name":"toby","age":32}]`))
363 require.NoError(t, err)
364 req.Header.Set("Content-Type", "application/json;charset=utf-8")
365 req.Header.Set("X-Request-Id", "1325959595")
366 op2 := parametersForJSONRequestSliceParams("")
367 binder = NewUntypedRequestBinder(op2, new(spec.Swagger), strfmt.Default)
368 data3 := jsonRequestSlice{}
369 err = binder.Bind(req, []RouteParam{{"id", "1"}}, runtime.JSONConsumer(), &data3)
370
371 expected3 := jsonRequestSlice{
372 Friend: []friend{{"toby", 32}},
373 Tags: []string{"one", "two", "three"},
374 }
375 require.NoError(t, err)
376 assert.Equal(t, expected3.Friend, data3.Friend)
377 assert.Equal(t, expected3.Tags, data3.Tags)
378 }
379
380 type formRequest struct {
381 Name string
382 Age int
383 }
384
385 func parametersForFormUpload() map[string]spec.Parameter {
386 nameParam := spec.FormDataParam("name").Typed(typeString, "")
387
388 ageParam := spec.FormDataParam("age").Typed("integer", "int32")
389
390 return map[string]spec.Parameter{"Name": *nameParam, "Age": *ageParam}
391 }
392
393 func TestFormUpload(t *testing.T) {
394 params := parametersForFormUpload()
395 binder := NewUntypedRequestBinder(params, new(spec.Swagger), strfmt.Default)
396
397 urlStr := testURL
398 req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, urlStr, bytes.NewBufferString(`name=the-name&age=32`))
399 require.NoError(t, err)
400 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
401
402 data := formRequest{}
403 res := binder.Bind(req, nil, runtime.JSONConsumer(), &data)
404 require.NoError(t, res)
405 assert.Equal(t, "the-name", data.Name)
406 assert.Equal(t, 32, data.Age)
407
408 req, err = http.NewRequestWithContext(context.Background(), http.MethodPost, urlStr, bytes.NewBufferString(`name=%3&age=32`))
409 require.NoError(t, err)
410 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
411
412 data = formRequest{}
413 require.Error(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data))
414 }
415
416 type fileRequest struct {
417 Name string
418 File runtime.File
419 }
420
421 func paramsForFileUpload() *UntypedRequestBinder {
422 nameParam := spec.FormDataParam("name").Typed(typeString, "")
423
424 fileParam := spec.FileParam("file").AsRequired()
425
426 params := map[string]spec.Parameter{"Name": *nameParam, "File": *fileParam}
427 return NewUntypedRequestBinder(params, new(spec.Swagger), strfmt.Default)
428 }
429
430 func TestBindingFileUpload(t *testing.T) {
431 binder := paramsForFileUpload()
432
433 body := bytes.NewBuffer(nil)
434 writer := multipart.NewWriter(body)
435 part, err := writer.CreateFormFile("file", "plain-jane.txt")
436 require.NoError(t, err)
437
438 _, err = part.Write([]byte("the file contents"))
439 require.NoError(t, err)
440 require.NoError(t, writer.WriteField("name", "the-name"))
441 require.NoError(t, writer.Close())
442
443 urlStr := testURL
444 req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, urlStr, body)
445 require.NoError(t, err)
446 req.Header.Set("Content-Type", writer.FormDataContentType())
447
448 data := fileRequest{}
449 require.NoError(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data))
450 assert.Equal(t, "the-name", data.Name)
451 assert.NotNil(t, data.File)
452 assert.NotNil(t, data.File.Header)
453 assert.Equal(t, "plain-jane.txt", data.File.Header.Filename)
454
455 bb, err := io.ReadAll(data.File.Data)
456 require.NoError(t, err)
457 assert.Equal(t, []byte("the file contents"), bb)
458
459 req, err = http.NewRequestWithContext(context.Background(), http.MethodPost, urlStr, body)
460 require.NoError(t, err)
461 req.Header.Set("Content-Type", "application/json")
462 data = fileRequest{}
463 require.Error(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data))
464
465 req, err = http.NewRequestWithContext(context.Background(), http.MethodPost, urlStr, body)
466 require.NoError(t, err)
467 req.Header.Set("Content-Type", "application(")
468 data = fileRequest{}
469 require.Error(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data))
470
471 body = bytes.NewBuffer(nil)
472 writer = multipart.NewWriter(body)
473 part, err = writer.CreateFormFile("bad-name", "plain-jane.txt")
474 require.NoError(t, err)
475
476 _, err = part.Write([]byte("the file contents"))
477 require.NoError(t, err)
478 require.NoError(t, writer.WriteField("name", "the-name"))
479 require.NoError(t, writer.Close())
480 req, err = http.NewRequestWithContext(context.Background(), http.MethodPost, urlStr, body)
481 require.NoError(t, err)
482 req.Header.Set("Content-Type", writer.FormDataContentType())
483
484 data = fileRequest{}
485 require.Error(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data))
486
487 req, err = http.NewRequestWithContext(context.Background(), http.MethodPost, urlStr, body)
488 require.NoError(t, err)
489 req.Header.Set("Content-Type", writer.FormDataContentType())
490 _, err = req.MultipartReader()
491 require.NoError(t, err)
492
493 data = fileRequest{}
494 require.Error(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data))
495
496 writer = multipart.NewWriter(body)
497 require.NoError(t, writer.WriteField("name", "the-name"))
498 require.NoError(t, writer.Close())
499
500 req, err = http.NewRequestWithContext(context.Background(), http.MethodPost, urlStr, body)
501 require.NoError(t, err)
502 req.Header.Set("Content-Type", writer.FormDataContentType())
503
504 data = fileRequest{}
505 require.Error(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data))
506 }
507
508 func paramsForOptionalFileUpload() *UntypedRequestBinder {
509 nameParam := spec.FormDataParam("name").Typed(typeString, "")
510 fileParam := spec.FileParam("file").AsOptional()
511
512 params := map[string]spec.Parameter{"Name": *nameParam, "File": *fileParam}
513 return NewUntypedRequestBinder(params, new(spec.Swagger), strfmt.Default)
514 }
515
516 func TestBindingOptionalFileUpload(t *testing.T) {
517 binder := paramsForOptionalFileUpload()
518
519 body := bytes.NewBuffer(nil)
520 writer := multipart.NewWriter(body)
521 require.NoError(t, writer.WriteField("name", "the-name"))
522 require.NoError(t, writer.Close())
523
524 urlStr := testURL
525 req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, urlStr, body)
526 require.NoError(t, err)
527 req.Header.Set("Content-Type", writer.FormDataContentType())
528
529 data := fileRequest{}
530 require.NoError(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data))
531 assert.Equal(t, "the-name", data.Name)
532 assert.Nil(t, data.File.Data)
533 assert.Nil(t, data.File.Header)
534
535 writer = multipart.NewWriter(body)
536 part, err := writer.CreateFormFile("file", "plain-jane.txt")
537 require.NoError(t, err)
538 _, err = part.Write([]byte("the file contents"))
539 require.NoError(t, err)
540 require.NoError(t, writer.WriteField("name", "the-name"))
541 require.NoError(t, writer.Close())
542
543 req, err = http.NewRequestWithContext(context.Background(), http.MethodPost, urlStr, body)
544 require.NoError(t, err)
545 req.Header.Set("Content-Type", writer.FormDataContentType())
546 require.NoError(t, writer.Close())
547
548 data = fileRequest{}
549 require.NoError(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data))
550 assert.Equal(t, "the-name", data.Name)
551 assert.NotNil(t, data.File)
552 assert.NotNil(t, data.File.Header)
553 assert.Equal(t, "plain-jane.txt", data.File.Header.Filename)
554
555 bb, err := io.ReadAll(data.File.Data)
556 require.NoError(t, err)
557 assert.Equal(t, []byte("the file contents"), bb)
558 }
559
View as plain text