Files
go-validate/validate_test.go
Jim Myhrberg 9c3b63361d fix(validate): do not panic with field errors on non-struct types
When validating non-struct types, we should not try and convert field
values in errors, because there is no struct field to lookup. Hence this
fix stops a panic from happening.

It is up to the Validate() method itself to provide the correct and
final field value when validating non-struct types.
2021-08-23 12:55:24 +01:00

957 lines
23 KiB
Go

package validate_test
import (
"errors"
"fmt"
"strings"
"testing"
"github.com/romdo/go-validate"
"github.com/stretchr/testify/assert"
)
//
// Test helper types
//
type validatableString string
func (s validatableString) Validate() error {
if strings.Contains(string(s), " ") {
return &validate.Error{Msg: "must not contain space"}
}
return nil
}
type validatableStruct struct {
Foo string
Bar string `json:"bar"`
Foz string `yaml:"foz"`
Baz string `form:"baz"`
f func() error
}
func (s *validatableStruct) Validate() error {
if s.f == nil {
return nil
}
return s.f()
}
type validatableSlice []validatableStruct
func (s validatableSlice) Validate() error {
var errs error
if len(s) < 1 {
errs = validate.AppendError(errs, "must contain at least 1 item")
}
fooVals := map[string]bool{}
for i, item := range s {
if fooVals[item.Foo] {
errs = validate.Append(errs, &validate.Error{
Field: fmt.Sprintf("%d.Foo", i),
Msg: "is not unique",
})
}
fooVals[item.Foo] = true
}
return errs
}
type nestedStruct struct {
OtherField *validatableStruct
OtherFieldJSON *validatableStruct `json:"other_field,omitempty"`
OtherFieldYAML *validatableStruct `yaml:"otherField,omitempty"`
OtherFieldFORM *validatableStruct `form:"other-field,omitempty"`
skippedField *validatableStruct
SkippedFieldJSON *validatableStruct `json:"-,omitempty"`
SkippedFieldYAML *validatableStruct `yaml:"-,omitempty"`
SkippedFieldFORM *validatableStruct `form:"-,omitempty"`
OtherArray [5]*validatableStruct
OtherArrayJSON [5]*validatableStruct `json:"other_array,omitempty"`
OtherArrayYAML [5]*validatableStruct `yaml:"otherArray,omitempty"`
OtherArrayFORM [5]*validatableStruct `form:"other-array,omitempty"`
OtherSlice []*validatableStruct
OtherSliceJSON []*validatableStruct `json:"other_slice,omitempty"`
OtherSliceYAML []*validatableStruct `yaml:"otherSlice,omitempty"`
OtherSliceFORM []*validatableStruct `form:"other-slice,omitempty"`
OtherStringMap map[string]*validatableStruct
OtherStringMapJSON map[string]*validatableStruct `json:"other_string_map,omitempty"`
OtherStringMapYAML map[string]*validatableStruct `yaml:"otherStringMap,omitempty"`
OtherStringMapFORM map[string]*validatableStruct `form:"other-string-map,omitempty"`
OtherIntMap map[int]*validatableStruct
OtherIntMapJSON map[int]*validatableStruct `json:"other_int_map,omitempty"`
OtherIntMapYAML map[int]*validatableStruct `yaml:"otherIntMap,omitempty"`
OtherIntMapFORM map[int]*validatableStruct `form:"other-int-map,omitempty"`
OtherStruct *nestedStruct
OtherStructJSON *nestedStruct `json:"other_struct,omitempty"`
OtherStructYAML *nestedStruct `yaml:"otherStruct,omitempty"`
OtherStructFORM *nestedStruct `form:"other-struct,omitempty"`
}
//
// Tests
//
func TestValidate(t *testing.T) {
mixedValidationErrors := &validatableStruct{
f: func() error {
var errs error
errs = validate.Append(errs, &validate.Error{
Field: "Foo", Msg: "is required",
Err: errors.New("oops"),
})
errs = validate.Append(errs, errors.New("bar: is missing"))
return errs
},
}
tests := []struct {
name string
obj interface{}
wantErrs []error
}{
{
name: "nil",
obj: nil,
wantErrs: []error{},
},
{
name: "no error",
obj: &validatableStruct{},
wantErrs: nil,
},
{
name: "valid validatable string type",
obj: validatableString("hello-world"),
wantErrs: []error{},
},
{
name: "invalid validatable string type",
obj: validatableString("hello world"),
wantErrs: []error{
&validate.Error{Msg: "must not contain space"},
},
},
{
name: "valid validatable slice type",
obj: validatableSlice{
{Foo: "Hello"},
{Foo: "World"},
},
wantErrs: []error{},
},
{
name: "invalid validatable slice type",
obj: validatableSlice{},
wantErrs: []error{
&validate.Error{Msg: "must contain at least 1 item"},
},
},
{
name: "invalid validatable slice type with custom field error",
obj: validatableSlice{
{Foo: "Hello"},
{Foo: "Hello"},
},
wantErrs: []error{
&validate.Error{Field: "1.Foo", Msg: "is not unique"},
},
},
{
name: "single Go error",
obj: &validatableStruct{f: func() error {
return errors.New("foo: is required")
}},
wantErrs: []error{
&validate.Error{Err: errors.New("foo: is required")},
},
},
{
name: "single *validate.Error",
obj: &validatableStruct{f: func() error {
return &validate.Error{
Field: "foo", Msg: "is required", Err: errors.New("oops"),
}
}},
wantErrs: []error{
&validate.Error{
Field: "foo", Msg: "is required", Err: errors.New("oops"),
},
},
},
{
name: "multiple Go errors",
obj: &validatableStruct{f: func() error {
var errs error
errs = validate.Append(errs, errors.New("foo: is required"))
errs = validate.Append(errs, errors.New("bar: is missing"))
return errs
}},
wantErrs: []error{
&validate.Error{Err: errors.New("foo: is required")},
&validate.Error{Err: errors.New("bar: is missing")},
},
},
{
name: "multiple *validate.Error",
obj: &validatableStruct{f: func() error {
var errs error
errs = validate.Append(errs, &validate.Error{
Field: "foo", Msg: "is required", Err: errors.New("oops"),
})
errs = validate.Append(errs, &validate.Error{
Field: "bar", Msg: "is required", Err: errors.New("whoops"),
})
return errs
}},
wantErrs: []error{
&validate.Error{
Field: "foo", Msg: "is required", Err: errors.New("oops"),
},
&validate.Error{
Field: "bar", Msg: "is required", Err: errors.New("whoops"),
},
},
},
{
name: "mix of Go error and *validate.Error",
obj: mixedValidationErrors,
wantErrs: []error{
&validate.Error{
Field: "Foo", Msg: "is required", Err: errors.New("oops"),
},
&validate.Error{Err: errors.New("bar: is missing")},
},
},
//
// Field name conversion
//
{
name: "no json, yaml or form field tag",
obj: &validatableStruct{f: func() error {
return &validate.Error{Field: "Foo", Msg: "is required"}
}},
wantErrs: []error{
&validate.Error{Field: "Foo", Msg: "is required"},
},
},
{
name: "converts field name via json field tag",
obj: &validatableStruct{f: func() error {
return &validate.Error{Field: "Bar", Msg: "is required"}
}},
wantErrs: []error{
&validate.Error{Field: "bar", Msg: "is required"},
},
},
{
name: "converts field name via yaml field tag",
obj: &validatableStruct{f: func() error {
return &validate.Error{Field: "Foz", Msg: "is required"}
}},
wantErrs: []error{
&validate.Error{Field: "foz", Msg: "is required"},
},
},
{
name: "converts field name via form field tag",
obj: &validatableStruct{f: func() error {
return &validate.Error{Field: "Baz", Msg: "is required"}
}},
wantErrs: []error{
&validate.Error{Field: "baz", Msg: "is required"},
},
},
{
name: "nested with no validation errors",
obj: &nestedStruct{
OtherField: &validatableStruct{},
OtherArray: [5]*validatableStruct{{}, {}, {}, {}},
OtherSlice: []*validatableStruct{{}},
OtherStringMap: map[string]*validatableStruct{
"hi": {},
"bye": {},
},
OtherIntMap: map[int]*validatableStruct{42: {}, 64: {}},
OtherStruct: &nestedStruct{
OtherField: &validatableStruct{},
},
},
wantErrs: []error{},
},
//
// Nested in a struct field.
//
{
name: "nested in a struct field",
obj: &nestedStruct{
OtherField: mixedValidationErrors,
},
wantErrs: []error{
&validate.Error{
Field: "OtherField.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "OtherField", Err: errors.New("bar: is missing"),
},
},
},
{
name: "nested in a struct field with json tag",
obj: &nestedStruct{
OtherFieldJSON: mixedValidationErrors,
},
wantErrs: []error{
&validate.Error{
Field: "other_field.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "other_field", Err: errors.New("bar: is missing"),
},
},
},
{
name: "nested in a struct field with yaml tag",
obj: &nestedStruct{
OtherFieldYAML: mixedValidationErrors,
},
wantErrs: []error{
&validate.Error{
Field: "otherField.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "otherField", Err: errors.New("bar: is missing"),
},
},
},
{
name: "nested in a struct field with form tag",
obj: &nestedStruct{
OtherFieldFORM: mixedValidationErrors,
},
wantErrs: []error{
&validate.Error{
Field: "other-field.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "other-field", Err: errors.New("bar: is missing"),
},
},
},
//
// Nested in a unexposed/ignored fields.
//
{
name: "nested in a unexposed field",
obj: &nestedStruct{
skippedField: mixedValidationErrors,
},
wantErrs: []error{},
},
{
name: "nested in a struct field skipped by json tag",
obj: &nestedStruct{
SkippedFieldJSON: mixedValidationErrors,
},
wantErrs: []error{},
},
{
name: "nested in a struct field skipped by yaml tag",
obj: &nestedStruct{
SkippedFieldYAML: mixedValidationErrors,
},
wantErrs: []error{},
},
{
name: "nested in a struct field skipped by yaml tag",
obj: &nestedStruct{
SkippedFieldFORM: mixedValidationErrors,
},
wantErrs: []error{},
},
//
// Nested in an array.
//
{
name: "nested in an array",
obj: &nestedStruct{
OtherArray: [5]*validatableStruct{
mixedValidationErrors,
mixedValidationErrors,
},
},
wantErrs: []error{
&validate.Error{
Field: "OtherArray.0.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "OtherArray.0", Err: errors.New("bar: is missing"),
},
&validate.Error{
Field: "OtherArray.1.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "OtherArray.1", Err: errors.New("bar: is missing"),
},
},
},
{
name: "nested in an array with json tag",
obj: &nestedStruct{
OtherArrayJSON: [5]*validatableStruct{
mixedValidationErrors,
mixedValidationErrors,
},
},
wantErrs: []error{
&validate.Error{
Field: "other_array.0.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "other_array.0", Err: errors.New("bar: is missing"),
},
&validate.Error{
Field: "other_array.1.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "other_array.1", Err: errors.New("bar: is missing"),
},
},
},
{
name: "nested in an array with yaml tag",
obj: &nestedStruct{
OtherArrayYAML: [5]*validatableStruct{
mixedValidationErrors,
mixedValidationErrors,
},
},
wantErrs: []error{
&validate.Error{
Field: "otherArray.0.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "otherArray.0", Err: errors.New("bar: is missing"),
},
&validate.Error{
Field: "otherArray.1.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "otherArray.1", Err: errors.New("bar: is missing"),
},
},
},
{
name: "nested in an array with form tag",
obj: &nestedStruct{
OtherArrayFORM: [5]*validatableStruct{
mixedValidationErrors,
mixedValidationErrors,
},
},
wantErrs: []error{
&validate.Error{
Field: "other-array.0.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "other-array.0", Err: errors.New("bar: is missing"),
},
&validate.Error{
Field: "other-array.1.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "other-array.1", Err: errors.New("bar: is missing"),
},
},
},
//
// Nested in a slice.
//
{
name: "nested in a slice",
obj: &nestedStruct{
OtherSlice: []*validatableStruct{
mixedValidationErrors,
mixedValidationErrors,
},
},
wantErrs: []error{
&validate.Error{
Field: "OtherSlice.0.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "OtherSlice.0", Err: errors.New("bar: is missing"),
},
&validate.Error{
Field: "OtherSlice.1.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "OtherSlice.1", Err: errors.New("bar: is missing"),
},
},
},
{
name: "nested in a slice with json tag",
obj: &nestedStruct{
OtherSliceJSON: []*validatableStruct{
mixedValidationErrors,
mixedValidationErrors,
},
},
wantErrs: []error{
&validate.Error{
Field: "other_slice.0.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "other_slice.0", Err: errors.New("bar: is missing"),
},
&validate.Error{
Field: "other_slice.1.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "other_slice.1", Err: errors.New("bar: is missing"),
},
},
},
{
name: "nested in a slice with yaml tag",
obj: &nestedStruct{
OtherSliceYAML: []*validatableStruct{
mixedValidationErrors,
mixedValidationErrors,
},
},
wantErrs: []error{
&validate.Error{
Field: "otherSlice.0.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "otherSlice.0", Err: errors.New("bar: is missing"),
},
&validate.Error{
Field: "otherSlice.1.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "otherSlice.1", Err: errors.New("bar: is missing"),
},
},
},
{
name: "nested in a slice with form tag",
obj: &nestedStruct{
OtherSliceFORM: []*validatableStruct{
mixedValidationErrors,
mixedValidationErrors,
},
},
wantErrs: []error{
&validate.Error{
Field: "other-slice.0.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "other-slice.0", Err: errors.New("bar: is missing"),
},
&validate.Error{
Field: "other-slice.1.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "other-slice.1", Err: errors.New("bar: is missing"),
},
},
},
//
// Nested in an string map.
//
{
name: "nested in a string map",
obj: &nestedStruct{
OtherStringMap: map[string]*validatableStruct{
"hello": mixedValidationErrors,
"world": mixedValidationErrors,
},
},
wantErrs: []error{
&validate.Error{
Field: "OtherStringMap.hello.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "OtherStringMap.hello",
Err: errors.New("bar: is missing"),
},
&validate.Error{
Field: "OtherStringMap.world.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "OtherStringMap.world",
Err: errors.New("bar: is missing"),
},
},
},
{
name: "nested in a string map with json tag",
obj: &nestedStruct{
OtherStringMapJSON: map[string]*validatableStruct{
"hello": mixedValidationErrors,
"world": mixedValidationErrors,
},
},
wantErrs: []error{
&validate.Error{
Field: "other_string_map.hello.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "other_string_map.hello",
Err: errors.New("bar: is missing"),
},
&validate.Error{
Field: "other_string_map.world.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "other_string_map.world",
Err: errors.New("bar: is missing"),
},
},
},
{
name: "nested in a string map with yaml tag",
obj: &nestedStruct{
OtherStringMapYAML: map[string]*validatableStruct{
"hello": mixedValidationErrors,
"world": mixedValidationErrors,
},
},
wantErrs: []error{
&validate.Error{
Field: "otherStringMap.hello.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "otherStringMap.hello",
Err: errors.New("bar: is missing"),
},
&validate.Error{
Field: "otherStringMap.world.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "otherStringMap.world",
Err: errors.New("bar: is missing"),
},
},
},
{
name: "nested in a string map with form tag",
obj: &nestedStruct{
OtherStringMapFORM: map[string]*validatableStruct{
"hello": mixedValidationErrors,
"world": mixedValidationErrors,
},
},
wantErrs: []error{
&validate.Error{
Field: "other-string-map.hello.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "other-string-map.hello",
Err: errors.New("bar: is missing"),
},
&validate.Error{
Field: "other-string-map.world.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "other-string-map.world",
Err: errors.New("bar: is missing"),
},
},
},
//
// Nested in an int map.
//
{
name: "nested in a int map",
obj: &nestedStruct{
OtherIntMap: map[int]*validatableStruct{
42: mixedValidationErrors,
64: mixedValidationErrors,
},
},
wantErrs: []error{
&validate.Error{
Field: "OtherIntMap.42.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "OtherIntMap.42",
Err: errors.New("bar: is missing"),
},
&validate.Error{
Field: "OtherIntMap.64.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "OtherIntMap.64",
Err: errors.New("bar: is missing"),
},
},
},
{
name: "nested in a int map with json tag",
obj: &nestedStruct{
OtherIntMapJSON: map[int]*validatableStruct{
42: mixedValidationErrors,
64: mixedValidationErrors,
},
},
wantErrs: []error{
&validate.Error{
Field: "other_int_map.42.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "other_int_map.42",
Err: errors.New("bar: is missing"),
},
&validate.Error{
Field: "other_int_map.64.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "other_int_map.64",
Err: errors.New("bar: is missing"),
},
},
},
{
name: "nested in a int map with yaml tag",
obj: &nestedStruct{
OtherIntMapYAML: map[int]*validatableStruct{
42: mixedValidationErrors,
64: mixedValidationErrors,
},
},
wantErrs: []error{
&validate.Error{
Field: "otherIntMap.42.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "otherIntMap.42",
Err: errors.New("bar: is missing"),
},
&validate.Error{
Field: "otherIntMap.64.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "otherIntMap.64",
Err: errors.New("bar: is missing"),
},
},
},
{
name: "nested in a int map with form tag",
obj: &nestedStruct{
OtherIntMapFORM: map[int]*validatableStruct{
42: mixedValidationErrors,
64: mixedValidationErrors,
},
},
wantErrs: []error{
&validate.Error{
Field: "other-int-map.42.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "other-int-map.42",
Err: errors.New("bar: is missing"),
},
&validate.Error{
Field: "other-int-map.64.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "other-int-map.64",
Err: errors.New("bar: is missing"),
},
},
},
//
// Nested in another struct.
//
{
name: "nested in another struct",
obj: &nestedStruct{
OtherStruct: &nestedStruct{
OtherField: mixedValidationErrors,
OtherStringMap: map[string]*validatableStruct{
"world": mixedValidationErrors,
},
},
},
wantErrs: []error{
&validate.Error{
Field: "OtherStruct.OtherField.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "OtherStruct.OtherField",
Err: errors.New("bar: is missing"),
},
&validate.Error{
Field: "OtherStruct.OtherStringMap.world.Foo",
Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "OtherStruct.OtherStringMap.world",
Err: errors.New("bar: is missing"),
},
},
},
{
name: "nested in a int map with json tag",
obj: &nestedStruct{
OtherStructJSON: &nestedStruct{
OtherFieldJSON: mixedValidationErrors,
OtherStringMapJSON: map[string]*validatableStruct{
"world": mixedValidationErrors,
},
},
},
wantErrs: []error{
&validate.Error{
Field: "other_struct.other_field.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "other_struct.other_field",
Err: errors.New("bar: is missing"),
},
&validate.Error{
Field: "other_struct.other_string_map.world.Foo",
Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "other_struct.other_string_map.world",
Err: errors.New("bar: is missing"),
},
},
},
{
name: "nested in a int map with yaml tag",
obj: &nestedStruct{
OtherStructYAML: &nestedStruct{
OtherFieldYAML: mixedValidationErrors,
OtherStringMapYAML: map[string]*validatableStruct{
"world": mixedValidationErrors,
},
},
},
wantErrs: []error{
&validate.Error{
Field: "otherStruct.otherField.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "otherStruct.otherField",
Err: errors.New("bar: is missing"),
},
&validate.Error{
Field: "otherStruct.otherStringMap.world.Foo",
Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "otherStruct.otherStringMap.world",
Err: errors.New("bar: is missing"),
},
},
},
{
name: "nested in a int map with form tag",
obj: &nestedStruct{
OtherStructFORM: &nestedStruct{
OtherFieldFORM: mixedValidationErrors,
OtherStringMapFORM: map[string]*validatableStruct{
"world": mixedValidationErrors,
},
},
},
wantErrs: []error{
&validate.Error{
Field: "other-struct.other-field.Foo", Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "other-struct.other-field",
Err: errors.New("bar: is missing"),
},
&validate.Error{
Field: "other-struct.other-string-map.world.Foo",
Msg: "is required",
Err: errors.New("oops"),
},
&validate.Error{
Field: "other-struct.other-string-map.world",
Err: errors.New("bar: is missing"),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validate.Validate(tt.obj)
if len(tt.wantErrs) == 0 {
assert.Nil(t, err, "validation error should be nil")
}
got := validate.Errors(err)
assert.ElementsMatch(t, tt.wantErrs, got)
})
}
}