Files
go-omni/contract/policy_test.go

118 lines
4.1 KiB
Go

package contract
import (
"bytes"
"context"
"encoding/json"
"errors"
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"testing/iotest"
)
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 TestJSONClosingDelimiterPreservesReaderCause(t *testing.T) {
cause := errors.New("body reader failed")
for _, prefix := range []string{"{", "[", `{"value":[]`, `[{}`} {
reader := io.MultiReader(byteReader{strings.NewReader(prefix)}, iotest.ErrReader(cause))
_, err := decodeValue(json.NewDecoder(reader), 0)
if !errors.Is(err, cause) {
t.Fatalf("prefix %q: lost reader cause: %v", prefix, 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")
}
}