Adding upstream version 0.28.1.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
88f1d47ab6
commit
e28c88ef14
933 changed files with 194711 additions and 0 deletions
80
core/validators/db.go
Normal file
80
core/validators/db.go
Normal file
|
@ -0,0 +1,80 @@
|
|||
package validators
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/pocketbase/dbx"
|
||||
)
|
||||
|
||||
// UniqueId checks whether a field string id already exists in the specified table.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// validation.Field(&form.RelId, validation.By(validators.UniqueId(form.app.DB(), "tbl_example"))
|
||||
func UniqueId(db dbx.Builder, tableName string) validation.RuleFunc {
|
||||
return func(value any) error {
|
||||
v, _ := value.(string)
|
||||
if v == "" {
|
||||
return nil // nothing to check
|
||||
}
|
||||
|
||||
var foundId string
|
||||
|
||||
err := db.
|
||||
Select("id").
|
||||
From(tableName).
|
||||
Where(dbx.HashExp{"id": v}).
|
||||
Limit(1).
|
||||
Row(&foundId)
|
||||
|
||||
if (err != nil && !errors.Is(err, sql.ErrNoRows)) || foundId != "" {
|
||||
return validation.NewError("validation_invalid_or_existing_id", "The model id is invalid or already exists.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// NormalizeUniqueIndexError attempts to convert a
|
||||
// "unique constraint failed" error into a validation.Errors.
|
||||
//
|
||||
// The provided err is returned as it is without changes if:
|
||||
// - err is nil
|
||||
// - err is already validation.Errors
|
||||
// - err is not "unique constraint failed" error
|
||||
func NormalizeUniqueIndexError(err error, tableOrAlias string, fieldNames []string) error {
|
||||
if err == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, ok := err.(validation.Errors); ok {
|
||||
return err
|
||||
}
|
||||
|
||||
msg := strings.ToLower(err.Error())
|
||||
|
||||
// check for unique constraint failure
|
||||
if strings.Contains(msg, "unique constraint failed") {
|
||||
// note: extra space to unify multi-columns lookup
|
||||
msg = strings.ReplaceAll(strings.TrimSpace(msg), ",", " ") + " "
|
||||
|
||||
normalizedErrs := validation.Errors{}
|
||||
|
||||
for _, name := range fieldNames {
|
||||
// note: extra spaces to exclude table name with suffix matching the current one
|
||||
// OR other fields starting with the current field name
|
||||
if strings.Contains(msg, strings.ToLower(" "+tableOrAlias+"."+name+" ")) {
|
||||
normalizedErrs[name] = validation.NewError("validation_not_unique", "Value must be unique")
|
||||
}
|
||||
}
|
||||
|
||||
if len(normalizedErrs) > 0 {
|
||||
return normalizedErrs
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
125
core/validators/db_test.go
Normal file
125
core/validators/db_test.go
Normal file
|
@ -0,0 +1,125 @@
|
|||
package validators_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/pocketbase/pocketbase/core/validators"
|
||||
"github.com/pocketbase/pocketbase/tests"
|
||||
)
|
||||
|
||||
func TestUniqueId(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
scenarios := []struct {
|
||||
id string
|
||||
tableName string
|
||||
expectError bool
|
||||
}{
|
||||
{"", "", false},
|
||||
{"test", "", true},
|
||||
{"wsmn24bux7wo113", "_collections", true},
|
||||
{"test_unique_id", "unknown_table", true},
|
||||
{"test_unique_id", "_collections", false},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
t.Run(fmt.Sprintf("%d_%s_%s", i, s.id, s.tableName), func(t *testing.T) {
|
||||
err := validators.UniqueId(app.DB(), s.tableName)(s.id)
|
||||
|
||||
hasErr := err != nil
|
||||
if hasErr != s.expectError {
|
||||
t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeUniqueIndexError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
err error
|
||||
table string
|
||||
names []string
|
||||
expectedKeys []string
|
||||
}{
|
||||
{
|
||||
"nil error (no changes)",
|
||||
nil,
|
||||
"test",
|
||||
[]string{"a", "b"},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"non-unique index error (no changes)",
|
||||
errors.New("abc"),
|
||||
"test",
|
||||
[]string{"a", "b"},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"validation error (no changes)",
|
||||
validation.Errors{"c": errors.New("abc")},
|
||||
"test",
|
||||
[]string{"a", "b"},
|
||||
[]string{"c"},
|
||||
},
|
||||
{
|
||||
"unique index error but mismatched table name",
|
||||
errors.New("UNIQUE constraint failed for fields test.a,test.b"),
|
||||
"example",
|
||||
[]string{"a", "b"},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"unique index error with table name suffix matching the specified one",
|
||||
errors.New("UNIQUE constraint failed for fields test_suffix.a,test_suffix.b"),
|
||||
"suffix",
|
||||
[]string{"a", "b", "c"},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"unique index error but mismatched fields",
|
||||
errors.New("UNIQUE constraint failed for fields test.a,test.b"),
|
||||
"test",
|
||||
[]string{"c", "d"},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"unique index error with matching table name and fields",
|
||||
errors.New("UNIQUE constraint failed for fields test.a,test.b"),
|
||||
"test",
|
||||
[]string{"a", "b", "c"},
|
||||
[]string{"a", "b"},
|
||||
},
|
||||
{
|
||||
"unique index error with matching table name and field starting with the name of another non-unique field",
|
||||
errors.New("UNIQUE constraint failed for fields test.a_2,test.c"),
|
||||
"test",
|
||||
[]string{"a", "a_2", "c"},
|
||||
[]string{"a_2", "c"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
result := validators.NormalizeUniqueIndexError(s.err, s.table, s.names)
|
||||
|
||||
if len(s.expectedKeys) == 0 {
|
||||
if result != s.err {
|
||||
t.Fatalf("Expected no error change, got %v", result)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
tests.TestValidationErrors(t, result, s.expectedKeys)
|
||||
})
|
||||
}
|
||||
}
|
85
core/validators/equal.go
Normal file
85
core/validators/equal.go
Normal file
|
@ -0,0 +1,85 @@
|
|||
package validators
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
)
|
||||
|
||||
// Equal checks whether the validated value matches another one from the same type.
|
||||
//
|
||||
// It expects the compared values to be from the same type and works
|
||||
// with booleans, numbers, strings and their pointer variants.
|
||||
//
|
||||
// If one of the value is pointer, the comparison is based on its
|
||||
// underlying value (when possible to determine).
|
||||
//
|
||||
// Note that empty/zero values are also compared (this differ from other validation.RuleFunc).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// validation.Field(&form.PasswordConfirm, validation.By(validators.Equal(form.Password)))
|
||||
func Equal[T comparable](valueToCompare T) validation.RuleFunc {
|
||||
return func(value any) error {
|
||||
if compareValues(value, valueToCompare) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return validation.NewError("validation_values_mismatch", "Values don't match.")
|
||||
}
|
||||
}
|
||||
|
||||
func compareValues(a, b any) bool {
|
||||
if a == b {
|
||||
return true
|
||||
}
|
||||
|
||||
if checkIsNil(a) && checkIsNil(b) {
|
||||
return true
|
||||
}
|
||||
|
||||
var result bool
|
||||
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
result = false
|
||||
}
|
||||
}()
|
||||
|
||||
reflectA := reflect.ValueOf(a)
|
||||
reflectB := reflect.ValueOf(b)
|
||||
|
||||
dereferencedA := dereference(reflectA)
|
||||
dereferencedB := dereference(reflectB)
|
||||
if dereferencedA.CanInterface() && dereferencedB.CanInterface() {
|
||||
result = dereferencedA.Interface() == dereferencedB.Interface()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// note https://github.com/golang/go/issues/51649
|
||||
func checkIsNil(value any) bool {
|
||||
if value == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
var result bool
|
||||
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
result = false
|
||||
}
|
||||
}()
|
||||
|
||||
result = reflect.ValueOf(value).IsNil()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func dereference(v reflect.Value) reflect.Value {
|
||||
for v.Kind() == reflect.Pointer {
|
||||
v = v.Elem()
|
||||
}
|
||||
return v
|
||||
}
|
62
core/validators/equal_test.go
Normal file
62
core/validators/equal_test.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
package validators_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core/validators"
|
||||
)
|
||||
|
||||
func Equal(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
strA := "abc"
|
||||
strB := "abc"
|
||||
strC := "123"
|
||||
var strNilPtr *string
|
||||
var strNilPtr2 *string
|
||||
|
||||
scenarios := []struct {
|
||||
valA any
|
||||
valB any
|
||||
expectError bool
|
||||
}{
|
||||
{nil, nil, false},
|
||||
{"", "", false},
|
||||
{"", "456", true},
|
||||
{"123", "", true},
|
||||
{"123", "456", true},
|
||||
{"123", "123", false},
|
||||
{true, false, true},
|
||||
{false, true, true},
|
||||
{false, false, false},
|
||||
{true, true, false},
|
||||
{0, 0, false},
|
||||
{0, 1, true},
|
||||
{1, 2, true},
|
||||
{1, 1, false},
|
||||
{&strA, &strA, false},
|
||||
{&strA, &strB, false},
|
||||
{&strA, &strC, true},
|
||||
{"abc", &strA, false},
|
||||
{&strA, "abc", false},
|
||||
{"abc", &strC, true},
|
||||
{"test", 123, true},
|
||||
{nil, 123, true},
|
||||
{nil, strA, true},
|
||||
{nil, &strA, true},
|
||||
{nil, strNilPtr, false},
|
||||
{strNilPtr, strNilPtr2, false},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
t.Run(fmt.Sprintf("%d_%v_%v", i, s.valA, s.valB), func(t *testing.T) {
|
||||
err := validators.Equal(s.valA)(s.valB)
|
||||
|
||||
hasErr := err != nil
|
||||
if hasErr != s.expectError {
|
||||
t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
96
core/validators/file.go
Normal file
96
core/validators/file.go
Normal file
|
@ -0,0 +1,96 @@
|
|||
package validators
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/pocketbase/pocketbase/tools/filesystem"
|
||||
)
|
||||
|
||||
// UploadedFileSize checks whether the validated [*filesystem.File]
|
||||
// size is no more than the provided maxBytes.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// validation.Field(&form.File, validation.By(validators.UploadedFileSize(1000)))
|
||||
func UploadedFileSize(maxBytes int64) validation.RuleFunc {
|
||||
return func(value any) error {
|
||||
v, ok := value.(*filesystem.File)
|
||||
if !ok {
|
||||
return ErrUnsupportedValueType
|
||||
}
|
||||
|
||||
if v == nil {
|
||||
return nil // nothing to validate
|
||||
}
|
||||
|
||||
if v.Size > maxBytes {
|
||||
return validation.NewError(
|
||||
"validation_file_size_limit",
|
||||
"Failed to upload {{.file}} - the maximum allowed file size is {{.maxSize}} bytes.",
|
||||
).SetParams(map[string]any{
|
||||
"file": v.OriginalName,
|
||||
"maxSize": maxBytes,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// UploadedFileMimeType checks whether the validated [*filesystem.File]
|
||||
// mimetype is within the provided allowed mime types.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// validMimeTypes := []string{"test/plain","image/jpeg"}
|
||||
// validation.Field(&form.File, validation.By(validators.UploadedFileMimeType(validMimeTypes)))
|
||||
func UploadedFileMimeType(validTypes []string) validation.RuleFunc {
|
||||
return func(value any) error {
|
||||
v, ok := value.(*filesystem.File)
|
||||
if !ok {
|
||||
return ErrUnsupportedValueType
|
||||
}
|
||||
|
||||
if v == nil {
|
||||
return nil // nothing to validate
|
||||
}
|
||||
|
||||
baseErr := validation.NewError(
|
||||
"validation_invalid_mime_type",
|
||||
fmt.Sprintf("Failed to upload %q due to unsupported file type.", v.OriginalName),
|
||||
)
|
||||
|
||||
if len(validTypes) == 0 {
|
||||
return baseErr
|
||||
}
|
||||
|
||||
f, err := v.Reader.Open()
|
||||
if err != nil {
|
||||
return baseErr
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
filetype, err := mimetype.DetectReader(f)
|
||||
if err != nil {
|
||||
return baseErr
|
||||
}
|
||||
|
||||
for _, t := range validTypes {
|
||||
if filetype.Is(t) {
|
||||
return nil // valid
|
||||
}
|
||||
}
|
||||
|
||||
return validation.NewError(
|
||||
"validation_invalid_mime_type",
|
||||
fmt.Sprintf(
|
||||
"%q mime type must be one of: %s.",
|
||||
v.Name,
|
||||
strings.Join(validTypes, ", "),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
75
core/validators/file_test.go
Normal file
75
core/validators/file_test.go
Normal file
|
@ -0,0 +1,75 @@
|
|||
package validators_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core/validators"
|
||||
"github.com/pocketbase/pocketbase/tools/filesystem"
|
||||
)
|
||||
|
||||
func TestUploadedFileSize(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
file, err := filesystem.NewFileFromBytes([]byte("test"), "test.txt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
scenarios := []struct {
|
||||
maxBytes int64
|
||||
file *filesystem.File
|
||||
expectError bool
|
||||
}{
|
||||
{0, nil, false},
|
||||
{4, nil, false},
|
||||
{3, file, true}, // all test files have "test" as content
|
||||
{4, file, false},
|
||||
{5, file, false},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(fmt.Sprintf("%d", s.maxBytes), func(t *testing.T) {
|
||||
err := validators.UploadedFileSize(s.maxBytes)(s.file)
|
||||
|
||||
hasErr := err != nil
|
||||
if hasErr != s.expectError {
|
||||
t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadedFileMimeType(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
file, err := filesystem.NewFileFromBytes([]byte("test"), "test.png") // the extension shouldn't matter
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
scenarios := []struct {
|
||||
types []string
|
||||
file *filesystem.File
|
||||
expectError bool
|
||||
}{
|
||||
{nil, nil, false},
|
||||
{[]string{"image/jpeg"}, nil, false},
|
||||
{[]string{}, file, true},
|
||||
{[]string{"image/jpeg"}, file, true},
|
||||
// test files are detected as "text/plain; charset=utf-8" content type
|
||||
{[]string{"image/jpeg", "text/plain; charset=utf-8"}, file, false},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
t.Run(fmt.Sprintf("%d_%s", i, strings.Join(s.types, ";")), func(t *testing.T) {
|
||||
err := validators.UploadedFileMimeType(s.types)(s.file)
|
||||
|
||||
hasErr := err != nil
|
||||
if hasErr != s.expectError {
|
||||
t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
29
core/validators/string.go
Normal file
29
core/validators/string.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package validators
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
)
|
||||
|
||||
// IsRegex checks whether the validated value is a valid regular expression pattern.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// validation.Field(&form.Pattern, validation.By(validators.IsRegex))
|
||||
func IsRegex(value any) error {
|
||||
v, ok := value.(string)
|
||||
if !ok {
|
||||
return ErrUnsupportedValueType
|
||||
}
|
||||
|
||||
if v == "" {
|
||||
return nil // nothing to check
|
||||
}
|
||||
|
||||
if _, err := regexp.Compile(v); err != nil {
|
||||
return validation.NewError("validation_invalid_regex", err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
33
core/validators/string_test.go
Normal file
33
core/validators/string_test.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
package validators_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core/validators"
|
||||
)
|
||||
|
||||
func TestIsRegex(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
scenarios := []struct {
|
||||
val string
|
||||
expectError bool
|
||||
}{
|
||||
{"", false},
|
||||
{`abc`, false},
|
||||
{`\w+`, false},
|
||||
{`\w*((abc+`, true},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
t.Run(fmt.Sprintf("%d_%#v", i, s.val), func(t *testing.T) {
|
||||
err := validators.IsRegex(s.val)
|
||||
|
||||
hasErr := err != nil
|
||||
if hasErr != s.expectError {
|
||||
t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
40
core/validators/validators.go
Normal file
40
core/validators/validators.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
// Package validators implements some common custom PocketBase validators.
|
||||
package validators
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"maps"
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
)
|
||||
|
||||
var ErrUnsupportedValueType = validation.NewError("validation_unsupported_value_type", "Invalid or unsupported value type.")
|
||||
|
||||
// JoinValidationErrors attempts to join the provided [validation.Errors] arguments.
|
||||
//
|
||||
// If only one of the arguments is [validation.Errors], it returns the first non-empty [validation.Errors].
|
||||
//
|
||||
// If both arguments are not [validation.Errors] then it returns a combined [errors.Join] error.
|
||||
func JoinValidationErrors(errA, errB error) error {
|
||||
vErrA, okA := errA.(validation.Errors)
|
||||
vErrB, okB := errB.(validation.Errors)
|
||||
|
||||
// merge
|
||||
if okA && okB {
|
||||
result := maps.Clone(vErrA)
|
||||
maps.Copy(result, vErrB)
|
||||
if len(result) > 0 {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
if okA && len(vErrA) > 0 {
|
||||
return vErrA
|
||||
}
|
||||
|
||||
if okB && len(vErrB) > 0 {
|
||||
return vErrB
|
||||
}
|
||||
|
||||
return errors.Join(errA, errB)
|
||||
}
|
39
core/validators/validators_test.go
Normal file
39
core/validators/validators_test.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package validators_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/pocketbase/pocketbase/core/validators"
|
||||
)
|
||||
|
||||
func TestJoinValidationErrors(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
errA error
|
||||
errB error
|
||||
expected string
|
||||
}{
|
||||
{nil, nil, "<nil>"},
|
||||
{errors.New("abc"), nil, "abc"},
|
||||
{nil, errors.New("abc"), "abc"},
|
||||
{errors.New("abc"), errors.New("456"), "abc\n456"},
|
||||
{validation.Errors{"test1": errors.New("test1_err")}, nil, "test1: test1_err."},
|
||||
{nil, validation.Errors{"test2": errors.New("test2_err")}, "test2: test2_err."},
|
||||
{validation.Errors{}, errors.New("456"), "\n456"},
|
||||
{errors.New("456"), validation.Errors{}, "456\n"},
|
||||
{validation.Errors{"test1": errors.New("test1_err")}, errors.New("456"), "test1: test1_err."},
|
||||
{errors.New("456"), validation.Errors{"test2": errors.New("test2_err")}, "test2: test2_err."},
|
||||
{validation.Errors{"test1": errors.New("test1_err")}, validation.Errors{"test2": errors.New("test2_err")}, "test1: test1_err; test2: test2_err."},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
t.Run(fmt.Sprintf("%d_%#T_%T", i, s.errA, s.errB), func(t *testing.T) {
|
||||
result := fmt.Sprintf("%v", validators.JoinValidationErrors(s.errA, s.errB))
|
||||
if result != s.expected {
|
||||
t.Fatalf("Expected\n%v\ngot\n%v", s.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue