223 lines
7.1 KiB
Go
223 lines
7.1 KiB
Go
package contract
|
|
|
|
import (
|
|
"encoding/json"
|
|
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
type flexibleID int64
|
|
|
|
func (v flexibleID) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(strconv.FormatInt(int64(v), 10))
|
|
}
|
|
func (v *flexibleID) UnmarshalJSON(b []byte) error {
|
|
var s string
|
|
if len(b) > 0 && b[0] == '"' {
|
|
if err := json.Unmarshal(b, &s); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
s = string(b)
|
|
}
|
|
return v.UnmarshalText([]byte(s))
|
|
}
|
|
func (v *flexibleID) UnmarshalText(b []byte) error {
|
|
n, err := strconv.ParseInt(string(b), 10, 64)
|
|
if err == nil {
|
|
*v = flexibleID(n)
|
|
}
|
|
return err
|
|
}
|
|
func TestDynamicJSONAndCustomScalars(t *testing.T) {
|
|
type request struct {
|
|
ID flexibleID `json:"id"`
|
|
Dynamic any `json:"dynamic"`
|
|
IDs []flexibleID `json:"ids,optional"`
|
|
}
|
|
scalar := ScalarOf[flexibleID]("string|int64", "string", true)
|
|
b, err := New[request](scalar)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, id := range []string{`"9223372036854775807"`, `9223372036854775807`} {
|
|
r := httptest.NewRequest("POST", "/", strings.NewReader(`{"id":`+id+`,"dynamic":{"n":9223372036854775807},"ids":["1",2]}`))
|
|
r.Header.Set("Content-Type", "application/json")
|
|
var req request
|
|
if err = b.Bind(r, nil, &req); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if req.ID != flexibleID(9223372036854775807) || req.Dynamic.(map[string]any)["n"] != json.Number("9223372036854775807") {
|
|
t.Fatalf("precision: %+v", req)
|
|
}
|
|
encoded, _ := json.Marshal(req)
|
|
if !strings.Contains(string(encoded), `"id":"9223372036854775807"`) {
|
|
t.Fatal(string(encoded))
|
|
}
|
|
}
|
|
for _, body := range []string{`{"id":true,"dynamic":null}`, `{"id":1.1,"dynamic":null}`, `{"id":9223372036854775808,"dynamic":null}`, `{"id":1,"dynamic":{"x":1,"x":2}}`, `{"id":1,"dynamic":null,"unexpected":1}`} {
|
|
r := httptest.NewRequest("POST", "/", strings.NewReader(body))
|
|
r.Header.Set("Content-Type", "application/json")
|
|
req := request{ID: 7}
|
|
if b.Bind(r, nil, &req) == nil || req.ID != 7 {
|
|
t.Fatalf("accepted or mutated %s", body)
|
|
}
|
|
}
|
|
for _, value := range []string{`null`, `[1,"x"]`, `false`, `"text"`} {
|
|
r := httptest.NewRequest("POST", "/", strings.NewReader(`{"id":1,"dynamic":`+value+`}`))
|
|
r.Header.Set("Content-Type", "application/json")
|
|
if err := b.Bind(r, nil, &request{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if _, err := New[request](); err == nil {
|
|
t.Fatal("implicit custom type registration")
|
|
}
|
|
response, err := b.ResponseSchema()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if response.Properties["id"].Type != "string" || response.Properties["ids"].Items.Type != "string" || len(b.BodySchema().Properties["id"].AnyOf) != 2 {
|
|
t.Fatal("input/output schemas conflated")
|
|
}
|
|
type path struct {
|
|
ID flexibleID `path:"id"`
|
|
}
|
|
p, err := New[path](scalar)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var req path
|
|
if err := p.Bind(httptest.NewRequest("GET", "/", nil), map[string]string{"id": "9223372036854775807"}, &req); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if p.Parameters()[0].Schema.Type != "string" {
|
|
t.Fatal("text parameters need text schema")
|
|
}
|
|
if _, err := New[path](ScalarOf[flexibleID]("string|int64", "string", false)); err == nil {
|
|
t.Fatal("text support not explicit")
|
|
}
|
|
schemaOnly, err := Describe(reflect.TypeOf(request{}), scalar)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if schemaOnly.Bind(httptest.NewRequest("POST", "/", nil), nil, &request{}) == nil {
|
|
t.Fatal("schema-only binder executed")
|
|
}
|
|
}
|
|
|
|
func TestScalarPointersCollectionsAndRegistration(t *testing.T) {
|
|
scalar := ScalarOf[flexibleID]("string|int64", "string", true)
|
|
type nullable struct {
|
|
ID *flexibleID `json:"id,optional"`
|
|
Values map[string]*flexibleID `json:"values,optional"`
|
|
}
|
|
b, err := New[nullable](scalar)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, body := range []string{`{}`, `{"id":null}`, `{"id":1,"values":{"a":"2","b":null}}`} {
|
|
r := httptest.NewRequest("POST", "/", strings.NewReader(body))
|
|
r.Header.Set("Content-Type", "application/json")
|
|
if err := b.Bind(r, nil, &nullable{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
input := b.BodySchema().Properties["id"]
|
|
if len(input.AnyOf) != 3 || !input.AnyOf[2].Nullable || len(input.AnyOf[2].Enum) != 1 || input.AnyOf[2].Enum[0] != nil {
|
|
t.Fatal("nullable input union does not describe null")
|
|
}
|
|
out, err := b.ResponseSchema()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if out.Properties["id"].Type != "string" || !out.Properties["id"].Nullable {
|
|
t.Fatal("nullable output type")
|
|
}
|
|
type query struct {
|
|
IDs []flexibleID `form:"ids"`
|
|
}
|
|
q, err := New[query](scalar)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var req query
|
|
if err := q.Bind(httptest.NewRequest("GET", "/?ids=1&ids=2", nil), nil, &req); err != nil || !reflect.DeepEqual(req.IDs, []flexibleID{1, 2}) {
|
|
t.Fatalf("query scalar collection: %+v %v", req, err)
|
|
}
|
|
if q.Parameters()[0].Schema.Items.Type != "string" {
|
|
t.Fatal("text array schema")
|
|
}
|
|
if _, err := New[query](ScalarOf[flexibleID]("string|int64", "string", false)); err == nil {
|
|
t.Fatal("array bypassed text declaration")
|
|
}
|
|
if err := ValidateScalars(ScalarOf[int64]("int64", "string", true)); err == nil {
|
|
t.Fatal("missing methods accepted")
|
|
}
|
|
if err := ValidateScalars(scalar, scalar); err == nil {
|
|
t.Fatal("duplicate registration")
|
|
}
|
|
type strict struct {
|
|
ID int64 `json:"id"`
|
|
}
|
|
strictBinder := MustNew[strict]()
|
|
r := httptest.NewRequest("POST", "/", strings.NewReader(`{"id":"1"}`))
|
|
r.Header.Set("Content-Type", "application/json")
|
|
if strictBinder.Bind(r, nil, &strict{}) == nil {
|
|
t.Fatal("other binder globally loosened")
|
|
}
|
|
}
|
|
|
|
type panicScalar string
|
|
|
|
func (v panicScalar) MarshalJSON() ([]byte, error) { return json.Marshal(string(v)) }
|
|
func (v *panicScalar) UnmarshalJSON([]byte) error { panic("decoder failure") }
|
|
func TestScalarPanicIsBindingError(t *testing.T) {
|
|
type req struct {
|
|
Value panicScalar `json:"value"`
|
|
}
|
|
b := MustNew[req](ScalarOf[panicScalar]("string", "string", false))
|
|
r := httptest.NewRequest("POST", "/", strings.NewReader(`{"value":"bad"}`))
|
|
r.Header.Set("Content-Type", "application/json")
|
|
dst := req{Value: "original"}
|
|
if err := b.Bind(r, nil, &dst); err == nil || dst.Value != "original" {
|
|
t.Fatalf("decoder panic: %+v %v", dst, err)
|
|
}
|
|
}
|
|
|
|
func TestLimitedJSONTrailingWhitespace(t *testing.T) {
|
|
type req struct {
|
|
Value any `json:"value"`
|
|
}
|
|
b := MustNew[req]()
|
|
r := httptest.NewRequest("POST", "/", strings.NewReader(`{"value":null}`+strings.Repeat(" ", 65)))
|
|
r.Header.Set("Content-Type", "application/json")
|
|
r.ContentLength = -1
|
|
r.Body = http.MaxBytesReader(httptest.NewRecorder(), r.Body, 64)
|
|
err := b.Bind(r, nil, &req{}, 64)
|
|
if apierrors.From(err).Status != 413 {
|
|
t.Fatalf("tail limit error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestResponseSchemaPreservesOrdinaryPointerConstraints(t *testing.T) {
|
|
type dto struct {
|
|
Count *int `json:"count,optional,range=[1:10]"`
|
|
Choice *string `json:"choice,options=a|b"`
|
|
Values *[]int `json:"values,optional" binding:"max=3"`
|
|
}
|
|
b := MustNew[dto]()
|
|
response, err := b.ResponseSchema()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(response, b.BodySchema()) {
|
|
t.Fatalf("changed ordinary response constraints: %#v", response)
|
|
}
|
|
}
|