- min and max compare the characters of a string or a number's decimal form and the element count of a slice or map on any field without numeric or integer (Laravel's getSize), with the min.array and max.array messages for lists - blank values, booleans, dates and byte slices are not measured; a malformed bound is an error on every non-numeric field - reverse the 261104-f4x pin, prove it through the cabana settings save sequence, and update the README, docs and the storePassword comment
544 lines
20 KiB
Go
544 lines
20 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)
|
|
}
|
|
}
|
|
}
|