snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s

This commit is contained in:
upstream-mirror
2026-09-18 01:22:23 +05:00
commit 7e385fc126
8391 changed files with 1766236 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
package values
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/modern-go/reflect2"
)
func makeInvalidExprErr(field *types.ModuleField, expr string, err error) types.RecordValueError {
return types.RecordValueError{
Kind: "valueExpression",
Message: fmt.Sprintf("invalid expression %q: %v", expr, err.Error()),
Meta: map[string]interface{}{"field": field.Name},
}
}
func makeExprEvalErr(field *types.ModuleField, expr string, err error) types.RecordValueError {
return types.RecordValueError{
Kind: "valueExpression",
Message: fmt.Sprintf("failed to evaluate formula expression %q: %v", expr, err.Error()),
Meta: map[string]interface{}{"field": field.Name},
}
}
func makeValueExprIncompErr(field *types.ModuleField) types.RecordValueError {
return types.RecordValueError{
Kind: "evaluatedValueIncompatible",
Message: "evaluated results incompatible",
Meta: map[string]interface{}{"field": field.Name},
}
}
// Expression evaluates expression in ModuleField.Expressions.Value and
// assigns results to the record on that field
func Expression(ctx context.Context, m *types.Module, r *types.Record, old *types.Record, rve *types.RecordValueErrorSet) {
var (
exprParser = expr.Parser()
scope = make(map[string]interface{})
reserved = map[string]bool{
"new": true,
"old": true,
}
)
// base scope with field=value(s) from new record
scope = r.Values.Dict(m.Fields)
// new record
r.SetModule(m)
scope["new"] = r.Dict()
if old != nil {
// old values on record (before update)
// this will not be set for new records
old.SetModule(m)
scope["old"] = old.Dict()
}
for _, f := range m.Fields {
if f.Expressions.ValueExpr == "" {
continue
}
expr := f.Expressions.ValueExpr
eval, err := exprParser.NewEvaluable(expr)
if err != nil {
rve.Push(makeInvalidExprErr(f, expr, err))
return
}
tmp, err := eval(ctx, scope)
if err != nil {
rve.Push(makeExprEvalErr(f, expr, err))
return
}
var strings []string
// When the expression results in null (nil) delete that record value.
// The below Values.Replace already does it so we simply ignore strings
if !reflect2.IsNil(tmp) {
if values, isSlice := tmp.([]interface{}); isSlice {
if !f.Multi {
rve.Push(makeValueExprIncompErr(f))
continue
}
strings = make([]string, len(values))
for i, value := range values {
strings[i] = sanitize(f, value)
}
} else {
if f.Multi {
rve.Push(makeValueExprIncompErr(f))
continue
}
strings = []string{sanitize(f, tmp)}
}
}
r.Values = r.Values.Replace(f.Name, strings...)
if !reserved[f.Name] {
// make sure we do not overrider reserved fields
scope[f.Name] = tmp
}
// Reset $new with updated data
r.SetModule(m)
scope["new"] = r.Dict()
}
}

View File

@@ -0,0 +1,179 @@
package values
import (
"context"
"testing"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/stretchr/testify/require"
)
func TestExpressions(t *testing.T) {
var (
ctx = context.Background()
// pairs = [<fieldname>, <value expression>, ...]
makeModule = func(pairs ...string) *types.Module {
var (
m = &types.Module{}
)
for i := 0; i < len(pairs); i += 3 {
f := &types.ModuleField{Name: pairs[i], Kind: pairs[i+1], Options: map[string]interface{}{}}
f.Expressions.ValueExpr = pairs[i+2]
m.Fields = append(m.Fields, f)
}
return m
}
)
t.Run("empty", func(t *testing.T) {
var (
req = require.New(t)
m = &types.Module{}
r = &types.Record{}
rve = &types.RecordValueErrorSet{}
)
Expression(ctx, m, r, nil, rve)
req.True(rve.IsValid())
})
t.Run("string", func(t *testing.T) {
var (
req = require.New(t)
m = makeModule("f1", "String", `"abc"`)
r = &types.Record{}
rve = &types.RecordValueErrorSet{}
)
Expression(ctx, m, r, nil, rve)
req.Truef(rve.IsValid(), "%v", rve.Set)
req.Equal("abc", r.Values.Get("f1", 0).Value)
})
t.Run("use fields", func(t *testing.T) {
var (
req = require.New(t)
m = makeModule("f1", "String", `fname + " " + lname`)
r = &types.Record{}
rve = &types.RecordValueErrorSet{}
)
m.Fields = append(m.Fields, &types.ModuleField{Name: "fname", Kind: "String"})
m.Fields = append(m.Fields, &types.ModuleField{Name: "lname", Kind: "String"})
r.Values = r.Values.Replace("fname", "Cor")
r.Values = r.Values.Replace("lname", "Teza")
Expression(ctx, m, r, nil, rve)
req.Truef(rve.IsValid(), "%v", rve.Set)
req.Equal("Cor Teza", r.Values.Get("f1", 0).Value)
})
t.Run("math", func(t *testing.T) {
var (
req = require.New(t)
m = makeModule("f1", "Number", `n1 * n2 * 1.511`)
r = &types.Record{}
rve = &types.RecordValueErrorSet{}
)
m.Fields.FindByName("f1").Options.SetPrecision(1)
m.Fields = append(m.Fields, &types.ModuleField{Name: "n1", Kind: "Number"})
m.Fields = append(m.Fields, &types.ModuleField{Name: "n2", Kind: "Number"})
r.Values = r.Values.Replace("n1", "10")
r.Values = r.Values.Replace("n2", "20")
Expression(ctx, m, r, nil, rve)
req.Truef(rve.IsValid(), "%v", rve.Set)
req.Equal("302.2", r.Values.Get("f1", 0).Value)
})
t.Run("booleans", func(t *testing.T) {
var (
m = makeModule("f1", "string", `b1 ? "yes" : "no"`)
r = &types.Record{}
rve = &types.RecordValueErrorSet{}
)
m.Fields = append(m.Fields, &types.ModuleField{Name: "b1", Kind: "Bool"})
t.Run("true", func(t *testing.T) {
var req = require.New(t)
r.Values = r.Values.Replace("b1", "true")
Expression(ctx, m, r, nil, rve)
req.Truef(rve.IsValid(), "%v", rve.Set)
req.Equal("yes", r.Values.Get("f1", 0).Value)
})
t.Run("false", func(t *testing.T) {
var req = require.New(t)
r.Values = r.Values.Replace("b1", "false")
Expression(ctx, m, r, nil, rve)
req.Truef(rve.IsValid(), "%v", rve.Set)
req.Equal("no", r.Values.Get("f1", 0).Value)
})
})
t.Run("old vs new", func(t *testing.T) {
var (
req = require.New(t)
m = makeModule("f1", "String", `new.values.test == old.values.test ? "same":"different"`)
new = &types.Record{}
old = &types.Record{}
rve = &types.RecordValueErrorSet{}
)
m.Fields = append(m.Fields, &types.ModuleField{Name: "test", Kind: "String"})
new.Values = new.Values.Replace("test", "a")
old.Values = old.Values.Replace("test", "a")
Expression(ctx, m, new, old, rve)
req.Truef(rve.IsValid(), "%v", rve.Set)
req.Equal("same", new.Values.Get("f1", 0).Value)
})
t.Run("multi value expressions", func(t *testing.T) {
var (
req = require.New(t)
m = makeModule("f1", "String", `[t1,t2,t3]`)
new = &types.Record{}
old = &types.Record{}
rve = &types.RecordValueErrorSet{}
)
m.Fields.FindByName("f1").Multi = true
m.Fields = append(m.Fields, &types.ModuleField{Name: "t1", Kind: "String"})
m.Fields = append(m.Fields, &types.ModuleField{Name: "t2", Kind: "String"})
m.Fields = append(m.Fields, &types.ModuleField{Name: "t3", Kind: "String"})
new.Values = new.Values.Replace("t1", "a")
new.Values = new.Values.Replace("t2", "b")
new.Values = new.Values.Replace("t3", "c")
Expression(ctx, m, new, old, rve)
req.Truef(rve.IsValid(), "%v", rve.Set)
req.Equal("a", new.Values.Get("f1", 0).Value)
req.Equal("b", new.Values.Get("f1", 1).Value)
req.Equal("c", new.Values.Get("f1", 2).Value)
})
t.Run("omit record value when expr returns null (nil)", func(t *testing.T) {
var (
req = require.New(t)
m = makeModule("f1", "String", `null`)
r = &types.Record{}
rve = &types.RecordValueErrorSet{}
)
Expression(ctx, m, r, nil, rve)
req.True(rve.IsValid())
req.Empty(r.Values)
})
}

View File

@@ -0,0 +1,126 @@
package values
import (
"strings"
"time"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/logger"
"go.uber.org/zap"
)
type (
formatter struct{}
)
// Formatter initializes formatter
//
// Not really needed, following pattern in the package
func Formatter() *formatter {
return &formatter{}
}
func (f formatter) Run(m *types.Module, vv types.RecordValueSet) types.RecordValueSet {
var (
exprParser = expr.Parser()
log = logger.Default().
With(logger.Uint64("module", m.ID))
)
for _, v := range vv {
fld := m.Fields.FindByName(v.Name)
if fld == nil {
// Unknown field,
// if it is not handled before,
// formatter does not care about it
continue
}
if fld.Expressions.ValueExpr != "" {
// do not do any validation if field has value expression!
continue
}
if !(fld.Expressions.DisableDefaultFormatters && len(fld.Expressions.Formatters) > 0) {
// Per field type validators
switch strings.ToLower(fld.Kind) {
case "bool":
v = f.fBool(v)
case "datetime":
v = f.fDatetime(v, fld)
}
}
if len(fld.Expressions.Formatters) > 0 {
for _, expr := range fld.Expressions.Formatters {
rval, err := exprParser.Evaluate(expr, map[string]interface{}{"value": v.Value})
if err != nil {
log.Error(
"failed to evaluate sanitizer expression",
zap.String("field", fld.Name),
zap.String("expr", expr),
zap.Error(err),
)
continue
}
v.Value = sanitize(fld, rval)
}
}
}
return vv
}
// Boolean values are outputed as "1" (true) and "" (false)
func (formatter) fBool(v *types.RecordValue) *types.RecordValue {
if v.Value != strBoolTrue {
v.Value = ""
}
return v
}
func (formatter) fDatetime(v *types.RecordValue, f *types.ModuleField) *types.RecordValue {
var (
// database formats
dbFormats []string
// output format
internalFormat string
)
if f.Options.Bool("onlyDate") {
internalFormat = datetimeInternalFormatDate
dbFormats = []string{
datetimeInternalFormatDate,
}
} else if f.Options.Bool("onlyTime") {
internalFormat = datetimeIntenralFormatTime
dbFormats = []string{
datetimeIntenralFormatTime,
"15:04",
}
} else {
internalFormat = datetimeInternalFormatFull
// date & time
dbFormats = []string{
datetimeInternalFormatFull,
"2006-01-02 15:04:05",
"2006-01-02 15:04",
}
}
for _, format := range dbFormats {
parsed, err := time.Parse(format, v.Value)
if err == nil {
v.Value = parsed.UTC().Format(internalFormat)
return v
}
}
v.Value = ""
return v
}

