github.com/machinefi/w3bstream@v1.6.5-rc9.0.20240426031326-b8c7c4876e72/pkg/depends/kit/httptransport/z_req_tsfm_test.go (about) 1 package httptransport_test 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "mime/multipart" 8 "net/http" 9 "net/http/httputil" 10 "reflect" 11 "regexp" 12 "sort" 13 "strconv" 14 "testing" 15 "time" 16 17 . "github.com/onsi/gomega" 18 pkgerr "github.com/pkg/errors" 19 20 . "github.com/machinefi/w3bstream/pkg/depends/kit/httptransport" 21 "github.com/machinefi/w3bstream/pkg/depends/kit/httptransport/httpx" 22 "github.com/machinefi/w3bstream/pkg/depends/kit/httptransport/transformer" 23 "github.com/machinefi/w3bstream/pkg/depends/kit/statusx" 24 vldterr "github.com/machinefi/w3bstream/pkg/depends/kit/validator/errors" 25 "github.com/machinefi/w3bstream/pkg/depends/testutil/httptransporttestutil/server/pkg/types" 26 "github.com/machinefi/w3bstream/pkg/depends/x/reflectx" 27 ) 28 29 var regexpContentTypeWithBoundary = regexp.MustCompile(`Content-Type: multipart/form-data; boundary=([A-Za-z0-9]+)`) 30 31 func UnifyRequestData(data []byte) []byte { 32 data = bytes.Replace(data, []byte("\r\n"), []byte("\n"), -1) 33 if regexpContentTypeWithBoundary.Match(data) { 34 matches := regexpContentTypeWithBoundary.FindAllSubmatch(data, 1) 35 data = bytes.Replace(data, matches[0][1], []byte("boundary1"), -1) 36 } 37 return data 38 } 39 40 // openapi:strfmt date-time 41 type Datetime time.Time 42 43 func (dt Datetime) IsZero() bool { 44 unix := time.Time(dt).Unix() 45 return unix == 0 || unix == (time.Time{}).Unix() 46 } 47 48 func (dt Datetime) MarshalText() ([]byte, error) { 49 str := time.Time(dt).Format(time.RFC3339) 50 return []byte(str), nil 51 } 52 53 func (dt *Datetime) UnmarshalText(data []byte) error { 54 if len(data) != 0 { 55 return nil 56 } 57 t, err := time.Parse(time.RFC3339, string(data)) 58 if err != nil { 59 return err 60 } 61 *dt = Datetime(t) 62 return nil 63 } 64 65 func TestRequestTsfm(t *testing.T) { 66 factory := NewRequestTsfmFactory(nil, nil) 67 68 type Headers struct { 69 HInt int `in:"header"` 70 HString string `in:"header"` 71 HBool bool `in:"header"` 72 } 73 74 type Queries struct { 75 QInt int `name:"int" in:"query"` 76 QEmptyInt int `name:"emptyInt,omitempty" in:"query"` 77 QString string `name:"string" in:"query"` 78 QSlice []string `name:"slice" in:"query"` 79 QBytes []byte `name:"bytes,omitempty" in:"query"` 80 StartedAt *Datetime `name:"startedAt,omitempty" in:"query"` 81 QBytesOmitEmpty []byte `name:"bytesOmit,omitempty" in:"query"` 82 } 83 84 type Cookies struct { 85 CString string `name:"a" in:"cookie"` 86 CSlice []string `name:"slice" in:"cookie"` 87 } 88 89 type Data struct { 90 A string `json:",omitempty" xml:",omitempty"` 91 B string `json:",omitempty" xml:",omitempty"` 92 C string `json:",omitempty" xml:",omitempty"` 93 } 94 95 type FormDataMultipart struct { 96 Bytes []byte `name:"bytes"` 97 A []int `name:"a"` 98 C uint `name:"c" ` 99 Data Data `name:"data"` 100 101 File *multipart.FileHeader `name:"file"` 102 Files []*multipart.FileHeader `name:"files"` 103 } 104 105 cases := []struct { 106 name string 107 path string 108 expect string 109 req interface{} 110 }{ 111 { 112 "FullInParameters", 113 "/:id", 114 `GET /1?bytes=Ynl0ZXM%3D&int=1&slice=1&slice=2&string=string HTTP/1.1 115 Content-Type: application/json; charset=utf-8 116 Cookie: a=xxx; slice=1; slice=2 117 Hbool: true 118 Hint: 1 119 Hstring: string 120 121 {} 122 `, 123 &struct { 124 Headers 125 Queries 126 Cookies 127 Data `in:"body"` 128 ID string `name:"id" in:"path"` 129 }{ 130 ID: "1", 131 Headers: Headers{ 132 HInt: 1, 133 HString: "string", 134 HBool: true, 135 }, 136 Queries: Queries{ 137 QInt: 1, 138 QString: "string", 139 QSlice: []string{"1", "2"}, 140 QBytes: []byte("bytes"), 141 }, 142 Cookies: Cookies{ 143 CString: "xxx", 144 CSlice: []string{"1", "2"}, 145 }, 146 }, 147 }, 148 { 149 "URLEncoded", 150 "/", 151 `GET / HTTP/1.1 152 Content-Type: application/x-www-form-urlencoded; param=value 153 154 int=1&slice=1&slice=2&string=string`, 155 &struct { 156 Queries `in:"body" mime:"urlencoded"` 157 }{ 158 Queries: Queries{ 159 QInt: 1, 160 QString: "string", 161 QSlice: []string{"1", "2"}, 162 }, 163 }, 164 }, 165 { 166 "XML", 167 "/", 168 `GET / HTTP/1.1 169 Content-Type: application/xml; charset=utf-8 170 171 <Data><A>1</A></Data>`, 172 &struct { 173 Data `in:"body" mime:"xml"` 174 }{ 175 Data: Data{ 176 A: "1", 177 }, 178 }, 179 }, 180 { 181 "form-data/multipart", 182 "/", 183 `GET / HTTP/1.1 184 Content-Type: multipart/form-data; boundary=5eaf397248958ac38281d1c034e1ad0d4a5f7d986d4c53ac32e8399cbcda 185 186 --5eaf397248958ac38281d1c034e1ad0d4a5f7d986d4c53ac32e8399cbcda 187 Content-Disposition: form-data; name="bytes" 188 Content-Type: text/plain; charset=utf-8 189 190 Ynl0ZXM= 191 --5eaf397248958ac38281d1c034e1ad0d4a5f7d986d4c53ac32e8399cbcda 192 Content-Disposition: form-data; name="a" 193 Content-Type: text/plain; charset=utf-8 194 195 -1 196 --5eaf397248958ac38281d1c034e1ad0d4a5f7d986d4c53ac32e8399cbcda 197 Content-Disposition: form-data; name="a" 198 Content-Type: text/plain; charset=utf-8 199 200 1 201 --5eaf397248958ac38281d1c034e1ad0d4a5f7d986d4c53ac32e8399cbcda 202 Content-Disposition: form-data; name="c" 203 Content-Type: text/plain; charset=utf-8 204 205 1 206 --5eaf397248958ac38281d1c034e1ad0d4a5f7d986d4c53ac32e8399cbcda 207 Content-Disposition: form-data; name="data" 208 Content-Type: application/json; charset=utf-8 209 210 {"A":"1"} 211 212 --5eaf397248958ac38281d1c034e1ad0d4a5f7d986d4c53ac32e8399cbcda 213 Content-Disposition: form-data; name="file"; filename="file.text" 214 Content-Type: application/octet-stream 215 216 test 217 --5eaf397248958ac38281d1c034e1ad0d4a5f7d986d4c53ac32e8399cbcda 218 Content-Disposition: form-data; name="files"; filename="file1.text" 219 Content-Type: application/octet-stream 220 221 test1 222 --5eaf397248958ac38281d1c034e1ad0d4a5f7d986d4c53ac32e8399cbcda 223 Content-Disposition: form-data; name="files"; filename="file2.text" 224 Content-Type: application/octet-stream 225 226 test2 227 --5eaf397248958ac38281d1c034e1ad0d4a5f7d986d4c53ac32e8399cbcda-- 228 `, 229 &struct { 230 FormDataMultipart `in:"body" mime:"multipart" boundary:"boundary1"` 231 }{ 232 FormDataMultipart: FormDataMultipart{ 233 A: []int{-1, 1}, 234 C: 1, 235 Bytes: []byte("bytes"), 236 Data: Data{ 237 A: "1", 238 }, 239 Files: []*multipart.FileHeader{ 240 transformer.MustNewFileHeader("files", "file1.text", bytes.NewBufferString("test1")), 241 transformer.MustNewFileHeader("files", "file2.text", bytes.NewBufferString("test2")), 242 }, 243 File: transformer.MustNewFileHeader("file", "file.text", bytes.NewBufferString("test")), 244 }, 245 }, 246 }, 247 } 248 249 for _, c := range cases { 250 t.Run(c.name, func(t *testing.T) { 251 for i := 0; i < 5; i++ { 252 rt, err := factory.NewRequestTsfm(bgctx, reflect.TypeOf(c.req)) 253 NewWithT(t).Expect(err).To(BeNil()) 254 255 req, err := rt.NewRequest(http.MethodGet, c.path, c.req) 256 NewWithT(t).Expect(err).To(BeNil()) 257 258 data, _ := httputil.DumpRequest(req, true) 259 NewWithT(t).Expect(string(UnifyRequestData(data))). 260 To(Equal(string(UnifyRequestData([]byte(c.expect))))) 261 262 rv := reflectx.New(reflect.PtrTo(reflectx.DeRef(reflect.TypeOf(c.req)))) 263 err2 := rt.DecodeAndValidate(bgctx, httpx.NewRequestInfo(req), rv) 264 NewWithT(t).Expect(err2).To(BeNil()) 265 NewWithT(t).Expect(reflectx.Indirect(rv).Interface()). 266 To(Equal(reflectx.Indirect(reflect.ValueOf(c.req)).Interface())) 267 } 268 }) 269 } 270 } 271 272 func ExampleNewRequestTsfmFactory() { 273 factory := NewRequestTsfmFactory(nil, nil) 274 275 type PlainBody struct { 276 A string `json:"a" validate:"@string[2,]"` 277 Int int `json:"int,omitempty" default:"0" validate:"@int[0,]"` 278 } 279 280 type Req struct { 281 Protocol types.Protocol `in:"query" name:"protocol,omitempty" default:"HTTP"` 282 QString string `in:"query" name:"string,omitempty" default:"s"` 283 PlainBody PlainBody `in:"body" mime:"plain" validate:"@struct<json>"` 284 } 285 286 req := &Req{} 287 req.PlainBody.A = "1" 288 289 rt, err := factory.NewRequestTsfm(bgctx, reflect.TypeOf(req)) 290 if err != nil { 291 panic(err) 292 } 293 294 statusErr := rt.Params["body"][0].Validator.Validate(req.PlainBody) 295 296 statusErr.(*vldterr.ErrorSet).Each(func(fieldErr *vldterr.FieldError) { 297 fmt.Println(fieldErr.Field, strconv.Quote(fieldErr.Error.Error())) 298 }) 299 // Output: 300 // a "string length should be larger than 2, but got invalid value 1" 301 } 302 303 func TestRequestTsfm_DecodeFromRequestInfo_WithDefaults(t *testing.T) { 304 type Data struct { 305 String string `json:"string,omitempty" default:"111" validate:"@string[3,]"` 306 Int int `json:"int,omitempty" default:"111" validate:"@int[3,]"` 307 } 308 309 type Req struct { 310 Protocol types.Protocol `in:"query" name:"protocol,omitempty" default:"HTTP"` 311 QInt int `in:"query" name:"int,omitempty" default:"1"` 312 QString string `in:"query" name:"string,omitempty" default:"s"` 313 List []Data `in:"body"` 314 } 315 316 factory := NewRequestTsfmFactory(nil, nil) 317 318 rt, err := factory.NewRequestTsfm(bgctx, reflect.TypeOf(&Req{})) 319 NewWithT(t).Expect(err).To(BeNil()) 320 if err != nil { 321 return 322 } 323 324 req, err := rt.NewRequest(http.MethodGet, "/", &Req{ 325 List: []Data{ 326 { 327 String: "2222", 328 }, 329 {}, 330 }, 331 }) 332 NewWithT(t).Expect(err).To(BeNil()) 333 334 r := &Req{} 335 err = rt.DecodeAndValidate(bgctx, httpx.NewRequestInfo(req), r) 336 NewWithT(t).Expect(err).To(BeNil()) 337 NewWithT(t).Expect(r).To(Equal(&Req{ 338 Protocol: types.PROTOCOL__HTTP, 339 QInt: 1, 340 QString: "s", 341 List: []Data{ 342 { 343 String: "2222", 344 Int: 111, 345 }, 346 { 347 String: "111", 348 Int: 111, 349 }, 350 }, 351 })) 352 } 353 354 func TestRequestTsfm_DecodeFromRequestInfo_WithEnumValidate(t *testing.T) { 355 type Req struct { 356 Protocol types.Protocol `name:"protocol,omitempty" validate:"@string{HTTP}" in:"query" default:"HTTP"` 357 } 358 359 factory := NewRequestTsfmFactory(nil, nil) 360 361 rt, err := factory.NewRequestTsfm(bgctx, reflect.TypeOf(&Req{})) 362 NewWithT(t).Expect(err).To(BeNil()) 363 364 req, err := rt.NewRequest(http.MethodGet, "/", &Req{types.PROTOCOL__HTTP}) 365 NewWithT(t).Expect(err).To(BeNil()) 366 367 r := &Req{} 368 err = rt.DecodeAndValidate(bgctx, httpx.NewRequestInfo(req), r) 369 NewWithT(t).Expect(err).To(BeNil()) 370 NewWithT(t).Expect(r).To(Equal(&Req{types.PROTOCOL__HTTP})) 371 } 372 373 func TestRequestTsfm_DecodeFromRequestInfo_Failed(t *testing.T) { 374 factory := NewRequestTsfmFactory(nil, nil) 375 376 type NestedForFailed struct { 377 A string `json:"a" validate:"@string[1,]" errMsg:"A wrong"` 378 B string `name:"b" validate:"@string[1,]" default:"1" ` 379 C string `json:"c" validate:"@string[2,]?"` 380 } 381 382 type DataForFailed struct { 383 A string ` validate:"@string[1,]"` 384 B string ` validate:"@string[1,]" default:"1" ` 385 C string `json:"c" validate:"@string[2,]?"` 386 NestedForFailed NestedForFailed 387 } 388 389 type ReqForFailed struct { 390 ID string `in:"path" name:"id" validate:"@string[2,]"` 391 QString string `in:"query" name:"string,omitempty" validate:"@string[2,]" default:"11" ` 392 QSlice []string `in:"query" name:"slice,omitempty" validate:"@slice<@string[2,]>[2,]"` 393 DataForFailed `in:"body"` 394 } 395 396 rt, err := factory.NewRequestTsfm(bgctx, reflect.TypeOf(&ReqForFailed{})) 397 if err != nil { 398 return 399 } 400 401 req, err := rt.NewRequest(http.MethodGet, "/:id", &ReqForFailed{ 402 ID: "1", 403 QString: "!", 404 QSlice: []string{"11", "1"}, 405 DataForFailed: DataForFailed{C: "1"}, 406 }) 407 if err != nil { 408 return 409 } 410 411 e := rt.DecodeAndValidate(bgctx, httpx.NewRequestInfo(req), &ReqForFailed{}) 412 if e == nil { 413 return 414 } 415 416 errFields := e.(*statusx.StatusErr).Fields 417 418 sort.Slice(errFields, func(i, j int) bool { 419 return errFields[i].Field < errFields[j].Field 420 }) 421 422 data, _ := json.MarshalIndent(errFields, "", " ") 423 424 NewWithT(t).Expect(string(data)).To(Equal(`[ 425 { 426 "field": "A", 427 "msg": "missing required field", 428 "in": "body" 429 }, 430 { 431 "field": "B", 432 "msg": "missing required field", 433 "in": "body" 434 }, 435 { 436 "field": "NestedForFailed.B", 437 "msg": "missing required field", 438 "in": "body" 439 }, 440 { 441 "field": "NestedForFailed.a", 442 "msg": "A wrong", 443 "in": "body" 444 }, 445 { 446 "field": "c", 447 "msg": "string length should be larger than 2, but got invalid value 1", 448 "in": "body" 449 }, 450 { 451 "field": "id", 452 "msg": "string length should be larger than 2, but got invalid value 1", 453 "in": "path" 454 }, 455 { 456 "field": "slice[1]", 457 "msg": "string length should be larger than 2, but got invalid value 1", 458 "in": "query" 459 }, 460 { 461 "field": "string", 462 "msg": "string length should be larger than 2, but got invalid value 1", 463 "in": "query" 464 } 465 ]`)) 466 } 467 468 type ReqWithPostValidate struct { 469 StartedAt string `in:"query"` 470 } 471 472 func (ReqWithPostValidate) PostValidate(badRequest BadRequestError) { 473 badRequest.AddErr(pkgerr.Errorf("ops"), "query", "StartedAt") 474 } 475 476 func ExampleRequestTsfm_DecodeAndValidate_RequestInfo_FailedOfPost() { 477 factory := NewRequestTsfmFactory(nil, nil) 478 479 rt, err := factory.NewRequestTsfm(bgctx, reflect.TypeOf(&ReqWithPostValidate{})) 480 if err != nil { 481 return 482 } 483 484 req, err := rt.NewRequest(http.MethodPost, "/:id", &ReqWithPostValidate{}) 485 if err != nil { 486 return 487 } 488 489 e := rt.DecodeAndValidate(bgctx, httpx.NewRequestInfo(req), &ReqWithPostValidate{}) 490 if e == nil { 491 return 492 } 493 494 errFields := e.(*statusx.StatusErr).Fields 495 496 sort.Slice(errFields, func(i, j int) bool { 497 return errFields[i].Field < errFields[j].Field 498 }) 499 500 for _, ef := range errFields { 501 fmt.Println(ef) 502 } 503 // Output: 504 // StartedAt in query - missing required field 505 // StartedAt in query - ops 506 }