3
0

Add value validators & sanitizers for all field kinds

This commit is contained in:
Denis Arh
2020-02-25 17:25:55 +01:00
parent 4d72fe1a13
commit 79047e9d39
6 changed files with 547 additions and 133 deletions
+13 -4
View File
@@ -131,7 +131,7 @@ func (svc record) With(ctx context.Context) RecordService {
// Initialize validator and setup all checkers it needs
validator := values.Validator()
validator.UniqueChecker(func(m *types.Module, f *types.ModuleField, v *types.RecordValue) (uint64, error) {
validator.UniqueChecker(func(v *types.RecordValue, f *types.ModuleField, m *types.Module) (uint64, error) {
if v.Ref == 0 {
return 0, nil
}
@@ -139,7 +139,7 @@ func (svc record) With(ctx context.Context) RecordService {
return repository.Record(ctx, db).RefValueLookup(m.ID, f.Name, v.Ref)
})
validator.RecordRefChecker(func(m *types.Module, f *types.ModuleField, v *types.RecordValue) (bool, error) {
validator.RecordRefChecker(func(v *types.RecordValue, f *types.ModuleField, m *types.Module) (bool, error) {
if v.Ref == 0 {
return false, nil
}
@@ -148,11 +148,20 @@ func (svc record) With(ctx context.Context) RecordService {
return r != nil, err
})
validator.UserRefChecker(func(m *types.Module, f *types.ModuleField, v *types.RecordValue) (bool, error) {
validator.UserRefChecker(func(v *types.RecordValue, f *types.ModuleField, m *types.Module) (bool, error) {
// @todo cross service check
return true, nil
})
validator.FileRefChecker(func(v *types.RecordValue, f *types.ModuleField, m *types.Module) (bool, error) {
if v.Ref == 0 {
return false, nil
}
r, err := repository.Attachment(ctx, db).FindByID(m.NamespaceID, v.Ref)
return r != nil, err
})
return &record{
db: db,
ctx: ctx,
@@ -430,7 +439,7 @@ func (svc record) Update(upd *types.Record) (rec *types.Record, err error) {
// This logic is kept in a utility function - it's used in the beginning
// of the update procedure and after results are back from the automation scripts
//
// Both these points introduce external data that need to be checked fully in the same manner
// Both these points introduce external data that need to be checked fully in the same maner
procChanges := func(m *types.Module, upd *types.Record, old *types.Record) *types.RecordValueErrorSet {
// First sanitization
//
+70 -75
View File
@@ -2,28 +2,13 @@ package values
import (
"github.com/cortezaproject/corteza-server/compose/types"
"regexp"
"strconv"
"strings"
"time"
)
const (
boolTrue = "1"
boolFalse = "0"
)
type (
sanitizer struct {
}
)
var (
// value resembles something that can be true
truthy = regexp.MustCompile(`^(t(rue)?|y(es)?|1)$`)
// valeu resembles something that can be a reference
refy = regexp.MustCompile(`^[1-9](\d*)$`)
sanitizer struct{}
)
// Sanitizer initializes sanitizer
@@ -86,42 +71,42 @@ func (s sanitizer) Run(m *types.Module, vv types.RecordValueSet) (out types.Reco
// Per field type validators
switch strings.ToLower(f.Kind) {
case "bool":
v = s.sanitizeBool(v, f, m)
v = s.sBool(v, f, m)
case "datetime":
v = s.sanitizeDatetime(v, f, m)
case "email":
v = s.sanitizeEmail(v, f, m)
case "file":
v = s.sanitizeFile(v, f, m)
case "number":
v = s.sanitizeNumber(v, f, m)
case "record":
v = s.sanitizeRecord(v, f, m)
case "select":
v = s.sanitizeSelect(v, f, m)
case "string":
v = s.sanitizeString(v, f, m)
case "url":
v = s.sanitizeUrl(v, f, m)
case "user":
v = s.sanitizeUser(v, f, m)
v = s.sDatetime(v, f, m)
//case "email":
// v = s.sEmail(v, f, m)
//case "file":
// v = s.sFile(v, f, m)
//case "number":
// v = s.sNumber(v, f, m)
//case "record":
// v = s.sRecord(v, f, m)
//case "select":
// v = s.sSelect(v, f, m)
//case "string":
// v = s.sString(v, f, m)
//case "url":
// v = s.sUrl(v, f, m)
//case "user":
// v = s.sUser(v, f, m)
}
}
return
}
func (sanitizer) sanitizeBool(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue {
func (sanitizer) sBool(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue {
if truthy.MatchString(strings.ToLower(v.Value)) {
v.Value = boolTrue
v.Value = strBoolTrue
} else {
v.Value = boolFalse
v.Value = strBoolFalse
}
return v
}
func (sanitizer) sanitizeDatetime(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue {
func (sanitizer) sDatetime(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue {
var (
// input format set
inputFormats []string
@@ -131,26 +116,32 @@ func (sanitizer) sanitizeDatetime(v *types.RecordValue, f *types.ModuleField, m
)
if f.Options.Bool("onlyDate") {
of = "2006-01-02"
of = datetimeInputFormatDate
inputFormats = []string{
"2006-01-02",
datetimeInputFormatDate,
"02 Jan 06",
"Monday, 02-Jan-06",
"Mon, 02 Jan 2006",
"2019/_1/_2",
}
} else if f.Options.Bool("onlyTime") {
of = "15:04:05"
of = datetimeInputFormatTime
inputFormats = []string{
"15:04:05",
datetimeInputFormatTime,
"15:04",
"15:04:05Z07:00",
"15:04:05 MST",
"15:04:05 -0700",
"15:04 MST",
"15:04Z07:00",
"15:04 -0700",
time.Kitchen,
}
} else {
of = time.RFC3339
of = datetimeInputFormatFull
// date & time
inputFormats = []string{
time.RFC3339,
datetimeInputFormatFull,
time.RFC1123Z,
time.RFC1123,
time.RFC850,
@@ -176,34 +167,38 @@ func (sanitizer) sanitizeDatetime(v *types.RecordValue, f *types.ModuleField, m
return v
}
func (sanitizer) sanitizeEmail(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue {
return v
}
func (sanitizer) sanitizeFile(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue {
return v
}
func (sanitizer) sanitizeNumber(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue {
return v
}
func (sanitizer) sanitizeRecord(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue {
return v
}
func (sanitizer) sanitizeSelect(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue {
return v
}
func (sanitizer) sanitizeString(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue {
return v
}
func (sanitizer) sanitizeUrl(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue {
return v
}
func (sanitizer) sanitizeUser(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue {
return v
}
//
//func (sanitizer) sEmail(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue {
// // @todo extract from "name" <email> format
// return v
//}
//
//func (sanitizer) sFile(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue {
// return v
//}
//
//func (sanitizer) sNumber(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue {
// // @todo cut off the decimals / round up
// // @todo sanitize decimal/thousands dot/comma
// return v
//}
//
//func (sanitizer) sRecord(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue {
// return v
//}
//
//func (sanitizer) sSelect(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue {
// return v
//}
//
//func (sanitizer) sString(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue {
// return v
//}
//
//func (sanitizer) sUrl(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue {
// return v
//}
//
//func (sanitizer) sUser(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue {
// return v
//}
+37
View File
@@ -0,0 +1,37 @@
package values
import (
"regexp"
"time"
)
const (
strBoolTrue = "1"
strBoolFalse = "0"
datetimeInputFormatDate = "2006-01-02"
datetimeInputFormatTime = "15:04:05"
datetimeInputFormatFull = time.RFC3339
fieldOpt_Datetime_onlyDate = "onlyDate"
fieldOpt_Datetime_onlyTime = "onlyTime"
fieldOpt_Datetime_onlyFutureValues = "onlyFutureValues"
fieldOpt_Datetime_onlyPastValues = "onlyPastValues"
fieldOpt_Number_precision = "precision"
fieldOpt_Url_onlySecure = "onlySecure"
)
var (
// value resembles something that can be true
truthy = regexp.MustCompile(`^(t(rue)?|y(es)?|1)$`)
// value resembles something that can be a reference
refy = regexp.MustCompile(`^[1-9](\d*)$`)
)
func nowPtr() *time.Time {
now := time.Now()
return &now
}
+193 -54
View File
@@ -1,8 +1,14 @@
package values
import (
"errors"
"github.com/cortezaproject/corteza-server/compose/types"
"github.com/cortezaproject/corteza-server/pkg/slice"
"math/big"
"net/mail"
"net/url"
"strings"
"time"
)
// Validator package provides tooling to validate
@@ -13,45 +19,70 @@ import (
// is no need for such level of interaction and dynamic we require on the frontend
type (
UniqueChecker func(*types.Module, *types.ModuleField, *types.RecordValue) (uint64, error)
ReferenceChecker func(*types.Module, *types.ModuleField, *types.RecordValue) (bool, error)
UniqueChecker func(*types.RecordValue, *types.ModuleField, *types.Module) (uint64, error)
ReferenceChecker func(*types.RecordValue, *types.ModuleField, *types.Module) (bool, error)
validator struct {
uniqueCheckerFn UniqueChecker
recordRefCheckerFn ReferenceChecker
userRefCheckerFn ReferenceChecker
fileRefCheckerFn ReferenceChecker
now func() time.Time
}
)
func makeInternalErr(field string, err error) types.RecordValueError {
return types.RecordValueError{Kind: "internal", Message: err.Error(), Meta: map[string]interface{}{"field": field}}
func makeInternalErr(field *types.ModuleField, err error) types.RecordValueError {
return types.RecordValueError{Kind: "internal", Message: err.Error(), Meta: map[string]interface{}{"field": field.Name}}
}
func makeEmptyErr(field string) types.RecordValueError {
return types.RecordValueError{Kind: "empty", Meta: map[string]interface{}{"field": field}}
func makeEmptyErr(field *types.ModuleField) types.RecordValueError {
return types.RecordValueError{Kind: "empty", Meta: map[string]interface{}{"field": field.Name}}
}
func makeDuplicateValueInSetErr(field string) types.RecordValueError {
return types.RecordValueError{Kind: "duplicateValueInSet", Meta: map[string]interface{}{"field": field}}
func makeInvalidValueErr(field *types.ModuleField, value string) types.RecordValueError {
return types.RecordValueError{Kind: "invalidValue", Meta: map[string]interface{}{"field": field.Name, "value": value}}
}
func makeDuplicateValueErr(field string, recordID uint64) types.RecordValueError {
return types.RecordValueError{Kind: "duplicateValue", Meta: map[string]interface{}{"recordID": recordID, "field": field}}
func makeInvalidRefErr(field *types.ModuleField, ref uint64) types.RecordValueError {
return types.RecordValueError{Kind: "invalidRef", Meta: map[string]interface{}{"field": field.Name, "ref": ref}}
}
func makeDuplicateValueInSetErr(field *types.ModuleField, value string) types.RecordValueError {
return types.RecordValueError{Kind: "duplicateValueInSet", Meta: map[string]interface{}{"field": field.Name, "value": value}}
}
func makeDuplicateValueErr(field *types.ModuleField, recordID uint64) types.RecordValueError {
return types.RecordValueError{Kind: "duplicateValue", Meta: map[string]interface{}{"field": field.Name, "recordID": recordID}}
}
// Simple wrapper for easier error returning from validation functions
func e2s(ee ...types.RecordValueError) []types.RecordValueError {
return ee
}
func Validator() *validator {
return &validator{}
return &validator{
now: func() time.Time { return time.Now() },
}
}
func (vldtr validator) UniqueChecker(fn UniqueChecker) {
func (vldtr *validator) UniqueChecker(fn UniqueChecker) {
vldtr.uniqueCheckerFn = fn
}
func (vldtr validator) RecordRefChecker(fn ReferenceChecker) {
func (vldtr *validator) RecordRefChecker(fn ReferenceChecker) {
vldtr.recordRefCheckerFn = fn
}
func (vldtr validator) UserRefChecker(fn ReferenceChecker) {
func (vldtr *validator) UserRefChecker(fn ReferenceChecker) {
vldtr.userRefCheckerFn = fn
}
func (vldtr *validator) FileRefChecker(fn ReferenceChecker) {
vldtr.fileRefCheckerFn = fn
}
// Run validates record and it's values against module & module fields options
//
//
@@ -73,13 +104,13 @@ fields:
if f.Required {
if len(vv) == 0 {
out.Push(makeEmptyErr(f.Name))
out.Push(makeEmptyErr(f))
continue fields
}
for _, v := range vv {
if len(v.Value) == 0 || (f.IsRef() && v.Ref == 0) {
out.Push(makeEmptyErr(f.Name))
out.Push(makeEmptyErr(f))
continue fields
}
}
@@ -89,7 +120,7 @@ fields:
flipped := make(map[string]bool)
for _, v := range vv {
if flipped[v.Value] {
out.Push(makeDuplicateValueInSetErr(f.Name))
out.Push(makeDuplicateValueInSetErr(f, v.Value))
continue fields
}
@@ -99,32 +130,42 @@ fields:
}
for _, v := range r.Values {
if !v.IsUpdated() || v.IsUpdated() {
// We'll validate only updated (and non-deleted) values
continue
}
if f = m.Fields.FindByName(v.Name); f == nil {
continue
}
if v.Value == "" {
// Nothing to do with empty value
return nil
}
// Per field type validators
switch strings.ToLower(f.Kind) {
case "bool":
out.Push(vldtr.validateBoolFieldKind(v, f, r, m)...)
out.Push(vldtr.vBool(v, f, r, m)...)
case "datetime":
out.Push(vldtr.validateDatetimeFieldKind(v, f, r, m)...)
out.Push(vldtr.vDatetime(v, f, r, m)...)
case "email":
out.Push(vldtr.validateEmailFieldKind(v, f, r, m)...)
out.Push(vldtr.vEmail(v, f, r, m)...)
case "file":
out.Push(vldtr.validateFileFieldKind(v, f, r, m)...)
out.Push(vldtr.vFile(v, f, r, m)...)
case "number":
out.Push(vldtr.validateNumberFieldKind(v, f, r, m)...)
out.Push(vldtr.vNumber(v, f, r, m)...)
case "record":
out.Push(vldtr.validateRecordFieldKind(v, f, r, m)...)
out.Push(vldtr.vRecord(v, f, r, m)...)
case "select":
out.Push(vldtr.validateSelectFieldKind(v, f, r, m)...)
case "string":
out.Push(vldtr.validateStringFieldKind(v, f, r, m)...)
out.Push(vldtr.vSelect(v, f, r, m)...)
//case "string":
// out.Push(vldtr.vString(v, f, r, m)...)
case "url":
out.Push(vldtr.validateUrlFieldKind(v, f, r, m)...)
out.Push(vldtr.vUrl(v, f, r, m)...)
case "user":
out.Push(vldtr.validateUserFieldKind(v, f, r, m)...)
out.Push(vldtr.vUser(v, f, r, m)...)
}
}
@@ -140,11 +181,11 @@ fields:
continue
}
duplicateRecordID, err := vldtr.uniqueCheckerFn(m, f, v)
duplicateRecordID, err := vldtr.uniqueCheckerFn(v, f, m)
if err != nil {
out.Push(makeInternalErr(f.Name, err))
out.Push(makeInternalErr(f, err))
} else if duplicateRecordID > 0 && duplicateRecordID != r.ID {
out.Push(makeDuplicateValueErr(f.Name, duplicateRecordID))
out.Push(makeDuplicateValueErr(f, duplicateRecordID))
}
}
@@ -155,53 +196,151 @@ fields:
return out
}
func (vldtr validator) validateBoolFieldKind(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
// @todo must be 1 or 0
func (vldtr validator) vBool(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
if v.Value == "" {
return nil
}
if v.Value != strBoolTrue && v.Value != strBoolFalse {
return e2s(makeInvalidValueErr(f, v.Value))
}
return nil
}
func (vldtr validator) validateDatetimeFieldKind(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
// @todo must be datetime, UTC!
// @todo how do we check past/future (no info about previous value)
func (vldtr validator) vDatetime(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
var (
inputFormat string
t time.Time
err error
// We'll validate against this value
refTime = vldtr.now()
)
if f.Options.Bool(fieldOpt_Datetime_onlyDate) {
inputFormat = datetimeInputFormatDate
// Round down ref time to midnight
refTime = time.Date(refTime.Year(), refTime.Month(), refTime.Day(), 0, 0, 0, 0, refTime.Location())
} else if f.Options.Bool(fieldOpt_Datetime_onlyTime) {
inputFormat = datetimeInputFormatTime
// Round down ref time to day one
refTime = time.Date(0, 1, 1, refTime.Hour(), refTime.Minute(), refTime.Second(), refTime.Nanosecond(), refTime.Location())
} else {
inputFormat = datetimeInputFormatFull
}
t, err = time.Parse(inputFormat, v.Value)
if err != nil {
return e2s(makeInvalidValueErr(f, v.Value))
}
if f.Options.Bool(fieldOpt_Datetime_onlyFutureValues) {
if !t.After(refTime) {
return e2s(makeInvalidValueErr(f, v.Value))
}
} else if f.Options.Bool(fieldOpt_Datetime_onlyPastValues) {
if !t.Before(refTime) {
return e2s(makeInvalidValueErr(f, v.Value))
}
}
// @todo check past/future
return nil
}
func (vldtr validator) validateEmailFieldKind(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
// @todo must be an email
func (vldtr validator) vEmail(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
if _, err := mail.ParseAddress(v.Value); err != nil {
return e2s(makeInvalidValueErr(f, v.Value))
}
return nil
}
func (vldtr validator) validateFileFieldKind(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
// @todo file is a Ref!
// @todo we can check for uniquenes (as we do for other refs)
func (vldtr validator) vFile(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
if ok, err := vldtr.fileRefCheckerFn(v, f, m); err != nil {
return e2s(makeInternalErr(f, err))
} else if !ok {
return e2s(makeInvalidRefErr(f, v.Ref))
}
return nil
}
func (vldtr validator) validateNumberFieldKind(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
// @todo make sure it is a number and cut decimals (precision)
func (vldtr validator) vNumber(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
var (
precision = uint(f.Options.Int64Def(fieldOpt_Number_precision, 2))
)
if _, _, err := big.ParseFloat(v.Value, 0, precision, big.ToNearestEven); err != nil {
return e2s(makeInvalidValueErr(f, v.Value))
}
return nil
}
func (vldtr validator) validateRecordFieldKind(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
// @todo record is a ref
func (vldtr validator) vRecord(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
if ok, err := vldtr.recordRefCheckerFn(v, f, m); err != nil {
return e2s(makeInternalErr(f, err))
} else if !ok {
return e2s(makeInvalidRefErr(f, v.Ref))
}
return nil
}
func (vldtr validator) validateSelectFieldKind(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
// @todo validate v.Value against f.Options.Strings("options")
func (vldtr validator) vSelect(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
// @todo validate v.Value against
var (
options = f.Options["options"]
sbm = make(map[string]bool)
)
if oo, ok := options.([]string); ok {
sbm = slice.ToStringBoolMap(oo)
} else if oo, ok := options.([]map[string]string); ok {
for _, o := range oo {
sbm[o["value"]] = true
}
} else if options == nil {
return nil
} else {
return e2s(makeInternalErr(f, errors.New("invalid select options definition")))
}
if !sbm[v.Value] {
return e2s(makeInvalidValueErr(f, v.Value))
}
return nil
}
func (vldtr validator) validateStringFieldKind(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
//func (vldtr validator) vString(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
// return nil
//}
func (vldtr validator) vUrl(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
if p, err := url.Parse(v.Value); err != nil {
return e2s(makeInvalidValueErr(f, v.Value))
} else if p.Scheme == "" || p.Host == "" {
return e2s(makeInvalidValueErr(f, v.Value))
} else if f.Options.Bool(fieldOpt_Url_onlySecure) && p.Scheme != "https" {
return e2s(makeInvalidValueErr(f, v.Value))
}
return nil
}
func (vldtr validator) validateUrlFieldKind(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
// @todo must be URL
return nil
}
func (vldtr validator) vUser(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
if ok, err := vldtr.userRefCheckerFn(v, f, m); err != nil {
return e2s(makeInternalErr(f, err))
} else if !ok {
return e2s(makeInvalidRefErr(f, v.Ref))
}
func (vldtr validator) validateUserFieldKind(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
// @todo user is a ref
return nil
}
+220
View File
@@ -0,0 +1,220 @@
package values
import (
"github.com/cortezaproject/corteza-server/compose/types"
"reflect"
"testing"
"time"
)
func Test_validator_vDatetime(t *testing.T) {
var (
vldtr = validator{
now: func() time.Time {
t, err := time.Parse(time.RFC3339, "2020-02-20T10:10:10Z")
if err != nil {
panic(err)
}
return t
},
}
tests = []struct {
name string
val string
opt types.ModuleFieldOptions
want []types.RecordValueError
}{
{
name: "unparsable",
val: "unparsable",
want: e2s(types.RecordValueError{Kind: "invalidValue", Meta: map[string]interface{}{"field": "", "value": "unparsable"}}),
},
{
name: "valid datetime value",
val: "2020-02-20T10:10:10Z",
},
{
name: "valid date value",
val: "2020-02-20",
opt: types.ModuleFieldOptions{fieldOpt_Datetime_onlyDate: true},
},
{
name: "valid time value",
val: "10:10:10",
opt: types.ModuleFieldOptions{fieldOpt_Datetime_onlyTime: true},
},
{
name: "valid future datetime value",
val: "2021-02-20T10:10:10Z",
opt: types.ModuleFieldOptions{fieldOpt_Datetime_onlyFutureValues: true},
},
{
name: "valid future date value",
val: "2021-02-20",
opt: types.ModuleFieldOptions{fieldOpt_Datetime_onlyFutureValues: true, fieldOpt_Datetime_onlyDate: true},
},
{
name: "valid future time value",
val: "11:10:10",
opt: types.ModuleFieldOptions{fieldOpt_Datetime_onlyFutureValues: true, fieldOpt_Datetime_onlyTime: true},
},
{
name: "valid past datetime value",
val: "2019-02-20T10:10:10Z",
opt: types.ModuleFieldOptions{fieldOpt_Datetime_onlyPastValues: true},
},
{
name: "valid past date value",
val: "2019-02-20",
opt: types.ModuleFieldOptions{fieldOpt_Datetime_onlyPastValues: true, fieldOpt_Datetime_onlyDate: true},
},
{
name: "valid past time value",
val: "09:10:10",
opt: types.ModuleFieldOptions{fieldOpt_Datetime_onlyPastValues: true, fieldOpt_Datetime_onlyTime: true},
},
}
)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := vldtr.vDatetime(&types.RecordValue{Value: tt.val}, &types.ModuleField{Options: tt.opt}, nil, nil); !reflect.DeepEqual(got, tt.want) {
t.Errorf("vDatetime() = %+v, want %+v", got, tt.want)
}
})
}
}
func Test_validator_vNumber(t *testing.T) {
var (
vldtr = validator{}
tests = []struct {
name string
val string
opt types.ModuleFieldOptions
want []types.RecordValueError
}{
{
name: "unparsable",
val: "unparsable",
want: e2s(types.RecordValueError{Kind: "invalidValue", Meta: map[string]interface{}{"field": "", "value": "unparsable"}}),
},
{
name: "valid number value",
val: "42",
},
{
name: "valid number value",
val: "42.123",
},
{
name: "valid number value",
val: "412412412.322894325892365",
},
}
)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := vldtr.vNumber(&types.RecordValue{Value: tt.val}, &types.ModuleField{Options: tt.opt}, nil, nil); !reflect.DeepEqual(got, tt.want) {
t.Errorf("vNumber() = %+v, want %+v", got, tt.want)
}
})
}
}
func Test_validator_vUrl(t *testing.T) {
var (
vldtr = validator{}
tests = []struct {
name string
val string
opt types.ModuleFieldOptions
want []types.RecordValueError
}{
{
name: "invalid-url",
val: "invalid-url",
want: e2s(types.RecordValueError{Kind: "invalidValue", Meta: map[string]interface{}{"field": "", "value": "invalid-url"}}),
},
{
name: "valid value",
val: "https://crust.tech/",
},
}
)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := vldtr.vUrl(&types.RecordValue{Value: tt.val}, &types.ModuleField{Options: tt.opt}, nil, nil); !reflect.DeepEqual(got, tt.want) {
t.Errorf("vUrl() = %+v, want %+v", got, tt.want)
}
})
}
}
func Test_validator_vEmail(t *testing.T) {
var (
vldtr = validator{}
tests = []struct {
name string
val string
opt types.ModuleFieldOptions
want []types.RecordValueError
}{
{
name: "unparsable",
val: "un pars able",
want: e2s(types.RecordValueError{Kind: "invalidValue", Meta: map[string]interface{}{"field": "", "value": "un pars able"}}),
},
{
name: "valid value",
val: "qa@crust.tech",
},
}
)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := vldtr.vEmail(&types.RecordValue{Value: tt.val}, &types.ModuleField{Options: tt.opt}, nil, nil); !reflect.DeepEqual(got, tt.want) {
t.Errorf("vEmail() = %+v, want %+v", got, tt.want)
}
})
}
}
func Test_validator_vSelect(t *testing.T) {
var (
vldtr = validator{}
tests = []struct {
name string
val string
opt types.ModuleFieldOptions
want []types.RecordValueError
}{
{
name: "unparsable",
val: "dummy",
},
{
name: "valid value",
val: "crust",
opt: types.ModuleFieldOptions{"options": []string{"crust", "corteza"}},
},
{
name: "valid value",
val: "the rest",
opt: types.ModuleFieldOptions{"options": []string{"crust", "corteza"}},
want: e2s(types.RecordValueError{Kind: "invalidValue", Meta: map[string]interface{}{"field": "", "value": "the rest"}}),
},
}
)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := vldtr.vSelect(&types.RecordValue{Value: tt.val}, &types.ModuleField{Options: tt.opt}, nil, nil); !reflect.DeepEqual(got, tt.want) {
t.Errorf("vSelect() = %+v, want %+v", got, tt.want)
}
})
}
}
+14
View File
@@ -47,6 +47,20 @@ func (opt ModuleFieldOptions) Bool(key string) bool {
return false
}
func (opt ModuleFieldOptions) Int64(key string) int64 {
return opt.Int64Def(key, 0)
}
func (opt ModuleFieldOptions) Int64Def(key string, def int64) int64 {
if _, has := opt[key]; has {
if n, ok := opt[key].(int64); ok {
return n
}
}
return def
}
// Strings returns option value for key as slice of strings
//
// Invalid, non-existing are returned as nil