Files
warmbly/internal/errx/bind_test.go
T

99 lines
4.2 KiB
Go

package errx
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type bindSend struct {
To []string `json:"to" binding:"required"`
Subject string `json:"subject" binding:"required"`
Mode string `json:"send_mode" binding:"omitempty,oneof=instant smart"`
Limit int `json:"limit" binding:"omitempty,max=10"`
Inner struct {
Name string `json:"name" binding:"required"`
} `json:"inner"`
}
type bindContact struct {
Email string `json:"email" binding:"required"`
Due *time.Time `json:"due"`
Campaign uuid.UUID `json:"campaign"`
Tags []uuid.UUID `json:"tags"`
}
func bindBody(t *testing.T, body string, dst any) error {
t.Helper()
gin.SetMode(gin.TestMode)
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request = httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body))
c.Request.Header.Set("Content-Type", "application/json")
return c.ShouldBindJSON(dst)
}
func TestInvalidBody(t *testing.T) {
cases := []struct {
name string
body string
dst func() any
want []string
}{
{"empty", "", func() any { return &bindSend{} }, []string{"request body is empty"}},
{"syntax", `{"to": [}`, func() any { return &bindSend{} }, []string{"not valid JSON", "at byte 9"}},
{"truncated", `{"to": ["a"`, func() any { return &bindSend{} }, []string{"ends before the value is complete"}},
{"required fields on an empty object", `{}`, func() any { return &bindSend{} }, []string{`"to" is required`, `"subject" is required`, `"inner.name" is required`}},
{"oneof", `{"to":["a"],"subject":"s","send_mode":"later","inner":{"name":"n"}}`, func() any { return &bindSend{} }, []string{`"send_mode" must be one of: instant, smart`}},
{"max", `{"to":["a"],"subject":"s","limit":11,"inner":{"name":"n"}}`, func() any { return &bindSend{} }, []string{`"limit" must be at most 10`}},
{"field of the wrong type", `{"to":"a@b.co","subject":"s"}`, func() any { return &bindSend{} }, []string{`Field "to" must be a JSON array, not a JSON string`}},
{"nested field of the wrong type", `{"to":["a"],"subject":"s","inner":{"name":5}}`, func() any { return &bindSend{} }, []string{`Field "inner.name" must be a JSON string, not a JSON number`}},
{"object where an array is expected", `{"email":"a@b.co"}`, func() any { return &[]bindContact{} }, []string{"must be a JSON array, not a JSON object"}},
{"array where an object is expected", `[]`, func() any { return &bindSend{} }, []string{"must be a JSON object, not a JSON array"}},
{"slice element validation", `[{"email":"a@b.co"},{}]`, func() any { return &[]bindContact{} }, []string{`"email" is required`}},
{"uuid", `{"email":"a@b.co","campaign":"nope"}`, func() any { return &bindContact{} }, []string{"should be a UUID"}},
{"time", `{"email":"a@b.co","due":"tomorrow"}`, func() any { return &bindContact{} }, []string{"not RFC 3339", `"tomorrow"`}},
{"uuid of the wrong JSON type", `{"email":"a@b.co","campaign":5}`, func() any { return &bindContact{} }, []string{`Field "campaign" must be a JSON string, not a JSON number`}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := bindBody(t, tc.body, tc.dst())
if err == nil {
t.Fatalf("bind of %q succeeded, want an error", tc.body)
}
got := InvalidBody(err)
if got.Code != BadRequest {
t.Fatalf("code = %d, want %d", got.Code, BadRequest)
}
if strings.Contains(got.Message, "malformed JSON") {
t.Fatalf("message still says malformed JSON: %q", got.Message)
}
for _, w := range tc.want {
if !strings.Contains(got.Message, w) {
t.Errorf("message %q does not contain %q", got.Message, w)
}
}
})
}
}
func TestInvalidBodyCapsTheList(t *testing.T) {
type many struct {
A string `json:"a" binding:"required"`
B string `json:"b" binding:"required"`
C string `json:"c" binding:"required"`
D string `json:"d" binding:"required"`
E string `json:"e" binding:"required"`
F string `json:"f" binding:"required"`
G string `json:"g" binding:"required"`
}
got := InvalidBody(bindBody(t, `{}`, &many{}))
if !strings.Contains(got.Message, "and 2 more") || strings.Contains(got.Message, `"f"`) {
t.Fatalf("message = %q, want five fields and a count of the rest", got.Message)
}
}