View File

@@ -0,0 +1,285 @@
package values
import (
"fmt"
"strconv"
"strings"
"time"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/logger"
"github.com/cortezaproject/corteza/server/pkg/xss"
"github.com/spf13/cast"
"go.uber.org/zap"
"github.com/cortezaproject/corteza/server/compose/types"
)
type (
sanitizer struct{}
)
// Sanitizer initializes sanitizer
//
// Not really needed, following pattern in the package
func Sanitizer() *sanitizer {
return &sanitizer{}
}
// Run cleans up input data
// - fix multi-value order/place index
// - trim all the strings!
// - parse & format input values to match field specific -- nullify/falsify invalid
// - field kind specific, no errors raised, data is modified
//
// Existing data (when updating record) is not yet loaded at this point
func (s sanitizer) Run(m *types.Module, vv types.RecordValueSet) (out types.RecordValueSet) {
var (
exprParser = expr.Parser()
)
out = make([]*types.RecordValue, 0, len(vv))
for _, f := range m.Fields {
// Reorder and sanitize place value (no gaps)
//
// Values are ordered when received so we treat them like it
// and assign the appropriate place no.
var i = 0
for _, v := range vv.FilterByName(f.Name) {
if v.IsDeleted() {
continue
}
c := v.Clone()
c.Place = uint(i)
out = append(out, c)
i++
}
}
var (
f *types.ModuleField
kind string
log = logger.Default().
With(logger.Uint64("module", m.ID))
)
for _, v := range out {
f = m.Fields.FindByName(v.Name)
if f == nil {
// Unknown field,
// if it is not handled before,
// sanitizer does not care about it
continue
}
if f.Expressions.ValueExpr != "" {
// do not do any sanitization if field has value expression!
continue
}
if v.IsDeleted() || !v.Updated {
// Ignore unchanged and deleted
continue
}
kind = strings.ToLower(f.Kind)
if len(f.Expressions.Sanitizers) > 0 {
for _, expr := range f.Expressions.Sanitizers {
rval, err := exprParser.Evaluate(expr, map[string]interface{}{"value": v.Value})
if err != nil {
log.Error(
"failed to evaluate sanitizer expression",
zap.String("field", f.Name),
zap.String("expr", expr),
zap.Error(err),
)
continue
}
v.Value = sanitize(f, rval)
}
}
if kind != "string" {
// Trim all but string
v.Value = strings.TrimSpace(v.Value)
}
if f.IsRef() {
if refy.MatchString(v.Value) {
v.Ref, _ = strconv.ParseUint(v.Value, 10, 64)
}
if v.Ref == 0 {
v.Value = ""
}
}
v.Value = sanitize(f, v.Value)
}
return
}
func (s sanitizer) RunXSS(m *types.Module, vv types.RecordValueSet) types.RecordValueSet {
var (
f *types.ModuleField
)
for _, v := range vv {
f = m.Fields.FindByName(v.Name)
if f == nil {
// Unknown field,
// if it is not handled before,
// sanitizer does not care about it
continue
}
switch strings.ToLower(f.Kind) {
case "string":
v.Value = sString(v.Value)
}
}
return vv
}
func sBool(v interface{}) string {
switch c := v.(type) {
case bool:
if c {
return strBoolTrue
}
case string:
if truthy.MatchString(strings.ToLower(c)) {
return strBoolTrue
}
}
// Returning empty string here to align false value with everything else
return strBoolFalseAlt
}
func sDatetime(v interface{}, onlyDate, onlyTime bool) string {
var (
// input format set
inputFormats []string
// output format
internalFormat string
datetime = fmt.Sprintf("%v", v)
)
if onlyTime {
internalFormat = datetimeIntenralFormatTime
inputFormats = []string{
datetimeIntenralFormatTime,
"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 {
if onlyDate {
// In case only date is used, make sure we format it properly
internalFormat = datetimeInternalFormatDate
} else {
internalFormat = datetimeInternalFormatFull
}
// date & time
inputFormats = []string{
datetimeInternalFormatFull,
"2006-01-02T15:04:05", // iso8601 without timezone
time.RFC1123Z,
time.RFC1123,
time.RFC822Z,
time.RFC822,
time.RFC850,
time.ANSIC,
time.UnixDate,
time.RubyDate,
"2006-01-02 15:04:05.999999999 -0700 MST", // Time.String()
"2006-01-02",
"02 Jan 2006",
"2006-01-02T15:04:05-0700", // RFC3339 without timezone hh:mm colon
"2006-01-02 15:04:05 -07:00",
"2006-01-02 15:04:05 -0700",
"2006-01-02 15:04:05Z07:00", // RFC3339 without T
"2006-01-02 15:04:05Z0700", // RFC3339 without T or timezone hh:mm colon
"2006-01-02 15:04:05",
time.Kitchen,
time.Stamp,
time.StampMilli,
time.StampMicro,
time.StampNano,
datetimeInternalFormatDate,
"02 Jan 06",
"Monday, 02-Jan-06",
"Mon, 02 Jan 2006",
"2006/_1/_2",
}
}
for _, format := range inputFormats {
parsed, err := time.Parse(format, datetime)
if err == nil {
return parsed.UTC().Format(internalFormat)
}
}
return ""
}
func sNumber(num interface{}, p uint, s uint) string {
base, err := strconv.ParseFloat(fmt.Sprintf("%v", num), 64)
if err != nil {
return "0"
}
// Format the value to the desired precision
str := strconv.FormatFloat(base, 'f', int(s), 64)
// In case of fractures, remove trailing 0's
if strings.Contains(str, ".") {
str = strings.TrimRight(str, "0")
str = strings.TrimRight(str, ".")
}
return str
}
func sString(str interface{}) string {
base, err := cast.ToStringE(str)
if err != nil {
return ""
}
return xss.RichText(base)
}
// sanitize casts value to field kind format
func sanitize(f *types.ModuleField, v interface{}) string {
switch strings.ToLower(f.Kind) {
case "bool":
return sBool(v)
case "datetime":
v = sDatetime(v, f.Options.Bool("onlyDate"), f.Options.Bool("onlyTime"))
case "number":
// @todo !! This is temporary; precision and scale require a rework
v = sNumber(v, maxPrecision, f.Options.Precision())
case "string":
v = sString(v)
}
return fmt.Sprintf("%v", v)
}

View File

@@ -0,0 +1,402 @@
package values
import (
"fmt"
"reflect"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/cortezaproject/corteza/server/compose/types"
)
func Test_sanitizer_Run(t *testing.T) {
tests := []struct {
name string
kind string
options map[string]interface{}
input string
output string
outref uint64
}{
{
name: "numbers should be trimmed",
kind: "Number",
input: " 42 ",
output: "42",
},
{
name: "object reference should be processed",
kind: "Record",
input: " 133569629112020995 ",
output: "133569629112020995",
outref: 133569629112020995,
},
{
name: "object reference should be numeric",
kind: "Record",
input: " foo ",
output: "",
},
{
name: "user reference should be processed",
kind: "User",
input: " 133569629112020995 ",
output: "133569629112020995",
outref: 133569629112020995,
},
{
name: "user reference should be numeric",
kind: "User",
input: " foo ",
output: "",
},
{
name: "strings should be kept intact",
kind: "String",
input: " The answer ",
output: " The answer ",
},
{
name: "booleans should be converted (t)",
kind: "Bool",
input: "t",
output: "1",
},
{
name: "booleans should be converted (false)",
kind: "Bool",
input: "false",
output: "",
},
{
name: "booleans should be converted (garbage)",
kind: "Bool",
input: "%%#)%)')$)'",
output: "",
},
{
name: "dates should be converted to ISO",
kind: "DateTime",
input: "Mon Jan 2 15:04:05 2006",
output: "2006-01-02T15:04:05Z",
},
{
name: "dates should be converted to UTC",
kind: "DateTime",
input: "2020-03-02T20:20:20+05:00",
output: "2020-03-02T15:20:20Z",
},
{
name: "micro/mili seconds should be cut off",
kind: "DateTime",
input: "2020-03-11T11:20:08.471Z",
output: "2020-03-11T11:20:08Z",
},
{
name: "number space trim",
kind: "Number",
input: " 42 ",
output: "42",
},
{
name: "number negative",
kind: "Number",
input: "-42",
output: "-42",
},
{
name: "number positive",
kind: "Number",
input: "+42",
output: "42",
},
{
name: "number precision",
kind: "Number",
options: map[string]interface{}{"precision": 3},
input: "42.44455",
output: "42.445",
},
{
name: "number precision; bigger precision then provided fracture",
kind: "Number",
options: map[string]interface{}{"precision": 3},
input: "42.4",
output: "42.4",
},
{
name: "number precision; default to 0",
kind: "Number",
options: map[string]interface{}{},
input: "42.4",
output: "42",
},
{
name: "number precision; clamped between [0, 6]",
kind: "Number",
options: map[string]interface{}{"precision": 10},
input: "42.5555555555",
output: "42.555556",
},
{
name: "number precision; round",
kind: "Number",
options: map[string]interface{}{"precision": 0},
input: "41.6",
output: "42",
},
{
name: "number precision; trailing .00",
kind: "Number",
options: map[string]interface{}{"precision": 2},
input: "42.00",
output: "42",
},
{
name: "number precision; trailing .040",
kind: "Number",
options: map[string]interface{}{"precision": 2},
input: "42.040",
output: "42.04",
},
{
name: "string escaping; html",
kind: "String",
options: map[string]interface{}{},
input: "<span onerror=alert()>Title here</span>",
output: "<span>Title here</span>",
},
{
name: "string escaping; html a.href with javascript alert",
kind: "String",
options: map[string]interface{}{},
input: `<a href="javascript:alert('XSS1')" onmouseover="alert('XSS2')">XSS<a>`,
output: "XSS",
},
{
name: "string escaping; a.href with javascript",
kind: "String",
options: map[string]interface{}{},
input: `<a href="javascript:document.location='https://cortezaproject.org/'">XSS</A>`,
output: "XSS",
},
{
name: "string escaping; script with script",
kind: "String",
options: map[string]interface{}{},
input: `<script>document.write("<scri");</script>pt src="https://cortezaproject.org/script.js"></script>`,
output: `pt src="https://cortezaproject.org/script.js">`,
},
{
name: "string escaping; inline styles unchanged",
kind: "String",
options: map[string]interface{}{},
input: `<span style="background-color: #00ff00"><span style="color: #4a86e8">nasty looking content</span></span>`,
output: `<span style="background-color: #00ff00"><span style="color: #4a86e8">nasty looking content</span></span>`,
},
{
name: "string escaping; script with a",
kind: "String",
options: map[string]interface{}{},
input: `<script a=">'>" src="https://cortezaproject.org/xss.js"></script>`,
output: "",
},
{
name: "string escaping; meta with script",
kind: "String",
options: map[string]interface{}{},
input: `<meta http-equiv="set-cookie" content="<script>alert('xss')</script>">`,
output: "",
},
{
name: "string escaping; object",
kind: "String",
options: map[string]interface{}{},
input: `<object type="text/x-scriptlet" data="https://cortezaproject.org/xss.html"></object>`,
output: "",
},
{
name: "string escaping; base href",
kind: "String",
options: map[string]interface{}{},
input: `<base href="javascript:alert('xss');//">`,
output: "",
},
{
name: "string escaping; script",
kind: "String",
options: map[string]interface{}{},
input: `<!--[if gte ie 4]><script>alert('xss');</script><![endif]-->`,
output: "",
},
{
name: "string escaping; div",
kind: "String",
options: map[string]interface{}{},
input: `<div style="background-image: url(javascript:alert('XSS'))"></div>`,
output: "<div></div>",
},
{
name: "string escaping; frameset",
kind: "String",
options: map[string]interface{}{},
input: `<frameset><frame src="javascript:alert('XSS');"></frameset>`,
output: "",
},
{
name: "string escaping; iframe",
kind: "String",
options: map[string]interface{}{},
input: `<iframe src=# onmouseover="alert(document.cookie)"></iframe>`,
output: "",
},
{
name: "string escaping; meta",
kind: "String",
options: map[string]interface{}{},
input: `<meta http-equiv="refresh" content="0; url=https://;url=javascript:alert('xss');">`,
output: "",
},
{
name: "string escaping; br",
kind: "String",
options: map[string]interface{}{},
input: `<br size="&{alert('XSS')}">`,
output: "<br>",
},
{
name: "string escaping; bgsound",
kind: "String",
options: map[string]interface{}{},
input: `<bgsound src="javascript:alert('XSS');">`,
output: "",
},
{
name: "string escaping; input type image",
kind: "String",
options: map[string]interface{}{},
input: `<input type="image" src="javascript:alert('XSS');">`,
output: "",
},
{
name: "string escaping; style",
kind: "String",
options: map[string]interface{}{},
input: `<style>@import 'https://cortezaproject.org/xss.css';</style>`,
output: "",
},
{
name: "string escaping; link",
kind: "String",
options: map[string]interface{}{},
input: `<link rel="stylesheet" href="javascript:alert('xss');">`,
output: "",
},
{
name: "string escaping; html body onload event",
kind: "String",
options: map[string]interface{}{},
input: `<body onload=alert('XSS')>`,
output: "",
},
{
name: "string escaping; xss element",
kind: "String",
options: map[string]interface{}{},
input: `'';!--"<XSS>=&{()}`,
output: `'';!--"=&{()}`,
},
{
name: "string escaping; xss element",
kind: "String",
options: map[string]interface{}{},
input: `Hello <span class="><script src='https://cortezaproject.org/XSS.js'></script>">there</span> world.`,
output: "Hello <span>there</span> world.",
},
{
name: "string escaping; xss element",
kind: "String",
options: map[string]interface{}{},
input: `<tag1>cor<tag2></tag2>teza</tag1><tag1>server</tag1><tag2>123</tag2>`,
output: "cortezaserver123",
},
{
name: "string escaping; preserve necessary chars",
kind: "String",
options: map[string]interface{}{},
input: `a < b ; "'"`,
output: `a < b ; "'"`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := sanitizer{}
m := &types.Module{Fields: types.ModuleFieldSet{&types.ModuleField{Name: "testField", Kind: tt.kind, Options: tt.options}}}
v := types.RecordValueSet{&types.RecordValue{Name: "testField", Value: tt.input}}
o := types.RecordValueSet{&types.RecordValue{Name: "testField", Value: tt.output, Ref: tt.outref}}
// Need to mark values as updated to trigger sanitization.
v.SetUpdatedFlag(true)
o.SetUpdatedFlag(true)
if sanitized := s.Run(m, v); !reflect.DeepEqual(sanitized, o) {
t.Errorf("\ninput value:\n%v\n\nresult of sanitization:\n%v\n\nexpected:\n%v\n", tt.input, sanitized, o)
}
})
}
}
func TestSanitizerExpr(t *testing.T) {
tests := []struct {
name string
kind string
expr []string
input string
output string
outref uint64
}{
{
name: "cap numbers",
kind: "Number",
expr: []string{`value > 42 ? 42 : value`},
input: "43",
output: "42",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := sanitizer{}
m := &types.Module{Fields: types.ModuleFieldSet{&types.ModuleField{Name: "testField", Kind: tt.kind}}}
m.Fields.FindByName("testField").Expressions.Sanitizers = tt.expr
v := types.RecordValueSet{&types.RecordValue{Name: "testField", Value: tt.input}}
o := types.RecordValueSet{&types.RecordValue{Name: "testField", Value: tt.output, Ref: tt.outref}}
// Need to mark values as updated to trigger sanitization.
v.SetUpdatedFlag(true)
o.SetUpdatedFlag(true)
if sanitized := s.Run(m, v); !reflect.DeepEqual(sanitized, o) {
t.Errorf("\ninput value:\n%v\n\nresult of sanitization:\n%v\n\nexpected:\n%v\n", tt.input, sanitized, o)
}
})
}
}
func TestDatetimeSanitizer(t *testing.T) {
tests := []struct {
input interface{}
onlyDate, onlyTime bool
rval string
}{
{time.Date(1999, 9, 9, 9, 9, 9, 9, time.UTC), false, false, "1999-09-09T09:09:09Z"},
{"2021-03-23T20:21:15Z", false, false, "2021-03-23T20:21:15Z"},
{"2021-03-23T20:21:15+01:00", false, false, "2021-03-23T19:21:15Z"},
{"2021-03-23", true, false, "2021-03-23"},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("%v", tt.input), func(t *testing.T) {
assert.New(t).Equal(tt.rval, sDatetime(tt.input, tt.onlyDate, tt.onlyTime))
})
}
}

