Files
readeck/pkg/forms/validators_test.go

643 lines
15 KiB
Go

// SPDX-FileCopyrightText: © 2024 Olivier Meunier <olivier@neokraft.net>
//
// SPDX-License-Identifier: AGPL-3.0-only
package forms_test
import (
"context"
"errors"
"strconv"
"testing"
"github.com/stretchr/testify/require"
"codeberg.org/readeck/readeck/pkg/forms"
)
type fieldValidatorTest struct {
f forms.Field
data string
expect any
errors []error
}
func (test fieldValidatorTest) assert(assert *require.Assertions) {
_ = test.f.UnmarshalJSON([]byte(test.data))
valid := test.f.IsValid()
if len(test.errors) > 0 {
assert.False(valid)
assert.Len(test.f.Errors(), len(test.errors))
for i, e := range test.f.Errors() {
assert.EqualError(e, test.errors[i].Error())
}
} else {
assert.True(valid)
}
assert.Equal(test.expect, test.f.Value())
}
func runValidatorTests(tests []fieldValidatorTest) func(t *testing.T) {
return func(t *testing.T) {
for i, test := range tests {
t.Run(strconv.Itoa(i+1), func(t *testing.T) {
test.assert(require.New(t))
})
}
}
}
func TestValidators(t *testing.T) {
t.Run("required", runValidatorTests([]fieldValidatorTest{
{
f: forms.NewTextField("", forms.Required),
data: `null`,
expect: nil,
errors: []error{forms.ErrRequired},
},
{
f: forms.NewTextField("", forms.Required),
data: `""`,
expect: "",
errors: []error{forms.ErrRequired},
},
{
f: forms.NewTextField("", forms.Trim, forms.Required),
data: `" \t \r\n"`,
expect: "",
errors: []error{forms.ErrRequired},
},
{
f: forms.NewTextField("", forms.Required),
data: `"abc"`,
expect: "abc",
},
{
f: forms.NewIntegerField("", forms.Required, forms.Gte(10)),
data: `null`,
expect: nil,
errors: []error{forms.ErrRequired},
},
}))
t.Run("required on list", runValidatorTests([]fieldValidatorTest{
{
f: forms.NewTextListField("", forms.Required),
data: `null`,
expect: nil,
errors: []error{forms.ErrRequired},
},
{
f: forms.NewTextListField("", forms.Required),
data: `[]`,
expect: []string(nil),
errors: []error{forms.ErrRequired},
},
{
f: forms.NewTextListField("", forms.Required),
data: `[""]`,
expect: []string{""},
},
{
f: forms.NewTextListField("", forms.Trim, forms.Required),
data: `[" ", "\t \n"]`,
expect: []string{"", ""},
},
}))
t.Run("required or null", runValidatorTests([]fieldValidatorTest{
{
f: forms.NewTextField("", forms.RequiredOrNil),
data: `""`,
expect: "",
errors: []error{forms.ErrRequired},
},
{
f: forms.NewTextField("", forms.Trim, forms.RequiredOrNil),
data: `" \t \r\n"`,
expect: "",
errors: []error{forms.ErrRequired},
},
{
f: forms.NewBooleanField("", forms.RequiredOrNil),
data: `null`,
expect: nil,
},
{
f: forms.NewBooleanField("", forms.RequiredOrNil),
data: `false`,
expect: false,
},
}))
t.Run("skip", runValidatorTests([]fieldValidatorTest{
{
f: forms.NewTextField("", forms.Skip, forms.IsEmail),
data: `null`,
expect: nil,
},
{
f: forms.NewTextField("", forms.Skip, forms.IsEmail),
data: `""`,
expect: "",
},
{
f: forms.NewTextField("", forms.Skip, forms.IsEmail),
data: `"alice@example.net"`,
expect: "alice@example.net",
},
{
f: forms.NewTextField("", forms.Skip, forms.IsEmail),
data: `"alice"`,
expect: "alice",
errors: []error{forms.ErrInvalidEmail},
},
}))
t.Run("when", runValidatorTests([]fieldValidatorTest{
{
f: forms.NewTextField("", forms.When(func(_ forms.Field, _ string) bool {
return true
}).True(forms.Required)),
data: `null`,
expect: nil,
errors: []error{forms.ErrRequired},
},
{
f: forms.NewTextField("", forms.When(func(_ forms.Field, _ string) bool {
return false
}).True(forms.Required)),
data: `null`,
expect: nil,
},
{
f: forms.NewTextField("",
forms.When(func(_ forms.Field, _ string) bool {
return true
}).
True(forms.Required),
forms.IsEmail,
),
data: `""`,
expect: "",
errors: []error{forms.ErrRequired},
},
{
f: forms.NewTextField("",
forms.When(func(_ forms.Field, _ string) bool {
return false
}).
True(forms.Required),
forms.IsEmail,
),
data: `""`,
expect: "",
errors: []error{forms.ErrInvalidEmail},
},
}))
t.Run("optional", runValidatorTests([]fieldValidatorTest{
{
f: forms.NewTextField("", forms.Optional[string](forms.IsEmail)),
data: `null`,
expect: nil,
},
{
f: forms.NewTextField("", forms.Optional[string](forms.IsEmail)),
data: `""`,
expect: "",
},
{
f: forms.NewTextField("", forms.Optional[string](forms.IsEmail)),
data: `"alice@example.net"`,
expect: "alice@example.net",
},
{
f: forms.NewTextField("", forms.Optional[string](forms.IsEmail)),
data: `"alice"`,
expect: "alice",
errors: []error{forms.ErrInvalidEmail},
},
}))
t.Run("email", runValidatorTests([]fieldValidatorTest{
{
f: forms.NewTextField("", forms.IsEmail),
data: `"test@example.org"`,
expect: "test@example.org",
},
{
f: forms.NewTextField("", forms.IsEmail),
data: `"test@example@.org"`,
expect: "test@example@.org",
errors: []error{forms.ErrInvalidEmail},
},
{
f: forms.NewTextField("", forms.IsEmail),
data: `"test@192.168.0.1"`,
expect: "test@192.168.0.1",
errors: []error{forms.ErrInvalidEmail},
},
{
f: forms.NewTextField("", forms.IsEmail),
data: `"@test"`,
expect: "@test",
errors: []error{forms.ErrInvalidEmail},
},
{
f: forms.NewTextField("", forms.IsEmail),
data: `"test@"`,
expect: "test@",
errors: []error{forms.ErrInvalidEmail},
},
{
f: forms.NewTextField("", forms.IsEmail),
data: `"foo"`,
expect: "foo",
errors: []error{forms.ErrInvalidEmail},
},
{
f: forms.NewTextField("", forms.IsEmail),
data: `""`,
expect: "",
errors: []error{forms.ErrInvalidEmail},
},
{
f: forms.NewTextField("", forms.IsEmail),
data: `null`,
expect: nil,
},
{
f: forms.NewTextListField("", forms.IsEmail),
data: `["test@example.net"]`,
expect: []string{"test@example.net"},
},
{
f: forms.NewTextListField("", forms.IsEmail),
data: `["test@example.net", "foo"]`,
expect: []string{"test@example.net", "foo"},
errors: []error{forms.ErrInvalidEmail},
},
}))
t.Run("URL", runValidatorTests([]fieldValidatorTest{
{
f: forms.NewTextField("", forms.IsURL("http")),
data: `"http://example.net/"`,
expect: "http://example.net/",
},
{
f: forms.NewTextField("", forms.IsURL("http", "https")),
data: `"https://example.net/"`,
expect: "https://example.net/",
},
{
f: forms.NewTextField("", forms.IsURL("http")),
data: `"http://` + string(rune(0x7f)) + `example.net/"`,
expect: "http://" + string(rune(0x7f)) + "example.net/",
errors: []error{forms.ErrInvalidURL},
},
{
f: forms.NewTextField("", forms.IsURL()),
data: `"http://example.net/"`,
expect: "http://example.net/",
errors: []error{forms.ErrInvalidURL},
},
{
f: forms.NewTextField("", forms.IsURL("http")),
data: `"http://"`,
expect: "http://",
errors: []error{forms.ErrInvalidURL},
},
{
f: forms.NewTextField("", forms.IsURL("http", "https")),
data: `"ftp://example.net/"`,
expect: "ftp://example.net/",
errors: []error{forms.ErrInvalidURL},
},
}))
t.Run("gte & lte", runValidatorTests([]fieldValidatorTest{
{
// Gte / Lte don't apply on non integer field
f: forms.NewTextField("test", forms.Gte(10)),
data: `"5"`,
expect: "5",
},
{
f: forms.NewIntegerField("test", forms.Gte(10)),
data: "10",
expect: 10,
},
{
f: forms.NewIntegerField("test", forms.Gte(10)),
data: "15",
expect: 15,
},
{
f: forms.NewIntegerField("test", forms.Gte(10)),
data: "2",
expect: 2,
errors: []error{errors.New("must be greater or equal than 10")},
},
{
f: forms.NewIntegerField("test", forms.Lte(10)),
data: "10",
expect: 10,
},
{
f: forms.NewIntegerField("test", forms.Lte(10)),
data: "15",
expect: 15,
errors: []error{errors.New("must be lower or equal than 10")},
},
{
f: forms.NewIntegerField("test", forms.Lte(10)),
data: "2",
expect: 2,
},
}))
t.Run("maxLen", runValidatorTests([]fieldValidatorTest{
{
f: forms.NewTextField("test", forms.MaxLen(10)),
data: `"abc"`,
expect: "abc",
},
{
f: forms.NewTextField("test", forms.MaxLen(10)),
data: `"問掃玉光尤向入神間示"`,
expect: "問掃玉光尤向入神間示",
},
{
f: forms.NewTextField("test", forms.MaxLen(10)),
data: `"abcdefghijk"`,
expect: "abcdefghijk",
errors: []error{errors.New("text must contain at most 10 characters")},
},
}))
t.Run("minLen", runValidatorTests([]fieldValidatorTest{
{
f: forms.NewTextField("test", forms.MinLen(10)),
data: `"abc"`,
expect: "abc",
errors: []error{errors.New("text must contain at least 10 characters")},
},
{
f: forms.NewTextField("test", forms.MinLen(10)),
data: `"問掃玉光尤向入神間"`,
expect: "問掃玉光尤向入神間",
errors: []error{errors.New("text must contain at least 10 characters")},
},
{
f: forms.NewTextField("test", forms.MinLen(10)),
data: `"abcdefghijk"`,
expect: "abcdefghijk",
},
}))
t.Run("len", runValidatorTests([]fieldValidatorTest{
{
f: forms.NewTextField("test", forms.Len(10)),
data: `"abc"`,
expect: "abc",
errors: []error{errors.New("text must contain 10 characters")},
},
{
f: forms.NewTextField("test", forms.Len(10)),
data: `"問掃玉光尤向入神間示"`,
expect: "問掃玉光尤向入神間示",
},
{
f: forms.NewTextField("test", forms.Len(10)),
data: `"abcdefghijk"`,
expect: "abcdefghijk",
errors: []error{errors.New("text must contain 10 characters")},
},
}))
t.Run("default", func(t *testing.T) {
tests := []struct {
f forms.Field
v any
}{
{
forms.NewTextField("", forms.Default("abc")),
"abc",
},
{
forms.NewIntegerField("", forms.Default("abc")),
0,
},
{
forms.NewBooleanField("", forms.Default(true)),
true,
},
{
forms.NewTextListField("", forms.Default([]string{"abc"})),
[]string{"abc"},
},
{
forms.NewTextField("", forms.DefaultFunc(func(_ forms.Field) any {
return "abc"
})),
"abc",
},
{
forms.NewTextField("", forms.DefaultFunc(func(_ forms.Field) any {
return 12
})),
"",
},
}
for i, test := range tests {
t.Run(strconv.Itoa(i+1), func(t *testing.T) {
require.Exactly(t, test.v, test.f.Value())
})
}
})
t.Run("split lines", runValidatorTests([]fieldValidatorTest{
{
f: forms.NewTextListField("", forms.SplitLines),
data: `[]`,
expect: []string{},
},
{
f: forms.NewTextListField("", forms.SplitLines),
data: `["a\nb"]`,
expect: []string{"a", "b"},
},
{
f: forms.NewTextListField("", forms.SplitLines),
data: `["a\r\nb"]`,
expect: []string{"a", "b"},
},
{
f: forms.NewTextListField("", forms.SplitLines),
data: `["a\n\n\nb\n\n"]`,
expect: []string{"a", "b"},
},
{
f: forms.NewTextListField("", forms.SplitLines),
data: `["a\nb\n", "\nc \nd"]`,
expect: []string{"a", "b", "c", "d"},
},
}))
t.Run("choices", runValidatorTests([]fieldValidatorTest{
{
f: forms.NewTextField("", forms.Choices(
forms.Choice("A", "a"), forms.Choice("B", "b"),
)),
data: `"a"`,
expect: "a",
},
{
f: forms.NewTextField("", forms.Choices(
forms.Choice("A", "a"), forms.Choice("B", "b"),
)),
data: `null`,
expect: nil,
},
{
f: forms.NewTextField("", forms.Required, forms.Choices(
forms.Choice("A", "a"), forms.Choice("B", "b"),
)),
data: `null`,
expect: nil,
errors: []error{forms.ErrRequired},
},
{
f: forms.NewTextField("", forms.Choices(
forms.Choice("A", "a"), forms.Choice("B", "b"),
)),
data: `"x"`,
expect: "x",
errors: []error{errors.New("x is not one of a, b")},
},
{
f: forms.NewIntegerField("", forms.Choices(
forms.Choice("A", 1), forms.Choice("B", 2),
)),
data: `2`,
expect: 2,
},
{
// It won't apply at all.
f: forms.NewIntegerField("", forms.Choices(
forms.Choice("A", "a"), forms.Choice("B", "b"),
)),
data: `5`,
expect: 5,
},
{
f: forms.NewTextField("", forms.ChoicesPairs([][2]string{
{"a", "A"},
{"b", "B"},
})),
data: `"a"`,
expect: "a",
},
{
f: forms.NewTextField("", forms.ChoicesPairs([][2]string{
{"a", "A"},
{"b", "B"},
})),
data: `"x"`,
expect: "x",
errors: []error{errors.New("x is not one of a, b")},
},
}))
t.Run("choices on lists", runValidatorTests([]fieldValidatorTest{
{
f: forms.NewTextListField("", forms.Choices(
forms.Choice("A", "a"), forms.Choice("B", "b"),
)),
data: `["a"]`,
expect: []string{"a"},
},
{
f: forms.NewTextListField("", forms.Choices(
forms.Choice("A", "a"), forms.Choice("B", "b"),
)),
data: `null`,
expect: nil,
},
{
f: forms.NewTextListField("", forms.Required, forms.Choices(
forms.Choice("A", "a"), forms.Choice("B", "b"),
)),
data: `null`,
expect: nil,
errors: []error{forms.ErrRequired},
},
{
f: forms.NewTextListField("", forms.Required, forms.Choices(
forms.Choice("A", "a"), forms.Choice("B", "b"),
)),
data: `[]`,
expect: []string(nil),
errors: []error{forms.ErrRequired},
},
{
f: forms.NewTextListField("", forms.Choices(
forms.Choice("A", "a"), forms.Choice("B", "b"),
)),
data: `["a", "x"]`,
expect: []string{"a", "x"},
errors: []error{errors.New("x is not one of a, b")},
},
{
f: forms.NewTextListField("", forms.Choices(
forms.Choice("A", "a"), forms.Choice("B", "b"),
)),
data: `["a", "x", "z"]`,
expect: []string{"a", "x", "z"},
errors: []error{
errors.New("x is not one of a, b"),
errors.New("z is not one of a, b"),
},
},
{
f: forms.NewIntegerListField("", forms.Choices(
forms.Choice("A", 1), forms.Choice("B", 2),
)),
data: `[2]`,
expect: []int{2},
},
{
// It won't apply at all.
f: forms.NewIntegerListField("", forms.Choices(
forms.Choice("A", "1"), forms.Choice("B", "2"),
)),
data: `[5, 10]`,
expect: []int{5, 10},
},
}))
t.Run("choices func", func(t *testing.T) {
assert := require.New(t)
cs := forms.ValueChoices[string]{
{"A", "a"}, {"B", "b"},
}
ci := forms.ValueChoices[int]{
{"A", 1}, {"B", 2},
}
form := forms.Must(
context.Background(),
forms.NewTextField("s", forms.Choices(cs...)),
forms.NewIntegerField("i", forms.Choices(ci...)),
)
assert.Exactly(cs, form.Get("s").(*forms.TextField).Choices())
assert.Exactly(ci, form.Get("i").(*forms.IntegerField).Choices())
assert.True(ci[0].In([]int{1, 2}))
assert.False(ci[0].In([]int{2}))
})
}