package contract import ( "bytes" "context" "errors" apierrors "git.yuntuo.hk/k2safe/go-omni/errors" "io" "net/http" "net/http/httptest" "strings" "testing" ) type byteReader struct{ r io.Reader } func (r byteReader) Read(p []byte) (int, error) { if len(p) > 1 { p = p[:1] } return r.r.Read(p) } func TestBodyPolicyPropagatesOuterLimit(t *testing.T) { for _, limit := range []int64{0, 1, 4} { r := httptest.NewRequest("POST", "/", byteReader{strings.NewReader("{} ")}) r.Header.Set("Content-Type", "application/json") r.Body = http.MaxBytesReader(httptest.NewRecorder(), r.Body, limit) err := MustNew[struct{}]().BindWithPolicy(r, nil, &struct{}{}, RequestPolicy{Body: "required", UTF8: "reject"}) var e *apierrors.APIError if !errors.As(err, &e) || e.Status != 413 { t.Fatalf("limit %d: expected 413, got %v", limit, err) } } } func TestStrictRequestPolicy(t *testing.T) { type request struct { Name string `json:"name"` Tags []string `form:"tag,optional"` } b := MustNew[request]() strict := RequestPolicy{UnknownQuery: "reject", UTF8: "reject", Charset: "utf8"} for _, tc := range []struct { name, url, ct, body string valid bool }{ {"valid", "/?tag=a&tag=b", "application/json; charset=UTF-8", `{"name":"企业😀"}`, true}, {"query", "/?typo=1", "application/json", `{"name":"ok"}`, false}, {"charset", "/", "application/json; charset=gbk", `{"name":"ok"}`, false}, {"utf8", "/", "application/json", "{\"name\":\"\xff\"}", false}, {"truncatedRune", "/", "application/json", "{\"name\":\"\xe4\"}", false}, {"tail", "/", "application/json", `{"name":"ok"} {}`, false}, {"tailInvalidByte", "/", "application/json", "{\"name\":\"ok\"} \xff", false}, } { t.Run(tc.name, func(t *testing.T) { r := httptest.NewRequest("POST", tc.url, byteReader{strings.NewReader(tc.body)}) r.Header.Set("Content-Type", tc.ct) var dst request err := b.BindWithPolicy(r, nil, &dst, strict) if (err == nil) != tc.valid { t.Fatal(err) } if tc.valid && len(dst.Tags) != 2 { t.Fatal("query slice rejected") } }) } // Explicit route overrides inherit the rest of the App policy. r := httptest.NewRequest("POST", "/?typo=1", strings.NewReader(`{"name":"ok"}`)) r.Header.Set("Content-Type", "application/json") r = r.WithContext(WithRequestPolicy(context.Background(), strict)) if err := b.BindWithPolicy(r, nil, &request{}, RequestPolicy{UnknownQuery: "ignore"}); err != nil { t.Fatal(err) } } func TestBodyPresencePolicies(t *testing.T) { b := MustNew[struct{}]() for _, mode := range []string{"none", "emptyObject", "required", "optional"} { for _, body := range []string{"", "{}", "null", `{"unexpected":1}`, "{} {}", " "} { r := httptest.NewRequest("POST", "/", byteReader{strings.NewReader(body)}) r.Header.Set("Content-Type", "application/json") err := b.BindWithPolicy(r, nil, &struct{}{}, RequestPolicy{Body: mode, UTF8: "reject"}) valid := (mode == "none" && body == "") || (mode != "none" && body == "{}") || (mode == "optional" && body == "") if (err == nil) != valid { t.Fatalf("mode=%s body=%q: %v", mode, body, err) } } } type object struct { Value string `json:"value"` } r := httptest.NewRequest("POST", "/", bytes.NewReader(nil)) if err := MustNew[object]().BindWithPolicy(r, nil, &object{}, RequestPolicy{Body: "optional"}); err != nil { t.Fatal(err) } // Chunked/unknown size is still bounded under strict UTF-8 checking. r = httptest.NewRequest("POST", "/", byteReader{strings.NewReader(`{"value":"1234567890"}`)}) r.Header.Set("Content-Type", "application/json") if err := MustNew[object]().BindWithPolicy(r, nil, &object{}, RequestPolicy{UTF8: "reject"}, 8); err == nil { t.Fatal("chunked over-limit accepted") } }