- pl validation.yaml string line is Polish (Winter's pl file leaves it in English); the header notes the deliberate divergence - pl validate.yaml min and max use Winter's numeric wording, since those keys only serve numeric and integer bounds
575 lines
21 KiB
Go
575 lines
21 KiB
Go
package lagoon
|
|
|
|
import (
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestValidateBetweenYear(t *testing.T) {
|
|
rules := map[string]string{"year": "nullable|integer|between:1889,2100"}
|
|
errs, err := Validate(t.Context(), nil, nil, rules, map[string]any{"year": 1700}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(errs["year"]) == 0 {
|
|
t.Fatal("year=1700 must fail")
|
|
}
|
|
errs, err = Validate(t.Context(), nil, nil, rules, map[string]any{"year": nil}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(errs) != 0 {
|
|
t.Fatalf("nil year = %v", errs)
|
|
}
|
|
errs, err = Validate(t.Context(), nil, nil, rules, map[string]any{"year": 1991}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(errs) != 0 {
|
|
t.Fatalf("1991 = %v", errs)
|
|
}
|
|
}
|
|
|
|
func TestValidateIntegerPointerAndJSONNumber(t *testing.T) {
|
|
rules := map[string]string{"year": "nullable|integer|between:1889,2100"}
|
|
year := 1991
|
|
var nilYear *int
|
|
pass := map[string]any{
|
|
"*int": &year,
|
|
"nil *int": nilYear,
|
|
"float64": float64(1991),
|
|
"digit string": "1991",
|
|
}
|
|
for name, val := range pass {
|
|
errs, err := Validate(t.Context(), nil, nil, rules, map[string]any{"year": val}, nil)
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", name, err)
|
|
}
|
|
if len(errs) != 0 {
|
|
t.Fatalf("%s must pass, got %v", name, errs)
|
|
}
|
|
}
|
|
zero := 0
|
|
early := 1700
|
|
fail := map[string]any{
|
|
"fractional float": 1991.5,
|
|
"out of range *int": &early,
|
|
"bool": true,
|
|
"non-digit string": "19x1",
|
|
"int 0": 0,
|
|
"float64 0": float64(0),
|
|
"*int 0": &zero,
|
|
"string 0": "0",
|
|
}
|
|
for name, val := range fail {
|
|
errs, err := Validate(t.Context(), nil, nil, rules, map[string]any{"year": val}, nil)
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", name, err)
|
|
}
|
|
if len(errs["year"]) == 0 {
|
|
t.Fatalf("%s must fail", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidateOneofQuotedSpace(t *testing.T) {
|
|
rules := map[string]string{"format": `nullable|in:LP,2LP,CD,2CD,MC,Box,EP 7"`}
|
|
errs, err := Validate(t.Context(), nil, nil, rules, map[string]any{"format": `EP 7"`}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(errs) != 0 {
|
|
t.Fatalf("EP 7\" rejected: %v", errs)
|
|
}
|
|
errs, err = Validate(t.Context(), nil, nil, rules, map[string]any{"format": "tape"}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(errs["format"]) == 0 {
|
|
t.Fatal("invalid format must fail")
|
|
}
|
|
}
|
|
|
|
func TestValidateMoneyRange(t *testing.T) {
|
|
rules := map[string]string{"market_price_stored": "nullable|numeric|min:0|max:999999.9999"}
|
|
errs, err := Validate(t.Context(), nil, nil, rules, map[string]any{"market_price_stored": "1000000.0000"}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(errs["market_price_stored"]) == 0 {
|
|
t.Fatal("1000000.0000 must fail max")
|
|
}
|
|
errs, err = Validate(t.Context(), nil, nil, rules, map[string]any{"market_price_stored": "25.0000"}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(errs) != 0 {
|
|
t.Fatalf("25.0000 = %v", errs)
|
|
}
|
|
errs, err = Validate(t.Context(), nil, nil, rules, map[string]any{"market_price_stored": 0}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(errs) != 0 {
|
|
t.Fatalf("numeric 0 must pass min:0, got %v", errs)
|
|
}
|
|
errs, err = Validate(t.Context(), nil, nil, rules, map[string]any{"market_price_stored": nil}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(errs) != 0 {
|
|
t.Fatalf("nil money = %v", errs)
|
|
}
|
|
}
|
|
|
|
func TestValidateBooleanNoop(t *testing.T) {
|
|
rules := map[string]string{"search_use_typesense": "boolean"}
|
|
for _, v := range []any{true, false} {
|
|
errs, err := Validate(t.Context(), nil, nil, rules, map[string]any{"search_use_typesense": v}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(errs) != 0 {
|
|
t.Fatalf("boolean %v = %v", v, errs)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidateEmailConfirmedDifferentMimes(t *testing.T) {
|
|
errs, err := Validate(t.Context(), nil, nil, map[string]string{"email": "email"}, map[string]any{"email": "not-an-email"}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(errs["email"]) == 0 {
|
|
t.Fatal("not-an-email must fail")
|
|
}
|
|
errs, err = Validate(t.Context(), nil, nil, map[string]string{"email": "email"}, map[string]any{"email": "a@b.com"}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(errs) != 0 {
|
|
t.Fatalf("a@b.com = %v", errs)
|
|
}
|
|
|
|
rules := map[string]string{"password": "confirmed"}
|
|
errs, err = Validate(t.Context(), nil, nil, rules, map[string]any{
|
|
"password": "secret", "password_confirmation": "secret",
|
|
}, nil)
|
|
if err != nil || len(errs) != 0 {
|
|
t.Fatalf("confirmed match: %v %v", errs, err)
|
|
}
|
|
errs, err = Validate(t.Context(), nil, nil, rules, map[string]any{
|
|
"password": "secret", "password_confirmation": "other",
|
|
}, nil)
|
|
if err != nil || len(errs["password"]) == 0 {
|
|
t.Fatalf("confirmed mismatch: %v %v", errs, err)
|
|
}
|
|
|
|
rules = map[string]string{"password": "different:current_password"}
|
|
errs, err = Validate(t.Context(), nil, nil, rules, map[string]any{
|
|
"password": "same", "current_password": "same",
|
|
}, nil)
|
|
if err != nil || len(errs["password"]) == 0 {
|
|
t.Fatalf("different equal: %v %v", errs, err)
|
|
}
|
|
errs, err = Validate(t.Context(), nil, nil, rules, map[string]any{
|
|
"password": "new", "current_password": "old",
|
|
}, nil)
|
|
if err != nil || len(errs) != 0 {
|
|
t.Fatalf("different distinct: %v %v", errs, err)
|
|
}
|
|
|
|
rules = map[string]string{"avatar": "mimes:jpeg,jpg,png,webp,gif"}
|
|
errs, err = Validate(t.Context(), nil, nil, rules, map[string]any{"avatar": "PNG"}, nil)
|
|
if err != nil || len(errs) != 0 {
|
|
t.Fatalf("png: %v %v", errs, err)
|
|
}
|
|
errs, err = Validate(t.Context(), nil, nil, rules, map[string]any{"avatar": "svg"}, nil)
|
|
if err != nil || len(errs["avatar"]) == 0 {
|
|
t.Fatalf("svg: %v %v", errs, err)
|
|
}
|
|
}
|
|
|
|
func TestValidateUnrecognizedRule(t *testing.T) {
|
|
_, err := Validate(t.Context(), nil, nil, map[string]string{"name": "required|nope"}, map[string]any{"name": "x"}, nil)
|
|
if err == nil || !strings.Contains(err.Error(), "nope") {
|
|
t.Fatalf("want unrecognized nope, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateUniqueRespectsDeletedAt(t *testing.T) {
|
|
sqlDB, _ := dedicatedDB(t, "lagoon_validate_unique")
|
|
gdb, err := Use(t.Context(), sqlDB)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := gdb.Exec(`CREATE TABLE lagoon_unique_rows (
|
|
id SERIAL PRIMARY KEY,
|
|
slug TEXT NOT NULL,
|
|
deleted_at TIMESTAMPTZ
|
|
)`).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := gdb.Exec(`INSERT INTO lagoon_unique_rows (slug, deleted_at) VALUES ('live', NULL), ('gone', NOW())`).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rules := map[string]string{"slug": "unique:lagoon_unique_rows"}
|
|
errs, err := Validate(t.Context(), gdb, nil, rules, map[string]any{"slug": "live"}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(errs["slug"]) == 0 {
|
|
t.Fatal("live slug must fail unique")
|
|
}
|
|
errs, err = Validate(t.Context(), gdb, nil, rules, map[string]any{"slug": "gone"}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(errs) != 0 {
|
|
t.Fatalf("soft-deleted slug should pass unique: %v", errs)
|
|
}
|
|
}
|
|
|
|
func TestValidateNumericRangeMessagePicksFailedBound(t *testing.T) {
|
|
cases := []struct {
|
|
rule string
|
|
val any
|
|
want string
|
|
}{
|
|
{"integer|min:0", -1, "The views must be at least 0."},
|
|
{"integer|max:10", 11, "The views may not be greater than 10."},
|
|
{"numeric|min:0|max:10", "-0.5", "The views must be at least 0."},
|
|
{"numeric|min:0|max:10", "10.5", "The views may not be greater than 10."},
|
|
{"integer|between:0,10", 11, "The views must be between 0 and 10."},
|
|
{"integer|between:0,10", -1, "The views must be between 0 and 10."},
|
|
{"integer|between:0,10|min:2", 1, "The views must be at least 2."},
|
|
}
|
|
for _, c := range cases {
|
|
errs, err := Validate(t.Context(), nil, nil, map[string]string{"views": c.rule}, map[string]any{"views": c.val}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := errs["views"]; len(got) != 1 || got[0] != c.want {
|
|
t.Fatalf("%s with %v = %v, want %q", c.rule, c.val, got, c.want)
|
|
}
|
|
}
|
|
errs, err := Validate(t.Context(), nil, nil, map[string]string{"views": "integer|between:0,10"}, map[string]any{"views": 5}, nil)
|
|
if err != nil || errs != nil {
|
|
t.Fatalf("in range: %v %v", errs, err)
|
|
}
|
|
}
|
|
|
|
func TestValidateNumericRangeMessageTranslated(t *testing.T) {
|
|
tr := requestTranslator(t)
|
|
errs, err := Validate(inLocale("pl"), nil, nil, map[string]string{"year": "integer|between:1889,2100"}, map[string]any{"year": 1700}, tr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := errs["year"]; len(got) != 1 || got[0] != "year musi zawierać się w granicach 1889 - 2100." {
|
|
t.Fatalf("pl between = %v", got)
|
|
}
|
|
}
|
|
|
|
// TestValidateRequiredZeroDates: required rejects a zero time.Time, Date or
|
|
// TimeOfDay and a non-nil pointer to one, accepts set values (midnight
|
|
// included), and keeps the old emptiness of every other type: an int 0, a
|
|
// false bool and a non-date struct are present.
|
|
func TestValidateRequiredZeroDates(t *testing.T) {
|
|
zeroTime, zeroDate, zeroClock := time.Time{}, Date{}, TimeOfDay{}
|
|
setTime, setDate, midnight := time.Date(2026, 10, 2, 0, 0, 0, 0, time.UTC), NewDate(2026, 10, 2), NewTimeOfDay(0, 0, 0)
|
|
type other struct{ A int }
|
|
cases := []struct {
|
|
name string
|
|
value any
|
|
empty bool
|
|
}{
|
|
{"zero time", zeroTime, true},
|
|
{"pointer to zero time", &zeroTime, true},
|
|
{"nil time pointer", (*time.Time)(nil), true},
|
|
{"zero date", zeroDate, true},
|
|
{"pointer to zero date", &zeroDate, true},
|
|
{"nil date pointer", (*Date)(nil), true},
|
|
{"zero clock", zeroClock, true},
|
|
{"pointer to zero clock", &zeroClock, true},
|
|
{"nil clock pointer", (*TimeOfDay)(nil), true},
|
|
{"set time", setTime, false},
|
|
{"pointer to set time", &setTime, false},
|
|
{"set date", setDate, false},
|
|
{"pointer to set date", &setDate, false},
|
|
{"midnight", midnight, false},
|
|
{"pointer to midnight", &midnight, false},
|
|
{"int zero", 0, false},
|
|
{"false", false, false},
|
|
{"zero struct", other{}, false},
|
|
{"empty string", "", true},
|
|
{"nil", nil, true},
|
|
}
|
|
for _, tc := range cases {
|
|
errs, err := Validate(t.Context(), nil, &fillDates{}, map[string]string{"v": "required"}, map[string]any{"v": tc.value}, nil)
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", tc.name, err)
|
|
}
|
|
if got := len(errs["v"]) > 0; got != tc.empty {
|
|
t.Fatalf("%s: required failed=%v, want %v (%v)", tc.name, got, tc.empty, errs)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ruleCase is one value under one rule string with the exact messages
|
|
// lagoon.Validate must report for the field (nil when it passes).
|
|
type ruleCase struct {
|
|
name string
|
|
rule string
|
|
value any
|
|
want []string
|
|
}
|
|
|
|
func runRuleCases(t *testing.T, field string, cases []ruleCase) {
|
|
t.Helper()
|
|
for _, tc := range cases {
|
|
errs, err := Validate(t.Context(), nil, nil, map[string]string{field: tc.rule}, map[string]any{field: tc.value}, nil)
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", tc.name, err)
|
|
}
|
|
if got := errs[field]; !slices.Equal(got, tc.want) {
|
|
t.Fatalf("%s: %q on %#v = %q, want %q", tc.name, tc.rule, tc.value, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestValidateStringRule: string accepts a string, a pointer to one and a
|
|
// named string type, refuses any other value with Laravel's message, and
|
|
// skips an empty or blank value, which is required's job.
|
|
func TestValidateStringRule(t *testing.T) {
|
|
type label string
|
|
empty, x := "", "x"
|
|
const rule = "nullable|string|max:255"
|
|
notString := []string{"The title must be a string."}
|
|
runRuleCases(t, "title", []ruleCase{
|
|
{"plain", rule, "hello", nil},
|
|
{"nil", rule, nil, nil},
|
|
{"empty", rule, "", nil},
|
|
{"blank", rule, " ", nil},
|
|
{"nil pointer", rule, (*string)(nil), nil},
|
|
{"pointer to empty", rule, &empty, nil},
|
|
{"pointer to value", rule, &x, nil},
|
|
{"named type", rule, label("news"), nil},
|
|
{"int", rule, 5, notString},
|
|
{"bool", rule, true, notString},
|
|
{"slice", rule, []string{"a"}, notString},
|
|
{"no nullable empty", "string", "", nil},
|
|
{"no nullable nil", "string", nil, nil},
|
|
{"no nullable int", "string", 5, notString},
|
|
{"required empty", "required|string", "", []string{"The title field is required."}},
|
|
})
|
|
}
|
|
|
|
// TestValidateURLRule: url accepts what Laravel's pattern accepts (the one
|
|
// lagoon.ValidateRequest uses) and skips an empty or blank value.
|
|
func TestValidateURLRule(t *testing.T) {
|
|
empty, nope := "", "nope"
|
|
const rule = "nullable|url|max:500"
|
|
invalid := []string{"The website format is invalid."}
|
|
runRuleCases(t, "website", []ruleCase{
|
|
{"https", rule, "https://example.com/feed.png", nil},
|
|
{"localhost port query fragment", rule, "http://localhost:8080/x?y=1#z", nil},
|
|
{"unicode host and path", rule, "https://przykład.pl/ścieżka", nil},
|
|
{"nil", rule, nil, nil},
|
|
{"empty", rule, "", nil},
|
|
{"blank", rule, " ", nil},
|
|
{"pointer to empty", rule, &empty, nil},
|
|
{"words", rule, "not a url", invalid},
|
|
{"no scheme", rule, "example.com", invalid},
|
|
{"javascript", rule, "javascript:alert(1)", invalid},
|
|
{"leading space", rule, " https://example.com", invalid},
|
|
{"trailing space", rule, "https://example.com ", invalid},
|
|
{"pointer to non-url", rule, &nope, invalid},
|
|
{"int", rule, 42, invalid},
|
|
})
|
|
}
|
|
|
|
// TestValidateStringURLTranslated: with a translator the string and url
|
|
// failures are the lagoon::validate lines of the request locale.
|
|
func TestValidateStringURLTranslated(t *testing.T) {
|
|
tr := requestTranslator(t)
|
|
cases := []struct {
|
|
field, rule string
|
|
value any
|
|
want string
|
|
}{
|
|
{"title", "nullable|string|max:255", 5, "title musi być ciągiem znaków."},
|
|
{"website", "nullable|url|max:500", "not a url", "Format website jest nieprawidłowy."},
|
|
{"title", "nullable|string|max:255", strings.Repeat("a", 256), "title nie może być dłuższy niż 255 znaków."},
|
|
{"title", "string|min:3", "ab", "title musi mieć przynajmniej 3 znaków."},
|
|
}
|
|
for _, tc := range cases {
|
|
errs, err := Validate(inLocale("pl"), nil, nil, map[string]string{tc.field: tc.rule}, map[string]any{tc.field: tc.value}, tr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := errs[tc.field]; len(got) != 1 || got[0] != tc.want {
|
|
t.Fatalf("%s %q = %q, want %q", tc.field, tc.rule, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestValidateStringLength: min and max compare the length in characters
|
|
// (runes, Laravel's mb_strlen) with the string size messages on every field
|
|
// that is not numeric or integer, with or without string or url.
|
|
func TestValidateStringLength(t *testing.T) {
|
|
const longURL = "https://example.com/a-very-long-path/feed.png"
|
|
const longWords = "this is not a url and it is far too long"
|
|
if len(longWords) != 40 {
|
|
t.Fatalf("fixture length %d", len(longWords))
|
|
}
|
|
runRuleCases(t, "title", []ruleCase{
|
|
{"255 ascii", "nullable|string|max:255", strings.Repeat("a", 255), nil},
|
|
{"256 ascii", "nullable|string|max:255", strings.Repeat("a", 256), []string{"The title may not be greater than 255 characters."}},
|
|
{"255 polish letters", "nullable|string|max:255", strings.Repeat("ż", 255), nil},
|
|
{"256 polish letters", "nullable|string|max:255", strings.Repeat("ż", 256), []string{"The title may not be greater than 255 characters."}},
|
|
{"below min", "string|min:3", "ab", []string{"The title must be at least 3 characters."}},
|
|
{"at min", "string|min:3", "abc", nil},
|
|
{"min empty", "string|min:3", "", nil},
|
|
{"min nil", "string|min:3", nil, nil},
|
|
{"required min empty", "required|string|min:3", "", []string{"The title field is required."}},
|
|
{"non-string with max", "string|max:3", 12345, []string{"The title must be a string."}},
|
|
{"max without string", "max:5", "0123456789", []string{"The title may not be greater than 5 characters."}},
|
|
{"nullable max without string", "nullable|max:5", "0123456789", []string{"The title may not be greater than 5 characters."}},
|
|
{"string between keeps its check", "string|between:1,5", "0123456789", []string{"The title must be between 1 and 5 characters."}},
|
|
{"string between passes", "string|between:1,5", "abc", nil},
|
|
})
|
|
runRuleCases(t, "website", []ruleCase{
|
|
{"short url", "nullable|url|max:30", "https://example.com/feed.png", nil},
|
|
{"long url", "nullable|url|max:30", longURL, []string{"The website may not be greater than 30 characters."}},
|
|
{"long non-url", "nullable|url|max:30", longWords, []string{"The website format is invalid.", "The website may not be greater than 30 characters."}},
|
|
})
|
|
for _, v := range []any{"x", nil} {
|
|
_, err := Validate(t.Context(), nil, nil, map[string]string{"title": "string|max:abc"}, map[string]any{"title": v}, nil)
|
|
if err == nil || !strings.Contains(err.Error(), "max:abc") {
|
|
t.Fatalf("malformed max on %#v: err = %v", v, err)
|
|
}
|
|
}
|
|
_, err := Validate(t.Context(), nil, nil, map[string]string{"title": "nullable|string|min:x"}, map[string]any{"title": "abc"}, nil)
|
|
if err == nil || !strings.Contains(err.Error(), "min:x") {
|
|
t.Fatalf("malformed min: err = %v", err)
|
|
}
|
|
}
|
|
|
|
// TestValidateLengthWithoutStringRule: on a field without numeric or integer,
|
|
// min and max are Laravel's getSize: the characters of a string or of a
|
|
// number's decimal form and the element count of a slice or map. Blank
|
|
// values, booleans, dates and byte slices are not measured, and a bound that
|
|
// is not a non-negative whole number is an error whatever the value.
|
|
func TestValidateLengthWithoutStringRule(t *testing.T) {
|
|
empty, long := "", "abcdef"
|
|
tooLong := []string{"The title may not be greater than 5 characters."}
|
|
runRuleCases(t, "title", []ruleCase{
|
|
{"255 polish letters", "required|max:255", strings.Repeat("ż", 255), nil},
|
|
{"256 polish letters", "required|max:255", strings.Repeat("ż", 256), []string{"The title may not be greater than 255 characters."}},
|
|
{"nil", "nullable|max:5", nil, nil},
|
|
{"empty", "nullable|max:5", "", nil},
|
|
{"blank", "nullable|max:5", " ", nil},
|
|
{"nil pointer", "nullable|max:5", (*string)(nil), nil},
|
|
{"pointer to empty", "nullable|max:5", &empty, nil},
|
|
{"at max", "nullable|max:5", "abcde", nil},
|
|
{"over max", "nullable|max:5", "abcdef", tooLong},
|
|
{"pointer over max", "nullable|max:5", &long, tooLong},
|
|
{"below min", "min:4", "abc", []string{"The title must be at least 4 characters."}},
|
|
{"min empty", "min:4", "", nil},
|
|
{"min nil", "min:4", nil, nil},
|
|
{"min blank", "min:4", " ", nil},
|
|
{"required min empty", "required|min:4", "", []string{"The title field is required."}},
|
|
{"email under max", "required|email|max:255", "a@example.com", nil},
|
|
{"int digits over max", "max:4", 12345, []string{"The title may not be greater than 4 characters."}},
|
|
{"int digits at max", "max:4", 2024, nil},
|
|
{"float digits at max", "max:4", float64(2024), nil},
|
|
{"float form over max", "max:2", float64(1.5), []string{"The title may not be greater than 2 characters."}},
|
|
{"bool not measured", "max:0", true, nil},
|
|
{"time not measured", "max:1", time.Date(2026, 11, 4, 12, 0, 0, 0, time.UTC), nil},
|
|
{"date not measured", "max:1", NewDate(2026, 11, 4), nil},
|
|
})
|
|
runRuleCases(t, "tags", []ruleCase{
|
|
{"slice over max", "max:2", []string{"a", "b", "c"}, []string{"The tags may not have more than 2 items."}},
|
|
{"slice at max", "max:2", []any{"a", "b"}, nil},
|
|
{"slice below min", "min:2", []string{"a"}, []string{"The tags must have at least 2 items."}},
|
|
{"empty slice skips", "min:2", []string{}, nil},
|
|
{"map over max", "max:1", map[string]any{"a": 1, "b": 2}, []string{"The tags may not have more than 1 items."}},
|
|
{"bytes not measured", "max:3", []byte("abcdef"), nil},
|
|
})
|
|
runRuleCases(t, "views", []ruleCase{
|
|
{"integer max", "integer|max:10", 11, []string{"The views may not be greater than 10."}},
|
|
{"numeric range", "numeric|min:0|max:10", "5", nil},
|
|
})
|
|
malformed := []struct {
|
|
rule string
|
|
value any
|
|
token string
|
|
}{
|
|
{"max:abc", "x", "max:abc"},
|
|
{"max:abc", nil, "max:abc"},
|
|
{"nullable|min:-1", "abc", "min:-1"},
|
|
}
|
|
for _, tc := range malformed {
|
|
_, err := Validate(t.Context(), nil, nil, map[string]string{"title": tc.rule}, map[string]any{"title": tc.value}, nil)
|
|
if err == nil || !strings.Contains(err.Error(), tc.token) {
|
|
t.Fatalf("%q on %#v: err = %v", tc.rule, tc.value, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestValidateLengthWithoutStringRuleTranslated: the character and item
|
|
// messages are the lagoon::validation lines of the request locale.
|
|
func TestValidateLengthWithoutStringRuleTranslated(t *testing.T) {
|
|
tr := requestTranslator(t)
|
|
cases := []struct {
|
|
field, rule string
|
|
value any
|
|
want string
|
|
}{
|
|
{"title", "max:5", "abcdef", "title nie może być dłuższy niż 5 znaków."},
|
|
{"tags", "max:2", []string{"a", "b", "c"}, "tags nie może mieć więcej niż 2 elementów."},
|
|
}
|
|
for _, tc := range cases {
|
|
errs, err := Validate(inLocale("pl"), nil, nil, map[string]string{tc.field: tc.rule}, map[string]any{tc.field: tc.value}, tr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := errs[tc.field]; len(got) != 1 || got[0] != tc.want {
|
|
t.Fatalf("%s %q = %q, want %q", tc.field, tc.rule, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestValidatePolishStringAndNumericLines: the pl request catalog answers a
|
|
// string failure in Polish, and lagoon.Validate's numeric min and max
|
|
// failures read as numbers, not character counts.
|
|
func TestValidatePolishStringAndNumericLines(t *testing.T) {
|
|
rules := []RequestRule{{Field: "s", Rules: ParseRules("string")}}
|
|
if got := mustValidate(t, inLocale("pl"), map[string]any{"s": 123}, rules)["s"]; !slices.Equal(got, []string{"s musi być ciągiem znaków."}) {
|
|
t.Fatalf("pl string = %q", got)
|
|
}
|
|
if got := mustValidate(t, inLocale("en"), map[string]any{"s": 123}, rules)["s"]; !slices.Equal(got, []string{"The s must be a string."}) {
|
|
t.Fatalf("en string = %q", got)
|
|
}
|
|
tr := requestTranslator(t)
|
|
cases := []struct {
|
|
rule string
|
|
value any
|
|
want string
|
|
}{
|
|
{"integer|max:10", 11, "views nie może być większy niż 10."},
|
|
{"integer|min:0", -1, "views musi być nie mniejszy od 0."},
|
|
}
|
|
for _, tc := range cases {
|
|
errs, err := Validate(inLocale("pl"), nil, nil, map[string]string{"views": tc.rule}, map[string]any{"views": tc.value}, tr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := errs["views"]; !slices.Equal(got, []string{tc.want}) {
|
|
t.Fatalf("%q on %v = %q, want %q", tc.rule, tc.value, got, tc.want)
|
|
}
|
|
}
|
|
}
|