- lagoon.ValidateRequest ports Laravel 9 request validation: wildcard expansion, implicit-rule stop, bail, sometimes/nullable/blank skipping, size messages split by type and character-counted string lengths - ParseRules, In, CustomRule, UploadedFile and ErrorKeys for rule tables - pl/en lagoon::validation catalogs ported verbatim from WinterCMS - lagoon.Validate answers a numeric range failure with the bound that failed (min, max or numeric between) instead of always max
272 lines
8.2 KiB
Go
272 lines
8.2 KiB
Go
package lagoon
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
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)
|
|
}
|
|
}
|