View File

@@ -0,0 +1,37 @@
package values
import (
"regexp"
"time"
)
const (
strBoolTrue = "1"
strBoolFalse = "0"
strBoolFalseAlt = ""
datetimeInternalFormatDate = "2006-01-02"
datetimeIntenralFormatTime = "15:04:05"
datetimeInternalFormatFull = time.RFC3339
fieldOpt_Datetime_onlyDate = "onlyDate"
fieldOpt_Datetime_onlyTime = "onlyTime"
fieldOpt_Datetime_onlyFutureValues = "onlyFutureValues"
fieldOpt_Datetime_onlyPastValues = "onlyPastValues"
fieldOpt_Url_onlySecure = "onlySecure"
maxPrecision = 15
)
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*)$`)
// value resembels something that can be parsed as ISO 8601
isoDaty = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}`)
hasTimezone = regexp.MustCompile(`(Z|\+\d{2}:\d{2})$`)
)

View File

@@ -0,0 +1,472 @@
package values
import (
"context"
"fmt"
"math/big"
"net/mail"
"net/url"
"strconv"
"strings"
"time"
"github.com/cortezaproject/corteza/server/pkg/locale"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/slice"
"github.com/cortezaproject/corteza/server/store"
)
// Validator package provides tooling to validate
// record and it's values against field constraints
//
// Structures and basic logic is similar to what we offer on the frontend
// (see corteza-js/validator package) but with less features as there
// is no need for such level of interaction and dynamic we require on the frontend
type (
UniqueChecker func(context.Context, store.Storer, *types.RecordValue, *types.ModuleField, *types.Module) (uint64, error)
ReferenceChecker func(context.Context, store.Storer, *types.RecordValue, *types.ModuleField, *types.Module) (bool, error)
localeService interface {
T(ctx context.Context, ns, key string, rr ...string) string
TResource(ctx context.Context, ns, key string, rr ...string) string
}
validator struct {
uniqueCheckerFn UniqueChecker
recordRefCheckerFn ReferenceChecker
userRefCheckerFn ReferenceChecker
fileRefCheckerFn ReferenceChecker
localeSvc localeService
now func() time.Time
}
)
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(ctx context.Context, field *types.ModuleField, ls localeService) types.RecordValueError {
return types.RecordValueError{
Kind: "empty",
Meta: map[string]interface{}{"field": field.Name},
Message: ls.T(ctx, "compose", "record-field.errors.empty"),
}
}
func makeInvalidValueErr(ctx context.Context, field *types.ModuleField, value string, ls localeService) types.RecordValueError {
return types.RecordValueError{
Kind: "invalidValue",
Meta: map[string]interface{}{"field": field.Name, "value": value},
Message: ls.T(ctx, "compose", "record-field.errors.invalidValue"),
}
}
func makeInvalidRefErr(ctx context.Context, field *types.ModuleField, ref uint64, ls localeService) types.RecordValueError {
return types.RecordValueError{
Kind: "invalidRef",
Meta: map[string]interface{}{"field": field.Name, "ref": ref},
Message: ls.T(ctx, "compose", "record-field.errors.invalidRef"),
}
}
func makeDuplicateValueInSetErr(ctx context.Context, field *types.ModuleField, value string, ls localeService) types.RecordValueError {
return types.RecordValueError{
Kind: "duplicateValueInSet",
Meta: map[string]interface{}{"field": field.Name, "value": value},
Message: ls.T(ctx, "compose", "record-field.errors.duplicateValueInSet"),
}
}
func makeDuplicateValueErr(ctx context.Context, field *types.ModuleField, recordID uint64, ls localeService) types.RecordValueError {
return types.RecordValueError{
Kind: "duplicateValue",
Meta: map[string]interface{}{"field": field.Name, "recordID": recordID},
Message: ls.T(ctx, "compose", "record-field.errors.duplicateValue"),
}
}
// Simple wrapper for easier error returning from validation functions
func e2s(ee ...types.RecordValueError) []types.RecordValueError {
return ee
}
func Validator() *validator {
return &validator{
now: func() time.Time { return time.Now() },
localeSvc: locale.Global(),
}
}
func (vldtr *validator) UniqueChecker(fn UniqueChecker) {
vldtr.uniqueCheckerFn = fn
}
func (vldtr *validator) RecordRefChecker(fn ReferenceChecker) {
vldtr.recordRefCheckerFn = fn
}
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
//
// Validation is done in phases for optimal resource usage:
// - check if required values are present
// - check for unique-multi-value in multi value fields
// - field-kind specific validation on all values
// - unique check on all values
func (vldtr validator) Run(ctx context.Context, s store.Storer, m *types.Module, r *types.Record) (out *types.RecordValueErrorSet) {
var (
f *types.ModuleField
valParser = expr.Parser()
valDict = r.Values.Dict(m.Fields)
)
out = &types.RecordValueErrorSet{}
fields:
for _, f := range m.Fields {
vv := r.Values.FilterByName(f.Name)
if f.Required {
if len(vv) == 0 {
out.Push(makeEmptyErr(ctx, f, vldtr.localeSvc))
continue fields
}
for _, v := range vv {
if len(v.Value) == 0 || (f.IsRef() && v.Ref == 0) {
out.Push(makeEmptyErr(ctx, f, vldtr.localeSvc))
continue fields
}
}
}
if f.Multi && f.Options.IsUniqueMultiValue() {
flipped := make(map[string]bool)
for _, v := range vv {
if flipped[v.Value] {
out.Push(makeDuplicateValueInSetErr(ctx, f, v.Value, vldtr.localeSvc))
continue fields
}
flipped[v.Value] = true
}
}
}
for _, v := range r.Values {
if !v.IsUpdated() || v.IsDeleted() {
// We'll validate only updated (and non-deleted) values
continue
}
if f = m.Fields.FindByName(v.Name); f == nil {
continue
}
if f.Expressions.ValueExpr != "" {
// do not do any validation if field has value expression!
continue
}
if !(f.Expressions.DisableDefaultValidators && len(f.Expressions.Validators) > 0) {
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.vBool(ctx, v, f, r, m)...)
case "datetime":
out.Push(vldtr.vDatetime(ctx, v, f, r, m)...)
case "email":
out.Push(vldtr.vEmail(ctx, v, f, r, m)...)
case "file":
out.Push(vldtr.vFile(ctx, s, v, f, r, m)...)
case "number":
out.Push(vldtr.vNumber(ctx, v, f, r, m)...)
case "record":
out.Push(vldtr.vRecord(ctx, s, v, f, r, m)...)
case "select":
out.Push(vldtr.vSelect(ctx, v, f, r, m)...)
//case "string":
// out.Push(vldtr.vString(v, f, r, m)...)
case "url":
out.Push(vldtr.vUrl(ctx, v, f, r, m)...)
case "user":
out.Push(vldtr.vUser(ctx, s, v, f, r, m)...)
}
}
if len(f.Expressions.Validators) > 0 {
for i, cv := range f.Expressions.Validators {
eval, err := valParser.NewEvaluable(cv.Test)
if err != nil {
out.Push(makeInternalErr(f, err))
break
}
invalid, err := eval.EvalBool(ctx, map[string]interface{}{
"value": v.Value,
"oldValue": v.OldValue,
"values": valDict,
})
if err != nil {
out.Push(makeInternalErr(f, err))
break
}
if invalid {
message := cv.Error
// In case tests mit this for easier testing; falling back to whatever
// the field validator has stored.
if vldtr.localeSvc != nil {
validatorID := locale.ContentID(cv.ValidatorID, i)
rpl := strings.NewReplacer(
"{{validatorID}}", strconv.FormatUint(validatorID, 10),
)
k := rpl.Replace(types.LocaleKeyModuleFieldExpressionValidatorValidatorIDError.Path)
message = vldtr.localeSvc.TResource(ctx, f.ResourceTranslation(), k)
}
out.Push(types.RecordValueError{
Kind: "error",
Message: message,
Meta: map[string]interface{}{"field": f.Name}},
)
// break at first failed test
break
}
}
}
}
// This is the most resource-heavy operation
// we'll do in at the end
for _, v := range r.Values {
if f = m.Fields.FindByName(v.Name); f == nil {
continue
}
if !f.Options.IsUnique() {
// Only interested in unique fields
continue
}
if vldtr.uniqueCheckerFn == nil {
return nil
}
duplicateRecordID, err := vldtr.uniqueCheckerFn(ctx, s, v, f, m)
if err != nil {
out.Push(makeInternalErr(f, err))
} else if duplicateRecordID > 0 && duplicateRecordID != r.ID {
out.Push(makeDuplicateValueErr(ctx, f, duplicateRecordID, vldtr.localeSvc))
}
}
if out.IsValid() {
return nil
}
return out
}
func (vldtr validator) vBool(ctx context.Context, 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 && v.Value != strBoolFalseAlt {
return e2s(makeInvalidValueErr(ctx, f, v.Value, vldtr.localeSvc))
}
return nil
}
func (vldtr validator) vDatetime(ctx context.Context, 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 = datetimeInternalFormatDate
// 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 = datetimeIntenralFormatTime
// 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 = datetimeInternalFormatFull
}
t, err = time.Parse(inputFormat, v.Value)
if err != nil {
return e2s(makeInvalidValueErr(ctx, f, v.Value, vldtr.localeSvc))
}
if f.Options.Bool(fieldOpt_Datetime_onlyFutureValues) {
if !t.After(refTime) {
return e2s(makeInvalidValueErr(ctx, f, v.Value, vldtr.localeSvc))
}
} else if f.Options.Bool(fieldOpt_Datetime_onlyPastValues) {
if !t.Before(refTime) {
return e2s(makeInvalidValueErr(ctx, f, v.Value, vldtr.localeSvc))
}
}
// @todo check past/future
return nil
}
func (vldtr validator) vEmail(ctx context.Context, 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(ctx, f, v.Value, vldtr.localeSvc))
}
return nil
}
func (vldtr validator) vFile(ctx context.Context, s store.Storer, v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
if vldtr.fileRefCheckerFn == nil {
return nil
}
if ok, err := vldtr.fileRefCheckerFn(ctx, s, v, f, m); err != nil {
return e2s(makeInternalErr(f, err))
} else if !ok {
return e2s(makeInvalidRefErr(ctx, f, v.Ref, vldtr.localeSvc))
}
return nil
}
func (vldtr validator) vNumber(ctx context.Context, v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
// @todo !! This is temporary; precision and scale require a rework
if _, _, err := big.ParseFloat(v.Value, 0, maxPrecision, big.ToNearestEven); err != nil {
return e2s(makeInvalidValueErr(ctx, f, v.Value, vldtr.localeSvc))
}
return nil
}
func (vldtr validator) vRecord(ctx context.Context, s store.Storer, v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
if vldtr.recordRefCheckerFn == nil {
return nil
}
if ok, err := vldtr.recordRefCheckerFn(ctx, s, v, f, m); err != nil {
return e2s(makeInternalErr(f, err))
} else if !ok {
return e2s(makeInvalidRefErr(ctx, f, v.Ref, vldtr.localeSvc))
}
return nil
}
func (vldtr validator) vSelect(ctx context.Context, v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
var (
sbm = make(map[string]bool)
options, has = f.Options["options"]
)
if !has {
return nil
}
switch oo := options.(type) {
case []string:
sbm = slice.ToStringBoolMap(oo)
case []interface{}:
for _, o := range oo {
switch c := o.(type) {
case string:
sbm[c] = true
case map[string]string:
if value, has := c["value"]; has {
sbm[value] = true
}
case map[string]interface{}:
if value, has := c["value"]; has {
if value, ok := value.(string); ok {
sbm[value] = true
}
}
case types.ModuleFieldOptions:
if value, has := c["value"]; has {
if value, ok := value.(string); ok {
sbm[value] = true
}
}
}
}
}
if len(sbm) == 0 {
return e2s(makeInternalErr(f, fmt.Errorf("invalid select options definition")))
}
if !sbm[v.Value] {
return e2s(makeInvalidValueErr(ctx, f, v.Value, vldtr.localeSvc))
}
return nil
}
//func (vldtr validator) vString(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
// return nil
//}
func (vldtr validator) vUrl(ctx context.Context, 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(ctx, f, v.Value, vldtr.localeSvc))
} else if p.Scheme == "" || p.Host == "" {
return e2s(makeInvalidValueErr(ctx, f, v.Value, vldtr.localeSvc))
} else if f.Options.Bool(fieldOpt_Url_onlySecure) && p.Scheme != "https" {
return e2s(makeInvalidValueErr(ctx, f, v.Value, vldtr.localeSvc))
}
return nil
}
func (vldtr validator) vUser(ctx context.Context, s store.Storer, v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
if vldtr.userRefCheckerFn == nil {
return nil
}
if ok, err := vldtr.userRefCheckerFn(ctx, s, v, f, m); err != nil {
return e2s(makeInternalErr(f, err))
} else if !ok {
return e2s(makeInvalidRefErr(ctx, f, v.Ref, vldtr.localeSvc))
}
return nil
}

