105 lines
3.7 KiB
Go
105 lines
3.7 KiB
Go
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")
|
|
}
|
|
}
|