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  }