View File

@@ -0,0 +1,258 @@
package values
import (
"context"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/locale"
"github.com/stretchr/testify/require"
"reflect"
"testing"
"time"
)
func makeLocaleService() localeService {
return locale.Global()
}
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
},
localeSvc: makeLocaleService(),
}
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"}, Message: "record-field.errors.invalidValue"}),
},
{
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(context.Background(), &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{
localeSvc: makeLocaleService(),
}
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"}, Message: "record-field.errors.invalidValue"}),
},
{
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(context.Background(), &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{
localeSvc: makeLocaleService(),
}
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"}, Message: "record-field.errors.invalidValue"}),
},
{
name: "valid value",
val: "https://crust.tech/",
},
}
)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := vldtr.vUrl(context.Background(), &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{
localeSvc: makeLocaleService(),
}
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"}, Message: "record-field.errors.invalidValue"}),
},
{
name: "valid value",
val: "qa@crust.tech",
},
}
)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := vldtr.vEmail(context.Background(), &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{
localeSvc: makeLocaleService(),
}
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"}, Message: "record-field.errors.invalidValue"}),
},
}
)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := vldtr.vSelect(context.Background(), &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)
}
})
}
}
func Test_validator_customExpr(t *testing.T) {
var (
vldtr = validator{}
m = &types.Module{}
f = &types.ModuleField{Name: "num", Kind: "Number"}
r = &types.Record{}
)
f.Expressions.Validators = []types.ModuleFieldValidator{
{Test: "value < 5", Error: "value is lower than 5"},
}
m.Fields = append(m.Fields, f)
r.Values = r.Values.Replace("num", "1")
rve := vldtr.Run(context.Background(), nil, m, r)
require.False(t, rve.IsValid())
r.Values = r.Values.Replace("num", "10")
rve = vldtr.Run(context.Background(), nil, m, r)
require.True(t, rve.IsValid())
}