From 3f3dde2f564374b7b33104b8ef3755784fa90294 Mon Sep 17 00:00:00 2001 From: kt Date: Tue, 14 Jan 2020 15:18:11 -0800 Subject: [PATCH 01/14] sort validation function into separate files --- helper/validation/float.go | 64 +++ helper/validation/float_test.go | 97 +++++ helper/validation/int.go | 85 ++++ helper/validation/int_test.go | 95 +++++ helper/validation/meta.go | 59 +++ helper/validation/meta_test.go | 102 +++++ helper/validation/network.go | 89 ++++ helper/validation/network_test.go | 49 +++ helper/validation/strings.go | 138 ++++++- helper/validation/strings_test.go | 190 +++++++++ helper/validation/testing.go | 44 ++ helper/validation/time.go | 19 + helper/validation/time_test.go | 58 +++ helper/validation/validation.go | 419 ------------------- helper/validation/validation_test.go | 593 --------------------------- 15 files changed, 1088 insertions(+), 1013 deletions(-) create mode 100644 helper/validation/float.go create mode 100644 helper/validation/float_test.go create mode 100644 helper/validation/int.go create mode 100644 helper/validation/int_test.go create mode 100644 helper/validation/meta.go create mode 100644 helper/validation/meta_test.go create mode 100644 helper/validation/network.go create mode 100644 helper/validation/network_test.go create mode 100644 helper/validation/testing.go create mode 100644 helper/validation/time.go create mode 100644 helper/validation/time_test.go delete mode 100644 helper/validation/validation.go delete mode 100644 helper/validation/validation_test.go diff --git a/helper/validation/float.go b/helper/validation/float.go new file mode 100644 index 00000000000..477563bb8e9 --- /dev/null +++ b/helper/validation/float.go @@ -0,0 +1,64 @@ +package validation + +import ( + `fmt` + + `github.com/hashicorp/terraform-plugin-sdk/helper/schema` +) + +// FloatBetween returns a SchemaValidateFunc which tests if the provided value +// is of type float64 and is between min and max (inclusive). +func FloatBetween(min, max float64) schema.SchemaValidateFunc { + return func(i interface{}, k string) (s []string, es []error) { + v, ok := i.(float64) + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be float64", k)) + return + } + + if v < min || v > max { + es = append(es, fmt.Errorf("expected %s to be in the range (%f - %f), got %f", k, min, max, v)) + return + } + + return + } +} + +// FloatAtLeast returns a SchemaValidateFunc which tests if the provided value +// is of type float and is at least min (inclusive) +func FloatAtLeast(min float64) schema.SchemaValidateFunc { + return func(i interface{}, k string) (s []string, es []error) { + v, ok := i.(float64) + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be float", k)) + return + } + + if v < min { + es = append(es, fmt.Errorf("expected %s to be at least (%f), got %f", k, min, v)) + return + } + + return + } +} + +// FloatAtMost returns a SchemaValidateFunc which tests if the provided value +// is of type float and is at most max (inclusive) +func FloatAtMost(max float64) schema.SchemaValidateFunc { + return func(i interface{}, k string) (s []string, es []error) { + v, ok := i.(float64) + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be float", k)) + return + } + + if v > max { + es = append(es, fmt.Errorf("expected %s to be at most (%f), got %f", k, max, v)) + return + } + + return + } +} diff --git a/helper/validation/float_test.go b/helper/validation/float_test.go new file mode 100644 index 00000000000..2f0d51f5416 --- /dev/null +++ b/helper/validation/float_test.go @@ -0,0 +1,97 @@ +package validation + +import ( + `regexp` + "testing" + + `github.com/hashicorp/terraform-plugin-sdk/helper/schema` +) + +func TestValidateFloatBetween(t *testing.T) { + cases := map[string]struct { + Value interface{} + ValidateFunc schema.SchemaValidateFunc + ExpectValidationErrors bool + }{ + "accept valid value": { + Value: 1.5, + ValidateFunc: FloatBetween(1.0, 2.0), + ExpectValidationErrors: false, + }, + "accept valid value inclusive upper bound": { + Value: 1.0, + ValidateFunc: FloatBetween(0.0, 1.0), + ExpectValidationErrors: false, + }, + "accept valid value inclusive lower bound": { + Value: 0.0, + ValidateFunc: FloatBetween(0.0, 1.0), + ExpectValidationErrors: false, + }, + "reject out of range value": { + Value: -1.0, + ValidateFunc: FloatBetween(0.0, 1.0), + ExpectValidationErrors: true, + }, + "reject incorrectly typed value": { + Value: 1, + ValidateFunc: FloatBetween(0.0, 1.0), + ExpectValidationErrors: true, + }, + } + + for tn, tc := range cases { + _, errors := tc.ValidateFunc(tc.Value, tn) + if len(errors) > 0 && !tc.ExpectValidationErrors { + t.Errorf("%s: unexpected errors %s", tn, errors) + } else if len(errors) == 0 && tc.ExpectValidationErrors { + t.Errorf("%s: expected errors but got none", tn) + } + } +} + +func TestValidateFloatAtLeast(t *testing.T) { + runTestCases(t, []testCase{ + { + val: 2.5, + f: FloatAtLeast(1.5), + }, + { + val: -1.0, + f: FloatAtLeast(-1.5), + }, + { + val: 1.5, + f: FloatAtLeast(2.5), + expectedErr: regexp.MustCompile("expected [\\w]+ to be at least \\(2\\.5\\d*\\), got 1\\.5\\d*"), + }, + { + val: "2.5", + f: FloatAtLeast(1.5), + expectedErr: regexp.MustCompile("expected type of [\\w]+ to be float"), + }, + }) +} + +func TestValidateFloatAtMost(t *testing.T) { + runTestCases(t, []testCase{ + { + val: 2.5, + f: FloatAtMost(3.5), + }, + { + val: -1.0, + f: FloatAtMost(-0.5), + }, + { + val: 2.5, + f: FloatAtMost(1.5), + expectedErr: regexp.MustCompile("expected [\\w]+ to be at most \\(1\\.5\\d*\\), got 2\\.5\\d*"), + }, + { + val: "2.5", + f: FloatAtMost(3.5), + expectedErr: regexp.MustCompile("expected type of [\\w]+ to be float"), + }, + }) +} diff --git a/helper/validation/int.go b/helper/validation/int.go new file mode 100644 index 00000000000..9ed3b3fb078 --- /dev/null +++ b/helper/validation/int.go @@ -0,0 +1,85 @@ +package validation + +import ( + `fmt` + + `github.com/hashicorp/terraform-plugin-sdk/helper/schema` +) + +// IntBetween returns a SchemaValidateFunc which tests if the provided value +// is of type int and is between min and max (inclusive) +func IntBetween(min, max int) schema.SchemaValidateFunc { + return func(i interface{}, k string) (s []string, es []error) { + v, ok := i.(int) + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be int", k)) + return + } + + if v < min || v > max { + es = append(es, fmt.Errorf("expected %s to be in the range (%d - %d), got %d", k, min, max, v)) + return + } + + return + } +} + +// IntAtLeast returns a SchemaValidateFunc which tests if the provided value +// is of type int and is at least min (inclusive) +func IntAtLeast(min int) schema.SchemaValidateFunc { + return func(i interface{}, k string) (s []string, es []error) { + v, ok := i.(int) + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be int", k)) + return + } + + if v < min { + es = append(es, fmt.Errorf("expected %s to be at least (%d), got %d", k, min, v)) + return + } + + return + } +} + +// IntAtMost returns a SchemaValidateFunc which tests if the provided value +// is of type int and is at most max (inclusive) +func IntAtMost(max int) schema.SchemaValidateFunc { + return func(i interface{}, k string) (s []string, es []error) { + v, ok := i.(int) + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be int", k)) + return + } + + if v > max { + es = append(es, fmt.Errorf("expected %s to be at most (%d), got %d", k, max, v)) + return + } + + return + } +} + +// IntInSlice returns a SchemaValidateFunc which tests if the provided value +// is of type int and matches the value of an element in the valid slice +func IntInSlice(valid []int) schema.SchemaValidateFunc { + return func(i interface{}, k string) (s []string, es []error) { + v, ok := i.(int) + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be an integer", k)) + return + } + + for _, validInt := range valid { + if v == validInt { + return + } + } + + es = append(es, fmt.Errorf("expected %s to be one of %v, got %d", k, valid, v)) + return + } +} diff --git a/helper/validation/int_test.go b/helper/validation/int_test.go new file mode 100644 index 00000000000..4d6ec88bb6f --- /dev/null +++ b/helper/validation/int_test.go @@ -0,0 +1,95 @@ +package validation + +import ( + "regexp" + "testing" +) + + +func TestValidationIntBetween(t *testing.T) { + runTestCases(t, []testCase{ + { + val: 1, + f: IntBetween(1, 1), + }, + { + val: 1, + f: IntBetween(0, 2), + }, + { + val: 1, + f: IntBetween(2, 3), + expectedErr: regexp.MustCompile("expected [\\w]+ to be in the range \\(2 - 3\\), got 1"), + }, + { + val: "1", + f: IntBetween(2, 3), + expectedErr: regexp.MustCompile("expected type of [\\w]+ to be int"), + }, + }) +} + +func TestValidationIntAtLeast(t *testing.T) { + runTestCases(t, []testCase{ + { + val: 1, + f: IntAtLeast(1), + }, + { + val: 1, + f: IntAtLeast(0), + }, + { + val: 1, + f: IntAtLeast(2), + expectedErr: regexp.MustCompile("expected [\\w]+ to be at least \\(2\\), got 1"), + }, + { + val: "1", + f: IntAtLeast(2), + expectedErr: regexp.MustCompile("expected type of [\\w]+ to be int"), + }, + }) +} + +func TestValidationIntAtMost(t *testing.T) { + runTestCases(t, []testCase{ + { + val: 1, + f: IntAtMost(1), + }, + { + val: 1, + f: IntAtMost(2), + }, + { + val: 1, + f: IntAtMost(0), + expectedErr: regexp.MustCompile("expected [\\w]+ to be at most \\(0\\), got 1"), + }, + { + val: "1", + f: IntAtMost(0), + expectedErr: regexp.MustCompile("expected type of [\\w]+ to be int"), + }, + }) +} + +func TestValidationIntInSlice(t *testing.T) { + runTestCases(t, []testCase{ + { + val: 42, + f: IntInSlice([]int{1, 42}), + }, + { + val: 42, + f: IntInSlice([]int{10, 20}), + expectedErr: regexp.MustCompile("expected [\\w]+ to be one of \\[10 20\\], got 42"), + }, + { + val: "InvalidValue", + f: IntInSlice([]int{10, 20}), + expectedErr: regexp.MustCompile("expected type of [\\w]+ to be an integer"), + }, + }) +} \ No newline at end of file diff --git a/helper/validation/meta.go b/helper/validation/meta.go new file mode 100644 index 00000000000..2dcbcc87f04 --- /dev/null +++ b/helper/validation/meta.go @@ -0,0 +1,59 @@ +package validation + +import ( + `fmt` + `reflect` + + `github.com/hashicorp/terraform-plugin-sdk/helper/schema` +) + +// NoZeroValues is a SchemaValidateFunc which tests if the provided value is +// not a zero value. It's useful in situations where you want to catch +// explicit zero values on things like required fields during validation. +func NoZeroValues(i interface{}, k string) (s []string, es []error) { + if reflect.ValueOf(i).Interface() == reflect.Zero(reflect.TypeOf(i)).Interface() { + switch reflect.TypeOf(i).Kind() { + case reflect.String: + es = append(es, fmt.Errorf("%s must not be empty", k)) + case reflect.Int, reflect.Float64: + es = append(es, fmt.Errorf("%s must not be zero", k)) + default: + // this validator should only ever be applied to TypeString, TypeInt and TypeFloat + panic(fmt.Errorf("can't use NoZeroValues with %T attribute %s", i, k)) + } + } + return +} + +// All returns a SchemaValidateFunc which tests if the provided value +// passes all provided SchemaValidateFunc +func All(validators ...schema.SchemaValidateFunc) schema.SchemaValidateFunc { + return func(i interface{}, k string) ([]string, []error) { + var allErrors []error + var allWarnings []string + for _, validator := range validators { + validatorWarnings, validatorErrors := validator(i, k) + allWarnings = append(allWarnings, validatorWarnings...) + allErrors = append(allErrors, validatorErrors...) + } + return allWarnings, allErrors + } +} + +// Any returns a SchemaValidateFunc which tests if the provided value +// passes any of the provided SchemaValidateFunc +func Any(validators ...schema.SchemaValidateFunc) schema.SchemaValidateFunc { + return func(i interface{}, k string) ([]string, []error) { + var allErrors []error + var allWarnings []string + for _, validator := range validators { + validatorWarnings, validatorErrors := validator(i, k) + if len(validatorWarnings) == 0 && len(validatorErrors) == 0 { + return []string{}, []error{} + } + allWarnings = append(allWarnings, validatorWarnings...) + allErrors = append(allErrors, validatorErrors...) + } + return allWarnings, allErrors + } +} diff --git a/helper/validation/meta_test.go b/helper/validation/meta_test.go new file mode 100644 index 00000000000..ab36d6cad49 --- /dev/null +++ b/helper/validation/meta_test.go @@ -0,0 +1,102 @@ +package validation + +import ( + `regexp` + `testing` +) + + +func TestValidationNoZeroValues(t *testing.T) { + runTestCases(t, []testCase{ + { + val: "foo", + f: NoZeroValues, + }, + { + val: 1, + f: NoZeroValues, + }, + { + val: float64(1), + f: NoZeroValues, + }, + { + val: "", + f: NoZeroValues, + expectedErr: regexp.MustCompile("must not be empty"), + }, + { + val: 0, + f: NoZeroValues, + expectedErr: regexp.MustCompile("must not be zero"), + }, + { + val: float64(0), + f: NoZeroValues, + expectedErr: regexp.MustCompile("must not be zero"), + }, + }) +} + +func TestValidationAll(t *testing.T) { + runTestCases(t, []testCase{ + { + val: "valid", + f: All( + StringLenBetween(5, 42), + StringMatch(regexp.MustCompile(`[a-zA-Z0-9]+`), "value must be alphanumeric"), + ), + }, + { + val: "foo", + f: All( + StringLenBetween(5, 42), + StringMatch(regexp.MustCompile(`[a-zA-Z0-9]+`), "value must be alphanumeric"), + ), + expectedErr: regexp.MustCompile("expected length of [\\w]+ to be in the range \\(5 - 42\\), got foo"), + }, + { + val: "!!!!!", + f: All( + StringLenBetween(5, 42), + StringMatch(regexp.MustCompile(`[a-zA-Z0-9]+`), "value must be alphanumeric"), + ), + expectedErr: regexp.MustCompile("value must be alphanumeric"), + }, + }) +} + +func TestValidationAny(t *testing.T) { + runTestCases(t, []testCase{ + { + val: 43, + f: Any( + IntAtLeast(42), + IntAtMost(5), + ), + }, + { + val: 4, + f: Any( + IntAtLeast(42), + IntAtMost(5), + ), + }, + { + val: 7, + f: Any( + IntAtLeast(42), + IntAtMost(5), + ), + expectedErr: regexp.MustCompile("expected [\\w]+ to be at least \\(42\\), got 7"), + }, + { + val: 7, + f: Any( + IntAtLeast(42), + IntAtMost(5), + ), + expectedErr: regexp.MustCompile("expected [\\w]+ to be at most \\(5\\), got 7"), + }, + }) +} \ No newline at end of file diff --git a/helper/validation/network.go b/helper/validation/network.go new file mode 100644 index 00000000000..d2a17d1572c --- /dev/null +++ b/helper/validation/network.go @@ -0,0 +1,89 @@ +package validation + +import ( + "bytes" + "fmt" + "net" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +// CIDRNetwork returns a SchemaValidateFunc which tests if the provided value +// is of type string, is in valid CIDR network notation, and has significant bits between min and max (inclusive) +func CIDRNetwork(min, max int) schema.SchemaValidateFunc { + return func(i interface{}, k string) (s []string, es []error) { + v, ok := i.(string) + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be string", k)) + return + } + + _, ipnet, err := net.ParseCIDR(v) + if err != nil { + es = append(es, fmt.Errorf( + "expected %s to contain a valid CIDR, got: %s with err: %s", k, v, err)) + return + } + + if ipnet == nil || v != ipnet.String() { + es = append(es, fmt.Errorf( + "expected %s to contain a valid network CIDR, expected %s, got %s", + k, ipnet, v)) + } + + sigbits, _ := ipnet.Mask.Size() + if sigbits < min || sigbits > max { + es = append(es, fmt.Errorf( + "expected %q to contain a network CIDR with between %d and %d significant bits, got: %d", + k, min, max, sigbits)) + } + + return + } +} + +// SingleIP returns a SchemaValidateFunc which tests if the provided value +// is of type string, and in valid single IP notation +func SingleIP() schema.SchemaValidateFunc { + return func(i interface{}, k string) (s []string, es []error) { + v, ok := i.(string) + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be string", k)) + return + } + + ip := net.ParseIP(v) + if ip == nil { + es = append(es, fmt.Errorf( + "expected %s to contain a valid IP, got: %s", k, v)) + } + return + } +} + +// IPRange returns a SchemaValidateFunc which tests if the provided value +// is of type string, and in valid IP range notation +func IPRange() schema.SchemaValidateFunc { + return func(i interface{}, k string) (s []string, es []error) { + v, ok := i.(string) + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be string", k)) + return + } + + ips := strings.Split(v, "-") + if len(ips) != 2 { + es = append(es, fmt.Errorf( + "expected %s to contain a valid IP range, got: %s", k, v)) + return + } + ip1 := net.ParseIP(ips[0]) + ip2 := net.ParseIP(ips[1]) + if ip1 == nil || ip2 == nil || bytes.Compare(ip1, ip2) > 0 { + es = append(es, fmt.Errorf( + "expected %s to contain a valid IP range, got: %s", k, v)) + } + return + } +} \ No newline at end of file diff --git a/helper/validation/network_test.go b/helper/validation/network_test.go new file mode 100644 index 00000000000..5f32ef7dc11 --- /dev/null +++ b/helper/validation/network_test.go @@ -0,0 +1,49 @@ +package validation + +import ( + `regexp` + `testing` +) + +func TestValidationSingleIP(t *testing.T) { + runTestCases(t, []testCase{ + { + val: "172.10.10.10", + f: SingleIP(), + }, + { + val: "1.1.1", + f: SingleIP(), + expectedErr: regexp.MustCompile(regexp.QuoteMeta("expected test_property to contain a valid IP, got:")), + }, + { + val: "1.1.1.0/20", + f: SingleIP(), + expectedErr: regexp.MustCompile(regexp.QuoteMeta("expected test_property to contain a valid IP, got:")), + }, + { + val: "256.1.1.1", + f: SingleIP(), + expectedErr: regexp.MustCompile(regexp.QuoteMeta("expected test_property to contain a valid IP, got:")), + }, + }) +} + +func TestValidationIPRange(t *testing.T) { + runTestCases(t, []testCase{ + { + val: "172.10.10.10-172.10.10.12", + f: IPRange(), + }, + { + val: "172.10.10.20", + f: IPRange(), + expectedErr: regexp.MustCompile(regexp.QuoteMeta("expected test_property to contain a valid IP range, got:")), + }, + { + val: "172.10.10.20-172.10.10.12", + f: IPRange(), + expectedErr: regexp.MustCompile(regexp.QuoteMeta("expected test_property to contain a valid IP range, got:")), + }, + }) +} \ No newline at end of file diff --git a/helper/validation/strings.go b/helper/validation/strings.go index f9fdec5dca6..35949fe80f9 100644 --- a/helper/validation/strings.go +++ b/helper/validation/strings.go @@ -1,9 +1,13 @@ package validation import ( - "encoding/base64" + `encoding/base64` "fmt" + `regexp` "strings" + + `github.com/hashicorp/terraform-plugin-sdk/helper/schema` + `github.com/hashicorp/terraform-plugin-sdk/helper/structure` ) // StringIsNotEmpty is a ValidateFunc that ensures a string is not empty or consisting entirely of whitespace characters @@ -62,6 +66,106 @@ func StringIsWhiteSpace(i interface{}, k string) ([]string, []error) { return nil, nil } +// StringLenBetween returns a SchemaValidateFunc which tests if the provided value +// is of type string and has length between min and max (inclusive) +func StringLenBetween(min, max int) schema.SchemaValidateFunc { + return func(i interface{}, k string) (s []string, es []error) { + v, ok := i.(string) + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be string", k)) + return + } + if len(v) < min || len(v) > max { + es = append(es, fmt.Errorf("expected length of %s to be in the range (%d - %d), got %s", k, min, max, v)) + } + return + } +} + +// StringMatch returns a SchemaValidateFunc which tests if the provided value +// matches a given regexp. Optionally an error message can be provided to +// return something friendlier than "must match some globby regexp". +func StringMatch(r *regexp.Regexp, message string) schema.SchemaValidateFunc { + return func(i interface{}, k string) ([]string, []error) { + v, ok := i.(string) + if !ok { + return nil, []error{fmt.Errorf("expected type of %s to be string", k)} + } + + if ok := r.MatchString(v); !ok { + if message != "" { + return nil, []error{fmt.Errorf("invalid value for %s (%s)", k, message)} + + } + return nil, []error{fmt.Errorf("expected value of %s to match regular expression %q", k, r)} + } + return nil, nil + } +} + +// StringDoesNotMatch returns a SchemaValidateFunc which tests if the provided value +// does not match a given regexp. Optionally an error message can be provided to +// return something friendlier than "must not match some globby regexp". +func StringDoesNotMatch(r *regexp.Regexp, message string) schema.SchemaValidateFunc { + return func(i interface{}, k string) ([]string, []error) { + v, ok := i.(string) + if !ok { + return nil, []error{fmt.Errorf("expected type of %s to be string", k)} + } + + if ok := r.MatchString(v); ok { + if message != "" { + return nil, []error{fmt.Errorf("invalid value for %s (%s)", k, message)} + + } + return nil, []error{fmt.Errorf("expected value of %s to not match regular expression %q", k, r)} + } + return nil, nil + } +} + +// StringInSlice returns a SchemaValidateFunc which tests if the provided value +// is of type string and matches the value of an element in the valid slice +// will test with in lower case if ignoreCase is true +func StringInSlice(valid []string, ignoreCase bool) schema.SchemaValidateFunc { + return func(i interface{}, k string) (s []string, es []error) { + v, ok := i.(string) + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be string", k)) + return + } + + for _, str := range valid { + if v == str || (ignoreCase && strings.ToLower(v) == strings.ToLower(str)) { + return + } + } + + es = append(es, fmt.Errorf("expected %s to be one of %v, got %s", k, valid, v)) + return + } +} + +// StringDoesNotContainAny returns a SchemaValidateFunc which validates that the +// provided value does not contain any of the specified Unicode code points in chars. +func StringDoesNotContainAny(chars string) schema.SchemaValidateFunc { + return func(i interface{}, k string) (s []string, es []error) { + v, ok := i.(string) + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be string", k)) + return + } + + if strings.ContainsAny(v, chars) { + es = append(es, fmt.Errorf("expected value of %s to not contain any of %q", k, chars)) + return + } + + return + } +} + + // StringIsBase64 is a ValidateFunc that ensures a string can be parsed as Base64 func StringIsBase64(i interface{}, k string) (warnings []string, errors []error) { // Empty string is not allowed @@ -77,3 +181,35 @@ func StringIsBase64(i interface{}, k string) (warnings []string, errors []error) return } + +// ValidateListUniqueStrings is a ValidateFunc that ensures a list has no +// duplicate items in it. It's useful for when a list is needed over a set +// because order matters, yet the items still need to be unique. +func ValidateListUniqueStrings(v interface{}, k string) (ws []string, errors []error) { + for n1, v1 := range v.([]interface{}) { + for n2, v2 := range v.([]interface{}) { + if v1.(string) == v2.(string) && n1 != n2 { + errors = append(errors, fmt.Errorf("%q: duplicate entry - %s", k, v1.(string))) + } + } + } + return +} + +// ValidateJsonString is a SchemaValidateFunc which tests to make sure the +// supplied string is valid JSON. +func ValidateJsonString(v interface{}, k string) (ws []string, errors []error) { + if _, err := structure.NormalizeJsonString(v); err != nil { + errors = append(errors, fmt.Errorf("%q contains an invalid JSON: %s", k, err)) + } + return +} + +// ValidateRegexp returns a SchemaValidateFunc which tests to make sure the +// supplied string is a valid regular expression. +func ValidateRegexp(v interface{}, k string) (ws []string, errors []error) { + if _, err := regexp.Compile(v.(string)); err != nil { + errors = append(errors, fmt.Errorf("%q: %s", k, err)) + } + return +} \ No newline at end of file diff --git a/helper/validation/strings_test.go b/helper/validation/strings_test.go index 05ce90a9f1c..7beab3f6da5 100644 --- a/helper/validation/strings_test.go +++ b/helper/validation/strings_test.go @@ -1,6 +1,7 @@ package validation import ( + `regexp` "testing" ) @@ -274,3 +275,192 @@ func TestValidationStringIsBase64(t *testing.T) { }) } } + +func TestValidationStringInSlice(t *testing.T) { + runTestCases(t, []testCase{ + { + val: "ValidValue", + f: StringInSlice([]string{"ValidValue", "AnotherValidValue"}, false), + }, + // ignore case + { + val: "VALIDVALUE", + f: StringInSlice([]string{"ValidValue", "AnotherValidValue"}, true), + }, + { + val: "VALIDVALUE", + f: StringInSlice([]string{"ValidValue", "AnotherValidValue"}, false), + expectedErr: regexp.MustCompile("expected [\\w]+ to be one of \\[ValidValue AnotherValidValue\\], got VALIDVALUE"), + }, + { + val: "InvalidValue", + f: StringInSlice([]string{"ValidValue", "AnotherValidValue"}, false), + expectedErr: regexp.MustCompile("expected [\\w]+ to be one of \\[ValidValue AnotherValidValue\\], got InvalidValue"), + }, + { + val: 1, + f: StringInSlice([]string{"ValidValue", "AnotherValidValue"}, false), + expectedErr: regexp.MustCompile("expected type of [\\w]+ to be string"), + }, + }) +} + +func TestValidationStringMatch(t *testing.T) { + runTestCases(t, []testCase{ + { + val: "foobar", + f: StringMatch(regexp.MustCompile(".*foo.*"), ""), + }, + { + val: "bar", + f: StringMatch(regexp.MustCompile(".*foo.*"), ""), + expectedErr: regexp.MustCompile("expected value of [\\w]+ to match regular expression " + regexp.QuoteMeta(`".*foo.*"`)), + }, + { + val: "bar", + f: StringMatch(regexp.MustCompile(".*foo.*"), "value must contain foo"), + expectedErr: regexp.MustCompile("invalid value for [\\w]+ \\(value must contain foo\\)"), + }, + }) +} + +func TestValidationStringDoesNotMatch(t *testing.T) { + runTestCases(t, []testCase{ + { + val: "foobar", + f: StringDoesNotMatch(regexp.MustCompile(".*baz.*"), ""), + }, + { + val: "bar", + f: StringDoesNotMatch(regexp.MustCompile(".*bar.*"), ""), + expectedErr: regexp.MustCompile("expected value of [\\w]+ to not match regular expression " + regexp.QuoteMeta(`".*bar.*"`)), + }, + { + val: "bar", + f: StringDoesNotMatch(regexp.MustCompile(".*bar.*"), "value must not contain foo"), + expectedErr: regexp.MustCompile("invalid value for [\\w]+ \\(value must not contain foo\\)"), + }, + }) +} + + +func TestValidateJsonString(t *testing.T) { + type testCases struct { + Value string + ErrCount int + } + + invalidCases := []testCases{ + { + Value: `{0:"1"}`, + ErrCount: 1, + }, + { + Value: `{'abc':1}`, + ErrCount: 1, + }, + { + Value: `{"def":}`, + ErrCount: 1, + }, + { + Value: `{"xyz":[}}`, + ErrCount: 1, + }, + } + + for _, tc := range invalidCases { + _, errors := ValidateJsonString(tc.Value, "json") + if len(errors) != tc.ErrCount { + t.Fatalf("Expected %q to trigger a validation error.", tc.Value) + } + } + + validCases := []testCases{ + { + Value: ``, + ErrCount: 0, + }, + { + Value: `{}`, + ErrCount: 0, + }, + { + Value: `{"abc":["1","2"]}`, + ErrCount: 0, + }, + } + + for _, tc := range validCases { + _, errors := ValidateJsonString(tc.Value, "json") + if len(errors) != tc.ErrCount { + t.Fatalf("Expected %q not to trigger a validation error.", tc.Value) + } + } +} + +func TestValidateListUniqueStrings(t *testing.T) { + runTestCases(t, []testCase{ + { + val: []interface{}{"foo", "bar"}, + f: ValidateListUniqueStrings, + }, + { + val: []interface{}{"foo", "bar", "foo"}, + f: ValidateListUniqueStrings, + expectedErr: regexp.MustCompile("duplicate entry - foo"), + }, + { + val: []interface{}{"foo", "bar", "foo", "baz", "bar"}, + f: ValidateListUniqueStrings, + expectedErr: regexp.MustCompile("duplicate entry - (?:foo|bar)"), + }, + }) +} + + +func TestStringDoesNotContainAny(t *testing.T) { + chars := "|:/" + + validStrings := []string{ + "HelloWorld", + "ABC_*&^%123", + } + for _, v := range validStrings { + _, errors := StringDoesNotContainAny(chars)(v, "name") + if len(errors) != 0 { + t.Fatalf("%q should not contain any of %q", v, chars) + } + } + + invalidStrings := []string{ + "Hello/World", + "ABC|123", + "This will fail:", + chars, + } + for _, v := range invalidStrings { + _, errors := StringDoesNotContainAny(chars)(v, "name") + if len(errors) == 0 { + t.Fatalf("%q should contain one of %q", v, chars) + } + } +} + + + + + +func TestValidationRegexp(t *testing.T) { + runTestCases(t, []testCase{ + { + val: ".*foo.*", + f: ValidateRegexp, + }, + { + val: "foo(bar", + f: ValidateRegexp, + expectedErr: regexp.MustCompile(regexp.QuoteMeta("error parsing regexp: missing closing ): `foo(bar`")), + }, + }) +} diff --git a/helper/validation/testing.go b/helper/validation/testing.go new file mode 100644 index 00000000000..b11ea2353f6 --- /dev/null +++ b/helper/validation/testing.go @@ -0,0 +1,44 @@ +package validation + +import ( + `regexp` + `testing` + + `github.com/hashicorp/terraform-plugin-sdk/helper/schema` +) + +type testCase struct { + val interface{} + f schema.SchemaValidateFunc + expectedErr *regexp.Regexp +} + + +func runTestCases(t *testing.T, cases []testCase) { + matchErr := func(errs []error, r *regexp.Regexp) bool { + // err must match one provided + for _, err := range errs { + if r.MatchString(err.Error()) { + return true + } + } + + return false + } + + for i, tc := range cases { + _, errs := tc.f(tc.val, "test_property") + + if len(errs) == 0 && tc.expectedErr == nil { + continue + } + + if len(errs) != 0 && tc.expectedErr == nil { + t.Fatalf("expected test case %d to produce no errors, got %v", i, errs) + } + + if !matchErr(errs, tc.expectedErr) { + t.Fatalf("expected test case %d to produce error matching \"%s\", got %v", i, tc.expectedErr, errs) + } + } +} diff --git a/helper/validation/time.go b/helper/validation/time.go new file mode 100644 index 00000000000..bceafea3345 --- /dev/null +++ b/helper/validation/time.go @@ -0,0 +1,19 @@ +package validation + +import ( + "fmt" + "reflect" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + + +// ValidateRFC3339TimeString is a ValidateFunc that ensures a string parses +// as time.RFC3339 format +func ValidateRFC3339TimeString(v interface{}, k string) (ws []string, errors []error) { + if _, err := time.Parse(time.RFC3339, v.(string)); err != nil { + errors = append(errors, fmt.Errorf("%q: invalid RFC3339 timestamp", k)) + } + return +} \ No newline at end of file diff --git a/helper/validation/time_test.go b/helper/validation/time_test.go new file mode 100644 index 00000000000..ac641783e72 --- /dev/null +++ b/helper/validation/time_test.go @@ -0,0 +1,58 @@ +package validation + +import ( + "regexp" + "testing" +) + +func TestValidateRFC3339TimeString(t *testing.T) { + runTestCases(t, []testCase{ + { + val: "2018-03-01T00:00:00Z", + f: ValidateRFC3339TimeString, + }, + { + val: "2018-03-01T00:00:00-05:00", + f: ValidateRFC3339TimeString, + }, + { + val: "2018-03-01T00:00:00+05:00", + f: ValidateRFC3339TimeString, + }, + { + val: "03/01/2018", + f: ValidateRFC3339TimeString, + expectedErr: regexp.MustCompile(regexp.QuoteMeta(`invalid RFC3339 timestamp`)), + }, + { + val: "03-01-2018", + f: ValidateRFC3339TimeString, + expectedErr: regexp.MustCompile(regexp.QuoteMeta(`invalid RFC3339 timestamp`)), + }, + { + val: "2018-03-01", + f: ValidateRFC3339TimeString, + expectedErr: regexp.MustCompile(regexp.QuoteMeta(`invalid RFC3339 timestamp`)), + }, + { + val: "2018-03-01T", + f: ValidateRFC3339TimeString, + expectedErr: regexp.MustCompile(regexp.QuoteMeta(`invalid RFC3339 timestamp`)), + }, + { + val: "2018-03-01T00:00:00", + f: ValidateRFC3339TimeString, + expectedErr: regexp.MustCompile(regexp.QuoteMeta(`invalid RFC3339 timestamp`)), + }, + { + val: "2018-03-01T00:00:00Z05:00", + f: ValidateRFC3339TimeString, + expectedErr: regexp.MustCompile(regexp.QuoteMeta(`invalid RFC3339 timestamp`)), + }, + { + val: "2018-03-01T00:00:00Z-05:00", + f: ValidateRFC3339TimeString, + expectedErr: regexp.MustCompile(regexp.QuoteMeta(`invalid RFC3339 timestamp`)), + }, + }) +} diff --git a/helper/validation/validation.go b/helper/validation/validation.go deleted file mode 100644 index eb97d654781..00000000000 --- a/helper/validation/validation.go +++ /dev/null @@ -1,419 +0,0 @@ -package validation - -import ( - "bytes" - "fmt" - "net" - "reflect" - "regexp" - "strings" - "time" - - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/helper/structure" -) - -// All returns a SchemaValidateFunc which tests if the provided value -// passes all provided SchemaValidateFunc -func All(validators ...schema.SchemaValidateFunc) schema.SchemaValidateFunc { - return func(i interface{}, k string) ([]string, []error) { - var allErrors []error - var allWarnings []string - for _, validator := range validators { - validatorWarnings, validatorErrors := validator(i, k) - allWarnings = append(allWarnings, validatorWarnings...) - allErrors = append(allErrors, validatorErrors...) - } - return allWarnings, allErrors - } -} - -// Any returns a SchemaValidateFunc which tests if the provided value -// passes any of the provided SchemaValidateFunc -func Any(validators ...schema.SchemaValidateFunc) schema.SchemaValidateFunc { - return func(i interface{}, k string) ([]string, []error) { - var allErrors []error - var allWarnings []string - for _, validator := range validators { - validatorWarnings, validatorErrors := validator(i, k) - if len(validatorWarnings) == 0 && len(validatorErrors) == 0 { - return []string{}, []error{} - } - allWarnings = append(allWarnings, validatorWarnings...) - allErrors = append(allErrors, validatorErrors...) - } - return allWarnings, allErrors - } -} - -// IntBetween returns a SchemaValidateFunc which tests if the provided value -// is of type int and is between min and max (inclusive) -func IntBetween(min, max int) schema.SchemaValidateFunc { - return func(i interface{}, k string) (s []string, es []error) { - v, ok := i.(int) - if !ok { - es = append(es, fmt.Errorf("expected type of %s to be int", k)) - return - } - - if v < min || v > max { - es = append(es, fmt.Errorf("expected %s to be in the range (%d - %d), got %d", k, min, max, v)) - return - } - - return - } -} - -// IntAtLeast returns a SchemaValidateFunc which tests if the provided value -// is of type int and is at least min (inclusive) -func IntAtLeast(min int) schema.SchemaValidateFunc { - return func(i interface{}, k string) (s []string, es []error) { - v, ok := i.(int) - if !ok { - es = append(es, fmt.Errorf("expected type of %s to be int", k)) - return - } - - if v < min { - es = append(es, fmt.Errorf("expected %s to be at least (%d), got %d", k, min, v)) - return - } - - return - } -} - -// IntAtMost returns a SchemaValidateFunc which tests if the provided value -// is of type int and is at most max (inclusive) -func IntAtMost(max int) schema.SchemaValidateFunc { - return func(i interface{}, k string) (s []string, es []error) { - v, ok := i.(int) - if !ok { - es = append(es, fmt.Errorf("expected type of %s to be int", k)) - return - } - - if v > max { - es = append(es, fmt.Errorf("expected %s to be at most (%d), got %d", k, max, v)) - return - } - - return - } -} - -// IntInSlice returns a SchemaValidateFunc which tests if the provided value -// is of type int and matches the value of an element in the valid slice -func IntInSlice(valid []int) schema.SchemaValidateFunc { - return func(i interface{}, k string) (s []string, es []error) { - v, ok := i.(int) - if !ok { - es = append(es, fmt.Errorf("expected type of %s to be an integer", k)) - return - } - - for _, validInt := range valid { - if v == validInt { - return - } - } - - es = append(es, fmt.Errorf("expected %s to be one of %v, got %d", k, valid, v)) - return - } -} - -// StringInSlice returns a SchemaValidateFunc which tests if the provided value -// is of type string and matches the value of an element in the valid slice -// will test with in lower case if ignoreCase is true -func StringInSlice(valid []string, ignoreCase bool) schema.SchemaValidateFunc { - return func(i interface{}, k string) (s []string, es []error) { - v, ok := i.(string) - if !ok { - es = append(es, fmt.Errorf("expected type of %s to be string", k)) - return - } - - for _, str := range valid { - if v == str || (ignoreCase && strings.ToLower(v) == strings.ToLower(str)) { - return - } - } - - es = append(es, fmt.Errorf("expected %s to be one of %v, got %s", k, valid, v)) - return - } -} - -// StringLenBetween returns a SchemaValidateFunc which tests if the provided value -// is of type string and has length between min and max (inclusive) -func StringLenBetween(min, max int) schema.SchemaValidateFunc { - return func(i interface{}, k string) (s []string, es []error) { - v, ok := i.(string) - if !ok { - es = append(es, fmt.Errorf("expected type of %s to be string", k)) - return - } - if len(v) < min || len(v) > max { - es = append(es, fmt.Errorf("expected length of %s to be in the range (%d - %d), got %s", k, min, max, v)) - } - return - } -} - -// StringMatch returns a SchemaValidateFunc which tests if the provided value -// matches a given regexp. Optionally an error message can be provided to -// return something friendlier than "must match some globby regexp". -func StringMatch(r *regexp.Regexp, message string) schema.SchemaValidateFunc { - return func(i interface{}, k string) ([]string, []error) { - v, ok := i.(string) - if !ok { - return nil, []error{fmt.Errorf("expected type of %s to be string", k)} - } - - if ok := r.MatchString(v); !ok { - if message != "" { - return nil, []error{fmt.Errorf("invalid value for %s (%s)", k, message)} - - } - return nil, []error{fmt.Errorf("expected value of %s to match regular expression %q", k, r)} - } - return nil, nil - } -} - -// StringDoesNotMatch returns a SchemaValidateFunc which tests if the provided value -// does not match a given regexp. Optionally an error message can be provided to -// return something friendlier than "must not match some globby regexp". -func StringDoesNotMatch(r *regexp.Regexp, message string) schema.SchemaValidateFunc { - return func(i interface{}, k string) ([]string, []error) { - v, ok := i.(string) - if !ok { - return nil, []error{fmt.Errorf("expected type of %s to be string", k)} - } - - if ok := r.MatchString(v); ok { - if message != "" { - return nil, []error{fmt.Errorf("invalid value for %s (%s)", k, message)} - - } - return nil, []error{fmt.Errorf("expected value of %s to not match regular expression %q", k, r)} - } - return nil, nil - } -} - -// NoZeroValues is a SchemaValidateFunc which tests if the provided value is -// not a zero value. It's useful in situations where you want to catch -// explicit zero values on things like required fields during validation. -func NoZeroValues(i interface{}, k string) (s []string, es []error) { - if reflect.ValueOf(i).Interface() == reflect.Zero(reflect.TypeOf(i)).Interface() { - switch reflect.TypeOf(i).Kind() { - case reflect.String: - es = append(es, fmt.Errorf("%s must not be empty", k)) - case reflect.Int, reflect.Float64: - es = append(es, fmt.Errorf("%s must not be zero", k)) - default: - // this validator should only ever be applied to TypeString, TypeInt and TypeFloat - panic(fmt.Errorf("can't use NoZeroValues with %T attribute %s", i, k)) - } - } - return -} - -// CIDRNetwork returns a SchemaValidateFunc which tests if the provided value -// is of type string, is in valid CIDR network notation, and has significant bits between min and max (inclusive) -func CIDRNetwork(min, max int) schema.SchemaValidateFunc { - return func(i interface{}, k string) (s []string, es []error) { - v, ok := i.(string) - if !ok { - es = append(es, fmt.Errorf("expected type of %s to be string", k)) - return - } - - _, ipnet, err := net.ParseCIDR(v) - if err != nil { - es = append(es, fmt.Errorf( - "expected %s to contain a valid CIDR, got: %s with err: %s", k, v, err)) - return - } - - if ipnet == nil || v != ipnet.String() { - es = append(es, fmt.Errorf( - "expected %s to contain a valid network CIDR, expected %s, got %s", - k, ipnet, v)) - } - - sigbits, _ := ipnet.Mask.Size() - if sigbits < min || sigbits > max { - es = append(es, fmt.Errorf( - "expected %q to contain a network CIDR with between %d and %d significant bits, got: %d", - k, min, max, sigbits)) - } - - return - } -} - -// SingleIP returns a SchemaValidateFunc which tests if the provided value -// is of type string, and in valid single IP notation -func SingleIP() schema.SchemaValidateFunc { - return func(i interface{}, k string) (s []string, es []error) { - v, ok := i.(string) - if !ok { - es = append(es, fmt.Errorf("expected type of %s to be string", k)) - return - } - - ip := net.ParseIP(v) - if ip == nil { - es = append(es, fmt.Errorf( - "expected %s to contain a valid IP, got: %s", k, v)) - } - return - } -} - -// IPRange returns a SchemaValidateFunc which tests if the provided value -// is of type string, and in valid IP range notation -func IPRange() schema.SchemaValidateFunc { - return func(i interface{}, k string) (s []string, es []error) { - v, ok := i.(string) - if !ok { - es = append(es, fmt.Errorf("expected type of %s to be string", k)) - return - } - - ips := strings.Split(v, "-") - if len(ips) != 2 { - es = append(es, fmt.Errorf( - "expected %s to contain a valid IP range, got: %s", k, v)) - return - } - ip1 := net.ParseIP(ips[0]) - ip2 := net.ParseIP(ips[1]) - if ip1 == nil || ip2 == nil || bytes.Compare(ip1, ip2) > 0 { - es = append(es, fmt.Errorf( - "expected %s to contain a valid IP range, got: %s", k, v)) - } - return - } -} - -// ValidateJsonString is a SchemaValidateFunc which tests to make sure the -// supplied string is valid JSON. -func ValidateJsonString(v interface{}, k string) (ws []string, errors []error) { - if _, err := structure.NormalizeJsonString(v); err != nil { - errors = append(errors, fmt.Errorf("%q contains an invalid JSON: %s", k, err)) - } - return -} - -// ValidateListUniqueStrings is a ValidateFunc that ensures a list has no -// duplicate items in it. It's useful for when a list is needed over a set -// because order matters, yet the items still need to be unique. -func ValidateListUniqueStrings(v interface{}, k string) (ws []string, errors []error) { - for n1, v1 := range v.([]interface{}) { - for n2, v2 := range v.([]interface{}) { - if v1.(string) == v2.(string) && n1 != n2 { - errors = append(errors, fmt.Errorf("%q: duplicate entry - %s", k, v1.(string))) - } - } - } - return -} - -// ValidateRegexp returns a SchemaValidateFunc which tests to make sure the -// supplied string is a valid regular expression. -func ValidateRegexp(v interface{}, k string) (ws []string, errors []error) { - if _, err := regexp.Compile(v.(string)); err != nil { - errors = append(errors, fmt.Errorf("%q: %s", k, err)) - } - return -} - -// ValidateRFC3339TimeString is a ValidateFunc that ensures a string parses -// as time.RFC3339 format -func ValidateRFC3339TimeString(v interface{}, k string) (ws []string, errors []error) { - if _, err := time.Parse(time.RFC3339, v.(string)); err != nil { - errors = append(errors, fmt.Errorf("%q: invalid RFC3339 timestamp", k)) - } - return -} - -// FloatBetween returns a SchemaValidateFunc which tests if the provided value -// is of type float64 and is between min and max (inclusive). -func FloatBetween(min, max float64) schema.SchemaValidateFunc { - return func(i interface{}, k string) (s []string, es []error) { - v, ok := i.(float64) - if !ok { - es = append(es, fmt.Errorf("expected type of %s to be float64", k)) - return - } - - if v < min || v > max { - es = append(es, fmt.Errorf("expected %s to be in the range (%f - %f), got %f", k, min, max, v)) - return - } - - return - } -} - -// FloatAtLeast returns a SchemaValidateFunc which tests if the provided value -// is of type float and is at least min (inclusive) -func FloatAtLeast(min float64) schema.SchemaValidateFunc { - return func(i interface{}, k string) (s []string, es []error) { - v, ok := i.(float64) - if !ok { - es = append(es, fmt.Errorf("expected type of %s to be float", k)) - return - } - - if v < min { - es = append(es, fmt.Errorf("expected %s to be at least (%f), got %f", k, min, v)) - return - } - - return - } -} - -// FloatAtMost returns a SchemaValidateFunc which tests if the provided value -// is of type float and is at most max (inclusive) -func FloatAtMost(max float64) schema.SchemaValidateFunc { - return func(i interface{}, k string) (s []string, es []error) { - v, ok := i.(float64) - if !ok { - es = append(es, fmt.Errorf("expected type of %s to be float", k)) - return - } - - if v > max { - es = append(es, fmt.Errorf("expected %s to be at most (%f), got %f", k, max, v)) - return - } - - return - } -} - -// StringDoesNotContainAny returns a SchemaValidateFunc which validates that the -// provided value does not contain any of the specified Unicode code points in chars. -func StringDoesNotContainAny(chars string) schema.SchemaValidateFunc { - return func(i interface{}, k string) (s []string, es []error) { - v, ok := i.(string) - if !ok { - es = append(es, fmt.Errorf("expected type of %s to be string", k)) - return - } - - if strings.ContainsAny(v, chars) { - es = append(es, fmt.Errorf("expected value of %s to not contain any of %q", k, chars)) - return - } - - return - } -} diff --git a/helper/validation/validation_test.go b/helper/validation/validation_test.go deleted file mode 100644 index 4f7c2e8c36e..00000000000 --- a/helper/validation/validation_test.go +++ /dev/null @@ -1,593 +0,0 @@ -package validation - -import ( - "regexp" - "testing" - - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" -) - -type testCase struct { - val interface{} - f schema.SchemaValidateFunc - expectedErr *regexp.Regexp -} - -func TestValidationAll(t *testing.T) { - runTestCases(t, []testCase{ - { - val: "valid", - f: All( - StringLenBetween(5, 42), - StringMatch(regexp.MustCompile(`[a-zA-Z0-9]+`), "value must be alphanumeric"), - ), - }, - { - val: "foo", - f: All( - StringLenBetween(5, 42), - StringMatch(regexp.MustCompile(`[a-zA-Z0-9]+`), "value must be alphanumeric"), - ), - expectedErr: regexp.MustCompile("expected length of [\\w]+ to be in the range \\(5 - 42\\), got foo"), - }, - { - val: "!!!!!", - f: All( - StringLenBetween(5, 42), - StringMatch(regexp.MustCompile(`[a-zA-Z0-9]+`), "value must be alphanumeric"), - ), - expectedErr: regexp.MustCompile("value must be alphanumeric"), - }, - }) -} - -func TestValidationAny(t *testing.T) { - runTestCases(t, []testCase{ - { - val: 43, - f: Any( - IntAtLeast(42), - IntAtMost(5), - ), - }, - { - val: 4, - f: Any( - IntAtLeast(42), - IntAtMost(5), - ), - }, - { - val: 7, - f: Any( - IntAtLeast(42), - IntAtMost(5), - ), - expectedErr: regexp.MustCompile("expected [\\w]+ to be at least \\(42\\), got 7"), - }, - { - val: 7, - f: Any( - IntAtLeast(42), - IntAtMost(5), - ), - expectedErr: regexp.MustCompile("expected [\\w]+ to be at most \\(5\\), got 7"), - }, - }) -} - -func TestValidationIntBetween(t *testing.T) { - runTestCases(t, []testCase{ - { - val: 1, - f: IntBetween(1, 1), - }, - { - val: 1, - f: IntBetween(0, 2), - }, - { - val: 1, - f: IntBetween(2, 3), - expectedErr: regexp.MustCompile("expected [\\w]+ to be in the range \\(2 - 3\\), got 1"), - }, - { - val: "1", - f: IntBetween(2, 3), - expectedErr: regexp.MustCompile("expected type of [\\w]+ to be int"), - }, - }) -} - -func TestValidationIntAtLeast(t *testing.T) { - runTestCases(t, []testCase{ - { - val: 1, - f: IntAtLeast(1), - }, - { - val: 1, - f: IntAtLeast(0), - }, - { - val: 1, - f: IntAtLeast(2), - expectedErr: regexp.MustCompile("expected [\\w]+ to be at least \\(2\\), got 1"), - }, - { - val: "1", - f: IntAtLeast(2), - expectedErr: regexp.MustCompile("expected type of [\\w]+ to be int"), - }, - }) -} - -func TestValidationIntAtMost(t *testing.T) { - runTestCases(t, []testCase{ - { - val: 1, - f: IntAtMost(1), - }, - { - val: 1, - f: IntAtMost(2), - }, - { - val: 1, - f: IntAtMost(0), - expectedErr: regexp.MustCompile("expected [\\w]+ to be at most \\(0\\), got 1"), - }, - { - val: "1", - f: IntAtMost(0), - expectedErr: regexp.MustCompile("expected type of [\\w]+ to be int"), - }, - }) -} - -func TestValidationIntInSlice(t *testing.T) { - runTestCases(t, []testCase{ - { - val: 42, - f: IntInSlice([]int{1, 42}), - }, - { - val: 42, - f: IntInSlice([]int{10, 20}), - expectedErr: regexp.MustCompile("expected [\\w]+ to be one of \\[10 20\\], got 42"), - }, - { - val: "InvalidValue", - f: IntInSlice([]int{10, 20}), - expectedErr: regexp.MustCompile("expected type of [\\w]+ to be an integer"), - }, - }) -} - -func TestValidationStringInSlice(t *testing.T) { - runTestCases(t, []testCase{ - { - val: "ValidValue", - f: StringInSlice([]string{"ValidValue", "AnotherValidValue"}, false), - }, - // ignore case - { - val: "VALIDVALUE", - f: StringInSlice([]string{"ValidValue", "AnotherValidValue"}, true), - }, - { - val: "VALIDVALUE", - f: StringInSlice([]string{"ValidValue", "AnotherValidValue"}, false), - expectedErr: regexp.MustCompile("expected [\\w]+ to be one of \\[ValidValue AnotherValidValue\\], got VALIDVALUE"), - }, - { - val: "InvalidValue", - f: StringInSlice([]string{"ValidValue", "AnotherValidValue"}, false), - expectedErr: regexp.MustCompile("expected [\\w]+ to be one of \\[ValidValue AnotherValidValue\\], got InvalidValue"), - }, - { - val: 1, - f: StringInSlice([]string{"ValidValue", "AnotherValidValue"}, false), - expectedErr: regexp.MustCompile("expected type of [\\w]+ to be string"), - }, - }) -} - -func TestValidationStringMatch(t *testing.T) { - runTestCases(t, []testCase{ - { - val: "foobar", - f: StringMatch(regexp.MustCompile(".*foo.*"), ""), - }, - { - val: "bar", - f: StringMatch(regexp.MustCompile(".*foo.*"), ""), - expectedErr: regexp.MustCompile("expected value of [\\w]+ to match regular expression " + regexp.QuoteMeta(`".*foo.*"`)), - }, - { - val: "bar", - f: StringMatch(regexp.MustCompile(".*foo.*"), "value must contain foo"), - expectedErr: regexp.MustCompile("invalid value for [\\w]+ \\(value must contain foo\\)"), - }, - }) -} - -func TestValidationStringDoesNotMatch(t *testing.T) { - runTestCases(t, []testCase{ - { - val: "foobar", - f: StringDoesNotMatch(regexp.MustCompile(".*baz.*"), ""), - }, - { - val: "bar", - f: StringDoesNotMatch(regexp.MustCompile(".*bar.*"), ""), - expectedErr: regexp.MustCompile("expected value of [\\w]+ to not match regular expression " + regexp.QuoteMeta(`".*bar.*"`)), - }, - { - val: "bar", - f: StringDoesNotMatch(regexp.MustCompile(".*bar.*"), "value must not contain foo"), - expectedErr: regexp.MustCompile("invalid value for [\\w]+ \\(value must not contain foo\\)"), - }, - }) -} - -func TestValidationRegexp(t *testing.T) { - runTestCases(t, []testCase{ - { - val: ".*foo.*", - f: ValidateRegexp, - }, - { - val: "foo(bar", - f: ValidateRegexp, - expectedErr: regexp.MustCompile(regexp.QuoteMeta("error parsing regexp: missing closing ): `foo(bar`")), - }, - }) -} - -func TestValidationSingleIP(t *testing.T) { - runTestCases(t, []testCase{ - { - val: "172.10.10.10", - f: SingleIP(), - }, - { - val: "1.1.1", - f: SingleIP(), - expectedErr: regexp.MustCompile(regexp.QuoteMeta("expected test_property to contain a valid IP, got:")), - }, - { - val: "1.1.1.0/20", - f: SingleIP(), - expectedErr: regexp.MustCompile(regexp.QuoteMeta("expected test_property to contain a valid IP, got:")), - }, - { - val: "256.1.1.1", - f: SingleIP(), - expectedErr: regexp.MustCompile(regexp.QuoteMeta("expected test_property to contain a valid IP, got:")), - }, - }) -} - -func TestValidationIPRange(t *testing.T) { - runTestCases(t, []testCase{ - { - val: "172.10.10.10-172.10.10.12", - f: IPRange(), - }, - { - val: "172.10.10.20", - f: IPRange(), - expectedErr: regexp.MustCompile(regexp.QuoteMeta("expected test_property to contain a valid IP range, got:")), - }, - { - val: "172.10.10.20-172.10.10.12", - f: IPRange(), - expectedErr: regexp.MustCompile(regexp.QuoteMeta("expected test_property to contain a valid IP range, got:")), - }, - }) -} - -func TestValidateRFC3339TimeString(t *testing.T) { - runTestCases(t, []testCase{ - { - val: "2018-03-01T00:00:00Z", - f: ValidateRFC3339TimeString, - }, - { - val: "2018-03-01T00:00:00-05:00", - f: ValidateRFC3339TimeString, - }, - { - val: "2018-03-01T00:00:00+05:00", - f: ValidateRFC3339TimeString, - }, - { - val: "03/01/2018", - f: ValidateRFC3339TimeString, - expectedErr: regexp.MustCompile(regexp.QuoteMeta(`invalid RFC3339 timestamp`)), - }, - { - val: "03-01-2018", - f: ValidateRFC3339TimeString, - expectedErr: regexp.MustCompile(regexp.QuoteMeta(`invalid RFC3339 timestamp`)), - }, - { - val: "2018-03-01", - f: ValidateRFC3339TimeString, - expectedErr: regexp.MustCompile(regexp.QuoteMeta(`invalid RFC3339 timestamp`)), - }, - { - val: "2018-03-01T", - f: ValidateRFC3339TimeString, - expectedErr: regexp.MustCompile(regexp.QuoteMeta(`invalid RFC3339 timestamp`)), - }, - { - val: "2018-03-01T00:00:00", - f: ValidateRFC3339TimeString, - expectedErr: regexp.MustCompile(regexp.QuoteMeta(`invalid RFC3339 timestamp`)), - }, - { - val: "2018-03-01T00:00:00Z05:00", - f: ValidateRFC3339TimeString, - expectedErr: regexp.MustCompile(regexp.QuoteMeta(`invalid RFC3339 timestamp`)), - }, - { - val: "2018-03-01T00:00:00Z-05:00", - f: ValidateRFC3339TimeString, - expectedErr: regexp.MustCompile(regexp.QuoteMeta(`invalid RFC3339 timestamp`)), - }, - }) -} - -func TestValidateJsonString(t *testing.T) { - type testCases struct { - Value string - ErrCount int - } - - invalidCases := []testCases{ - { - Value: `{0:"1"}`, - ErrCount: 1, - }, - { - Value: `{'abc':1}`, - ErrCount: 1, - }, - { - Value: `{"def":}`, - ErrCount: 1, - }, - { - Value: `{"xyz":[}}`, - ErrCount: 1, - }, - } - - for _, tc := range invalidCases { - _, errors := ValidateJsonString(tc.Value, "json") - if len(errors) != tc.ErrCount { - t.Fatalf("Expected %q to trigger a validation error.", tc.Value) - } - } - - validCases := []testCases{ - { - Value: ``, - ErrCount: 0, - }, - { - Value: `{}`, - ErrCount: 0, - }, - { - Value: `{"abc":["1","2"]}`, - ErrCount: 0, - }, - } - - for _, tc := range validCases { - _, errors := ValidateJsonString(tc.Value, "json") - if len(errors) != tc.ErrCount { - t.Fatalf("Expected %q not to trigger a validation error.", tc.Value) - } - } -} - -func TestValidateListUniqueStrings(t *testing.T) { - runTestCases(t, []testCase{ - { - val: []interface{}{"foo", "bar"}, - f: ValidateListUniqueStrings, - }, - { - val: []interface{}{"foo", "bar", "foo"}, - f: ValidateListUniqueStrings, - expectedErr: regexp.MustCompile("duplicate entry - foo"), - }, - { - val: []interface{}{"foo", "bar", "foo", "baz", "bar"}, - f: ValidateListUniqueStrings, - expectedErr: regexp.MustCompile("duplicate entry - (?:foo|bar)"), - }, - }) -} - -func TestValidationNoZeroValues(t *testing.T) { - runTestCases(t, []testCase{ - { - val: "foo", - f: NoZeroValues, - }, - { - val: 1, - f: NoZeroValues, - }, - { - val: float64(1), - f: NoZeroValues, - }, - { - val: "", - f: NoZeroValues, - expectedErr: regexp.MustCompile("must not be empty"), - }, - { - val: 0, - f: NoZeroValues, - expectedErr: regexp.MustCompile("must not be zero"), - }, - { - val: float64(0), - f: NoZeroValues, - expectedErr: regexp.MustCompile("must not be zero"), - }, - }) -} - -func TestValidateFloatBetween(t *testing.T) { - cases := map[string]struct { - Value interface{} - ValidateFunc schema.SchemaValidateFunc - ExpectValidationErrors bool - }{ - "accept valid value": { - Value: 1.5, - ValidateFunc: FloatBetween(1.0, 2.0), - ExpectValidationErrors: false, - }, - "accept valid value inclusive upper bound": { - Value: 1.0, - ValidateFunc: FloatBetween(0.0, 1.0), - ExpectValidationErrors: false, - }, - "accept valid value inclusive lower bound": { - Value: 0.0, - ValidateFunc: FloatBetween(0.0, 1.0), - ExpectValidationErrors: false, - }, - "reject out of range value": { - Value: -1.0, - ValidateFunc: FloatBetween(0.0, 1.0), - ExpectValidationErrors: true, - }, - "reject incorrectly typed value": { - Value: 1, - ValidateFunc: FloatBetween(0.0, 1.0), - ExpectValidationErrors: true, - }, - } - - for tn, tc := range cases { - _, errors := tc.ValidateFunc(tc.Value, tn) - if len(errors) > 0 && !tc.ExpectValidationErrors { - t.Errorf("%s: unexpected errors %s", tn, errors) - } else if len(errors) == 0 && tc.ExpectValidationErrors { - t.Errorf("%s: expected errors but got none", tn) - } - } -} - -func TestValidateFloatAtLeast(t *testing.T) { - runTestCases(t, []testCase{ - { - val: 2.5, - f: FloatAtLeast(1.5), - }, - { - val: -1.0, - f: FloatAtLeast(-1.5), - }, - { - val: 1.5, - f: FloatAtLeast(2.5), - expectedErr: regexp.MustCompile("expected [\\w]+ to be at least \\(2\\.5\\d*\\), got 1\\.5\\d*"), - }, - { - val: "2.5", - f: FloatAtLeast(1.5), - expectedErr: regexp.MustCompile("expected type of [\\w]+ to be float"), - }, - }) -} - -func TestValidateFloatAtMost(t *testing.T) { - runTestCases(t, []testCase{ - { - val: 2.5, - f: FloatAtMost(3.5), - }, - { - val: -1.0, - f: FloatAtMost(-0.5), - }, - { - val: 2.5, - f: FloatAtMost(1.5), - expectedErr: regexp.MustCompile("expected [\\w]+ to be at most \\(1\\.5\\d*\\), got 2\\.5\\d*"), - }, - { - val: "2.5", - f: FloatAtMost(3.5), - expectedErr: regexp.MustCompile("expected type of [\\w]+ to be float"), - }, - }) -} - -func TestStringDoesNotContainAny(t *testing.T) { - chars := "|:/" - - validStrings := []string{ - "HelloWorld", - "ABC_*&^%123", - } - for _, v := range validStrings { - _, errors := StringDoesNotContainAny(chars)(v, "name") - if len(errors) != 0 { - t.Fatalf("%q should not contain any of %q", v, chars) - } - } - - invalidStrings := []string{ - "Hello/World", - "ABC|123", - "This will fail:", - chars, - } - for _, v := range invalidStrings { - _, errors := StringDoesNotContainAny(chars)(v, "name") - if len(errors) == 0 { - t.Fatalf("%q should contain one of %q", v, chars) - } - } -} - -func runTestCases(t *testing.T, cases []testCase) { - matchErr := func(errs []error, r *regexp.Regexp) bool { - // err must match one provided - for _, err := range errs { - if r.MatchString(err.Error()) { - return true - } - } - - return false - } - - for i, tc := range cases { - _, errs := tc.f(tc.val, "test_property") - - if len(errs) == 0 && tc.expectedErr == nil { - continue - } - - if len(errs) != 0 && tc.expectedErr == nil { - t.Fatalf("expected test case %d to produce no errors, got %v", i, errs) - } - - if !matchErr(errs, tc.expectedErr) { - t.Fatalf("expected test case %d to produce error matching \"%s\", got %v", i, tc.expectedErr, errs) - } - } -} From 9a8633808efe2639230e793f491a8136008d9f44 Mon Sep 17 00:00:00 2001 From: kt Date: Tue, 14 Jan 2020 15:28:44 -0800 Subject: [PATCH 02/14] cleanup --- helper/validation/strings_test.go | 4 ---- helper/validation/time.go | 3 --- 2 files changed, 7 deletions(-) diff --git a/helper/validation/strings_test.go b/helper/validation/strings_test.go index 7beab3f6da5..d60044f17b4 100644 --- a/helper/validation/strings_test.go +++ b/helper/validation/strings_test.go @@ -447,10 +447,6 @@ func TestStringDoesNotContainAny(t *testing.T) { } } - - - - func TestValidationRegexp(t *testing.T) { runTestCases(t, []testCase{ { diff --git a/helper/validation/time.go b/helper/validation/time.go index bceafea3345..f35c58c4a94 100644 --- a/helper/validation/time.go +++ b/helper/validation/time.go @@ -2,10 +2,7 @@ package validation import ( "fmt" - "reflect" "time" - - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" ) From ee4fe2a2f61250bbf697537baf508354397dad01 Mon Sep 17 00:00:00 2001 From: kt Date: Tue, 14 Jan 2020 15:30:08 -0800 Subject: [PATCH 03/14] make fmt --- helper/validation/float.go | 4 ++-- helper/validation/float_test.go | 4 ++-- helper/validation/int.go | 4 ++-- helper/validation/int_test.go | 3 +-- helper/validation/meta.go | 6 +++--- helper/validation/meta_test.go | 7 +++---- helper/validation/network.go | 2 +- helper/validation/network_test.go | 6 +++--- helper/validation/strings.go | 11 +++++------ helper/validation/strings_test.go | 4 +--- helper/validation/testing.go | 7 +++---- helper/validation/time.go | 3 +-- 12 files changed, 27 insertions(+), 34 deletions(-) diff --git a/helper/validation/float.go b/helper/validation/float.go index 477563bb8e9..f0bfbfa530d 100644 --- a/helper/validation/float.go +++ b/helper/validation/float.go @@ -1,9 +1,9 @@ package validation import ( - `fmt` + "fmt" - `github.com/hashicorp/terraform-plugin-sdk/helper/schema` + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" ) // FloatBetween returns a SchemaValidateFunc which tests if the provided value diff --git a/helper/validation/float_test.go b/helper/validation/float_test.go index 2f0d51f5416..3bb8f1aee30 100644 --- a/helper/validation/float_test.go +++ b/helper/validation/float_test.go @@ -1,10 +1,10 @@ package validation import ( - `regexp` + "regexp" "testing" - `github.com/hashicorp/terraform-plugin-sdk/helper/schema` + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" ) func TestValidateFloatBetween(t *testing.T) { diff --git a/helper/validation/int.go b/helper/validation/int.go index 9ed3b3fb078..54b082feb16 100644 --- a/helper/validation/int.go +++ b/helper/validation/int.go @@ -1,9 +1,9 @@ package validation import ( - `fmt` + "fmt" - `github.com/hashicorp/terraform-plugin-sdk/helper/schema` + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" ) // IntBetween returns a SchemaValidateFunc which tests if the provided value diff --git a/helper/validation/int_test.go b/helper/validation/int_test.go index 4d6ec88bb6f..b3358b796e0 100644 --- a/helper/validation/int_test.go +++ b/helper/validation/int_test.go @@ -5,7 +5,6 @@ import ( "testing" ) - func TestValidationIntBetween(t *testing.T) { runTestCases(t, []testCase{ { @@ -92,4 +91,4 @@ func TestValidationIntInSlice(t *testing.T) { expectedErr: regexp.MustCompile("expected type of [\\w]+ to be an integer"), }, }) -} \ No newline at end of file +} diff --git a/helper/validation/meta.go b/helper/validation/meta.go index 2dcbcc87f04..e2948ace2ef 100644 --- a/helper/validation/meta.go +++ b/helper/validation/meta.go @@ -1,10 +1,10 @@ package validation import ( - `fmt` - `reflect` + "fmt" + "reflect" - `github.com/hashicorp/terraform-plugin-sdk/helper/schema` + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" ) // NoZeroValues is a SchemaValidateFunc which tests if the provided value is diff --git a/helper/validation/meta_test.go b/helper/validation/meta_test.go index ab36d6cad49..1ec0b9c66a8 100644 --- a/helper/validation/meta_test.go +++ b/helper/validation/meta_test.go @@ -1,11 +1,10 @@ package validation import ( - `regexp` - `testing` + "regexp" + "testing" ) - func TestValidationNoZeroValues(t *testing.T) { runTestCases(t, []testCase{ { @@ -99,4 +98,4 @@ func TestValidationAny(t *testing.T) { expectedErr: regexp.MustCompile("expected [\\w]+ to be at most \\(5\\), got 7"), }, }) -} \ No newline at end of file +} diff --git a/helper/validation/network.go b/helper/validation/network.go index d2a17d1572c..a53725a2996 100644 --- a/helper/validation/network.go +++ b/helper/validation/network.go @@ -86,4 +86,4 @@ func IPRange() schema.SchemaValidateFunc { } return } -} \ No newline at end of file +} diff --git a/helper/validation/network_test.go b/helper/validation/network_test.go index 5f32ef7dc11..25769404c34 100644 --- a/helper/validation/network_test.go +++ b/helper/validation/network_test.go @@ -1,8 +1,8 @@ package validation import ( - `regexp` - `testing` + "regexp" + "testing" ) func TestValidationSingleIP(t *testing.T) { @@ -46,4 +46,4 @@ func TestValidationIPRange(t *testing.T) { expectedErr: regexp.MustCompile(regexp.QuoteMeta("expected test_property to contain a valid IP range, got:")), }, }) -} \ No newline at end of file +} diff --git a/helper/validation/strings.go b/helper/validation/strings.go index 35949fe80f9..067b77d11a5 100644 --- a/helper/validation/strings.go +++ b/helper/validation/strings.go @@ -1,13 +1,13 @@ package validation import ( - `encoding/base64` + "encoding/base64" "fmt" - `regexp` + "regexp" "strings" - `github.com/hashicorp/terraform-plugin-sdk/helper/schema` - `github.com/hashicorp/terraform-plugin-sdk/helper/structure` + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/structure" ) // StringIsNotEmpty is a ValidateFunc that ensures a string is not empty or consisting entirely of whitespace characters @@ -165,7 +165,6 @@ func StringDoesNotContainAny(chars string) schema.SchemaValidateFunc { } } - // StringIsBase64 is a ValidateFunc that ensures a string can be parsed as Base64 func StringIsBase64(i interface{}, k string) (warnings []string, errors []error) { // Empty string is not allowed @@ -212,4 +211,4 @@ func ValidateRegexp(v interface{}, k string) (ws []string, errors []error) { errors = append(errors, fmt.Errorf("%q: %s", k, err)) } return -} \ No newline at end of file +} diff --git a/helper/validation/strings_test.go b/helper/validation/strings_test.go index d60044f17b4..3f96d9e2094 100644 --- a/helper/validation/strings_test.go +++ b/helper/validation/strings_test.go @@ -1,7 +1,7 @@ package validation import ( - `regexp` + "regexp" "testing" ) @@ -343,7 +343,6 @@ func TestValidationStringDoesNotMatch(t *testing.T) { }) } - func TestValidateJsonString(t *testing.T) { type testCases struct { Value string @@ -418,7 +417,6 @@ func TestValidateListUniqueStrings(t *testing.T) { }) } - func TestStringDoesNotContainAny(t *testing.T) { chars := "|:/" diff --git a/helper/validation/testing.go b/helper/validation/testing.go index b11ea2353f6..8a2da7f892b 100644 --- a/helper/validation/testing.go +++ b/helper/validation/testing.go @@ -1,10 +1,10 @@ package validation import ( - `regexp` - `testing` + "regexp" + "testing" - `github.com/hashicorp/terraform-plugin-sdk/helper/schema` + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" ) type testCase struct { @@ -13,7 +13,6 @@ type testCase struct { expectedErr *regexp.Regexp } - func runTestCases(t *testing.T, cases []testCase) { matchErr := func(errs []error, r *regexp.Regexp) bool { // err must match one provided diff --git a/helper/validation/time.go b/helper/validation/time.go index f35c58c4a94..30010ceb91d 100644 --- a/helper/validation/time.go +++ b/helper/validation/time.go @@ -5,7 +5,6 @@ import ( "time" ) - // ValidateRFC3339TimeString is a ValidateFunc that ensures a string parses // as time.RFC3339 format func ValidateRFC3339TimeString(v interface{}, k string) (ws []string, errors []error) { @@ -13,4 +12,4 @@ func ValidateRFC3339TimeString(v interface{}, k string) (ws []string, errors []e errors = append(errors, fmt.Errorf("%q: invalid RFC3339 timestamp", k)) } return -} \ No newline at end of file +} From c1005d6cd50c7a2af487264b1ac212ea54564dde Mon Sep 17 00:00:00 2001 From: kt Date: Tue, 14 Jan 2020 22:49:36 -0800 Subject: [PATCH 04/14] validation: add Int, Network, Time and Web validators --- helper/validation/int.go | 40 ++++ helper/validation/int_test.go | 82 ++++++++ helper/validation/network.go | 156 +++++++++++---- helper/validation/network_test.go | 322 +++++++++++++++++++++++++++++- helper/validation/time.go | 56 +++++- helper/validation/time_test.go | 113 ++++++----- helper/validation/web.go | 52 +++++ helper/validation/web_test.go | 101 ++++++++++ 8 files changed, 825 insertions(+), 97 deletions(-) create mode 100644 helper/validation/web.go create mode 100644 helper/validation/web_test.go diff --git a/helper/validation/int.go b/helper/validation/int.go index 54b082feb16..47014cbd123 100644 --- a/helper/validation/int.go +++ b/helper/validation/int.go @@ -2,6 +2,7 @@ package validation import ( "fmt" + "math" "github.com/hashicorp/terraform-plugin-sdk/helper/schema" ) @@ -63,6 +64,25 @@ func IntAtMost(max int) schema.SchemaValidateFunc { } } +// IntDivisibleBy returns a SchemaValidateFunc which tests if the provided value +// is of type int and is divisible by a given number +func IntDivisibleBy(divisor int) schema.SchemaValidateFunc { + return func(i interface{}, k string) (warnings []string, errors []error) { + v, ok := i.(int) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %s to be int", k)) + return + } + + if math.Mod(float64(v), float64(divisor)) != 0 { + errors = append(errors, fmt.Errorf("expected %s to be divisible by %d, got: %v", k, divisor, i)) + return + } + + return warnings, errors + } +} + // IntInSlice returns a SchemaValidateFunc which tests if the provided value // is of type int and matches the value of an element in the valid slice func IntInSlice(valid []int) schema.SchemaValidateFunc { @@ -83,3 +103,23 @@ func IntInSlice(valid []int) schema.SchemaValidateFunc { return } } + +// IntInSlice returns a SchemaValidateFunc which tests if the provided value +// is of type int and matches the value of an element in the valid slice +func IntNotInSlice(valid []int) schema.SchemaValidateFunc { + return func(i interface{}, k string) (_ []string, errors []error) { + v, ok := i.(int) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %s to be an integer", k)) + return + } + + for _, validInt := range valid { + if v == validInt { + errors = append(errors, fmt.Errorf("expected %s to not be one of %v, got %d", k, valid, v)) + } + } + + return + } +} diff --git a/helper/validation/int_test.go b/helper/validation/int_test.go index b3358b796e0..5fcf93a165d 100644 --- a/helper/validation/int_test.go +++ b/helper/validation/int_test.go @@ -74,6 +74,42 @@ func TestValidationIntAtMost(t *testing.T) { }) } +func TestValidationIntDivisibleBy(t *testing.T) { + cases := map[string]struct { + Value interface{} + Divisor int + Error bool + }{ + "NotInt": { + Value: "words", + Divisor: 2, + Error: true, + }, + "NotDivisible": { + Value: 15, + Divisor: 7, + Error: true, + }, + "Divisible": { + Value: 14, + Divisor: 7, + Error: false, + }, + } + + for tn, tc := range cases { + t.Run(tn, func(t *testing.T) { + _, errors := IntDivisibleBy(tc.Divisor)(tc.Value, tn) + + if len(errors) > 0 && !tc.Error { + t.Errorf("IntDivisibleBy(%v) produced an unexpected error for %v", tc.Divisor, tc.Value) + } else if len(errors) == 0 && tc.Error { + t.Errorf("IntDivisibleBy(%v) did not error for %v", tc.Divisor, tc.Value) + } + }) + } +} + func TestValidationIntInSlice(t *testing.T) { runTestCases(t, []testCase{ { @@ -92,3 +128,49 @@ func TestValidationIntInSlice(t *testing.T) { }, }) } + +func TestValidationIntNotInSlice(t *testing.T) { + cases := map[string]struct { + Value interface{} + Slice []int + Error bool + }{ + "NotInt": { + Value: "words", + Slice: []int{7, 77}, + Error: true, + }, + "NotInSlice": { + Value: 1, + Slice: []int{7, 77}, + Error: false, + }, + "InSlice": { + Value: 7, + Slice: []int{7, 77}, + Error: true, + }, + "InSliceOfOne": { + Value: 7, + Slice: []int{7}, + Error: true, + }, + "NotInSliceOfOne": { + Value: 1, + Slice: []int{7}, + Error: false, + }, + } + + for tn, tc := range cases { + t.Run(tn, func(t *testing.T) { + _, errors := IntNotInSlice(tc.Slice)(tc.Value, tn) + + if len(errors) > 0 && !tc.Error { + t.Errorf("IntNotInSlice(%v) produced an unexpected error for %v", tc.Slice, tc.Value) + } else if len(errors) == 0 && tc.Error { + t.Errorf("IntNotInSlice(%v) did not error for %v", tc.Slice, tc.Value) + } + }) + } +} diff --git a/helper/validation/network.go b/helper/validation/network.go index a53725a2996..b9140af7ef6 100644 --- a/helper/validation/network.go +++ b/helper/validation/network.go @@ -9,9 +9,9 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/helper/schema" ) -// CIDRNetwork returns a SchemaValidateFunc which tests if the provided value -// is of type string, is in valid CIDR network notation, and has significant bits between min and max (inclusive) -func CIDRNetwork(min, max int) schema.SchemaValidateFunc { +// SingleIP returns a SchemaValidateFunc which tests if the provided value +// is of type string, and in valid single Value notation +func SingleIP() schema.SchemaValidateFunc { return func(i interface{}, k string) (s []string, es []error) { v, ok := i.(string) if !ok { @@ -19,33 +19,47 @@ func CIDRNetwork(min, max int) schema.SchemaValidateFunc { return } - _, ipnet, err := net.ParseCIDR(v) - if err != nil { - es = append(es, fmt.Errorf( - "expected %s to contain a valid CIDR, got: %s with err: %s", k, v, err)) - return + ip := net.ParseIP(v) + if ip == nil { + es = append(es, fmt.Errorf("expected %s to contain a valid Value, got: %s", k, v)) } + return + } +} - if ipnet == nil || v != ipnet.String() { - es = append(es, fmt.Errorf( - "expected %s to contain a valid network CIDR, expected %s, got %s", - k, ipnet, v)) - } +func IPv6Address(i interface{}, k string) (warnings []string, errors []error) { + v, ok := i.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %q to be string", k)) + return + } - sigbits, _ := ipnet.Mask.Size() - if sigbits < min || sigbits > max { - es = append(es, fmt.Errorf( - "expected %q to contain a network CIDR with between %d and %d significant bits, got: %d", - k, min, max, sigbits)) - } + ip := net.ParseIP(v) + if six := ip.To16(); six == nil { + errors = append(errors, fmt.Errorf("expected %s to contain a valid IPv6 address, got: %s", k, v)) + } + + return warnings, errors +} +func IPv4Address(i interface{}, k string) (warnings []string, errors []error) { + v, ok := i.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %q to be string", k)) return } + + ip := net.ParseIP(v) + if four := ip.To4(); four == nil { + errors = append(errors, fmt.Errorf("expected %s to contain a valid IPv4 address, got: %s", k, v)) + } + + return warnings, errors } -// SingleIP returns a SchemaValidateFunc which tests if the provided value -// is of type string, and in valid single IP notation -func SingleIP() schema.SchemaValidateFunc { +// IPRange returns a SchemaValidateFunc which tests if the provided value +// is of type string, and in valid Value range notation +func IPRange() schema.SchemaValidateFunc { return func(i interface{}, k string) (s []string, es []error) { v, ok := i.(string) if !ok { @@ -53,18 +67,40 @@ func SingleIP() schema.SchemaValidateFunc { return } - ip := net.ParseIP(v) - if ip == nil { + ips := strings.Split(v, "-") + if len(ips) != 2 { es = append(es, fmt.Errorf( - "expected %s to contain a valid IP, got: %s", k, v)) + "expected %s to contain a valid Value range, got: %s", k, v)) + return + } + ip1 := net.ParseIP(ips[0]) + ip2 := net.ParseIP(ips[1]) + if ip1 == nil || ip2 == nil || bytes.Compare(ip1, ip2) > 0 { + es = append(es, fmt.Errorf( + "expected %s to contain a valid Value range, got: %s", k, v)) } return } } -// IPRange returns a SchemaValidateFunc which tests if the provided value -// is of type string, and in valid IP range notation -func IPRange() schema.SchemaValidateFunc { +func CIDR(i interface{}, k string) (warnings []string, errors []error) { + v, ok := i.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %s to be string", k)) + return + } + + _, _, err := net.ParseCIDR(v) + if err != nil { + errors = append(errors, fmt.Errorf("expected %q to be a valid IPv4 Value, got %v: %v", k, i, err)) + } + + return warnings, errors +} + +// CIDRNetwork returns a SchemaValidateFunc which tests if the provided value +// is of type string, is in valid Value network notation, and has significant bits between min and max (inclusive) +func CIDRNetwork(min, max int) schema.SchemaValidateFunc { return func(i interface{}, k string) (s []string, es []error) { v, ok := i.(string) if !ok { @@ -72,18 +108,68 @@ func IPRange() schema.SchemaValidateFunc { return } - ips := strings.Split(v, "-") - if len(ips) != 2 { + _, ipnet, err := net.ParseCIDR(v) + if err != nil { es = append(es, fmt.Errorf( - "expected %s to contain a valid IP range, got: %s", k, v)) + "expected %s to contain a valid Value, got: %s with err: %s", k, v, err)) return } - ip1 := net.ParseIP(ips[0]) - ip2 := net.ParseIP(ips[1]) - if ip1 == nil || ip2 == nil || bytes.Compare(ip1, ip2) > 0 { + + if ipnet == nil || v != ipnet.String() { + es = append(es, fmt.Errorf( + "expected %s to contain a valid network Value, expected %s, got %s", + k, ipnet, v)) + } + + sigbits, _ := ipnet.Mask.Size() + if sigbits < min || sigbits > max { es = append(es, fmt.Errorf( - "expected %s to contain a valid IP range, got: %s", k, v)) + "expected %q to contain a network Value with between %d and %d significant bits, got: %d", + k, min, max, sigbits)) } + + return + } +} + +func MACAddress(i interface{}, k string) (warnings []string, errors []error) { + v, ok := i.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %q to be string", k)) return } + + if _, err := net.ParseMAC(v); err != nil { + errors = append(errors, fmt.Errorf("expected %q to be a valid MAC address, got %v: %v", k, i, err)) + } + + return warnings, errors +} + +func PortNumber(i interface{}, k string) (warnings []string, errors []error) { + v, ok := i.(int) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %q to be int", k)) + return + } + + if v < 1 || 65535 < v { + errors = append(errors, fmt.Errorf("expected %q to be a valid port number, got: %v", k, v)) + } + + return warnings, errors +} + +func PortNumberOrZero(i interface{}, k string) (warnings []string, errors []error) { + v, ok := i.(int) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %q to be int", k)) + return + } + + if v < 0 || 65535 < v { + errors = append(errors, fmt.Errorf("expected %q to be a valid port number or 0, got: %v", k, v)) + } + + return warnings, errors } diff --git a/helper/validation/network_test.go b/helper/validation/network_test.go index 25769404c34..462b7d7f85c 100644 --- a/helper/validation/network_test.go +++ b/helper/validation/network_test.go @@ -5,6 +5,162 @@ import ( "testing" ) +func TestValidateCIDR(t *testing.T) { + cases := map[string]struct { + Value string + Error bool + }{ + "NotString": { + Value: "777", + Error: true, + }, + "Empty": { + Value: "", + Error: true, + }, + "Zeros": { + Value: "0.0.0.0", + Error: true, + }, + "Slash8": { + Value: "127.0.0.1/8", + Error: false, + }, + "Slash33": { + Value: "127.0.0.1/33", + Error: true, + }, + "Slash-1": { + Value: "127.0.0.1/-1", + Error: true, + }, + } + + for tn, tc := range cases { + t.Run(tn, func(t *testing.T) { + _, errors := CIDR(tc.Value, tn) + + if len(errors) > 0 && !tc.Error { + t.Errorf("CIDR(%s) produced an unexpected error", tc.Value) + } else if len(errors) == 0 && tc.Error { + t.Errorf("CIDR(%s) did not error", tc.Value) + } + }) + } +} + +func TestValidateIPv6Address(t *testing.T) { + cases := map[string]struct { + Value string + Error bool + }{ + "NotString": { + Value: "777", + Error: true, + }, + "Empty": { + Value: "", + Error: true, + }, + "ZeroIpv4": { + Value: "0.0.0.0", + Error: false, + }, + "NotARealAddress": { + Value: "not:a:real:address:1:2:3:4", + Error: true, + }, + "Text": { + Value: "text", + Error: true, + }, + "Colons": { + Value: "::", + Error: false, + }, + "ZeroIPv6": { + Value: "0:0:0:0:0:0:0:0", + Error: false, + }, + "Valid1": { + Value: "2001:0db8:85a3:0:0:8a2e:0370:7334", + Error: false, + }, + "Valid2": { + Value: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + Error: false, + }, + } + + for tn, tc := range cases { + t.Run(tn, func(t *testing.T) { + _, errors := IPv6Address(tc.Value, tn) + + if len(errors) > 0 && !tc.Error { + t.Errorf("IPv6Address(%s) produced an unexpected error", tc.Value) + } else if len(errors) == 0 && tc.Error { + t.Errorf("IPv6Address(%s) did not error", tc.Value) + } + }) + } +} + +func TestValidateIPv4Address(t *testing.T) { + cases := map[string]struct { + Value string + Error bool + }{ + "NotString": { + Value: "777", + Error: true, + }, + "Empty": { + Value: "", + Error: true, + }, + "Zeros": { + Value: "0.0.0.0", + Error: false, + }, + "Chars": { + Value: "1.2.3.no", + Error: true, + }, + "Text": { + Value: "text", + Error: true, + }, + "Valid": { + Value: "1.2.3.4", + Error: false, + }, + "Valid10s": { + Value: "12.34.43.21", + Error: false, + }, + "Valid100s": { + Value: "100.123.199.0", + Error: false, + }, + "Valid255": { + Value: "255.255.255.255", + Error: false, + }, + } + + for tn, tc := range cases { + t.Run(tn, func(t *testing.T) { + _, errors := IPv4Address(tc.Value, tn) + + if len(errors) > 0 && !tc.Error { + t.Errorf("IPv4Address(%s) produced an unexpected error", tc.Value) + } else if len(errors) == 0 && tc.Error { + t.Errorf("IPv4Address(%s) did not error", tc.Value) + } + }) + } +} + func TestValidationSingleIP(t *testing.T) { runTestCases(t, []testCase{ { @@ -14,17 +170,17 @@ func TestValidationSingleIP(t *testing.T) { { val: "1.1.1", f: SingleIP(), - expectedErr: regexp.MustCompile(regexp.QuoteMeta("expected test_property to contain a valid IP, got:")), + expectedErr: regexp.MustCompile(regexp.QuoteMeta("expected test_property to contain a valid Value, got:")), }, { val: "1.1.1.0/20", f: SingleIP(), - expectedErr: regexp.MustCompile(regexp.QuoteMeta("expected test_property to contain a valid IP, got:")), + expectedErr: regexp.MustCompile(regexp.QuoteMeta("expected test_property to contain a valid Value, got:")), }, { val: "256.1.1.1", f: SingleIP(), - expectedErr: regexp.MustCompile(regexp.QuoteMeta("expected test_property to contain a valid IP, got:")), + expectedErr: regexp.MustCompile(regexp.QuoteMeta("expected test_property to contain a valid Value, got:")), }, }) } @@ -38,12 +194,168 @@ func TestValidationIPRange(t *testing.T) { { val: "172.10.10.20", f: IPRange(), - expectedErr: regexp.MustCompile(regexp.QuoteMeta("expected test_property to contain a valid IP range, got:")), + expectedErr: regexp.MustCompile(regexp.QuoteMeta("expected test_property to contain a valid Value range, got:")), }, { val: "172.10.10.20-172.10.10.12", f: IPRange(), - expectedErr: regexp.MustCompile(regexp.QuoteMeta("expected test_property to contain a valid IP range, got:")), + expectedErr: regexp.MustCompile(regexp.QuoteMeta("expected test_property to contain a valid Value range, got:")), }, }) } + +func TestValidationMACAddress(t *testing.T) { + cases := map[string]struct { + Value interface{} + Error bool + }{ + "NotString": { + Value: "777", + Error: true, + }, + "Empty": { + Value: "", + Error: true, + }, + "Text": { + Value: "text d", + Error: true, + }, + "Gibberish": { + Value: "12:34:no", + Error: true, + }, + "InvalidOctetSize": { + Value: "123:34:56:78:90:ab", + Error: true, + }, + "InvalidOctetChars": { + Value: "12:34:56:78:90:NO", + Error: true, + }, + "ValidLowercase": { + Value: "12:34:56:78:90:ab", + Error: false, + }, + "ValidUppercase": { + Value: "ab:cd:ef:AB:CD:EF", + Error: false, + }, + } + + for tn, tc := range cases { + t.Run(tn, func(t *testing.T) { + _, errors := MACAddress(tc.Value, tn) + + if len(errors) > 0 && !tc.Error { + t.Errorf("MACAddress(%s) produced an unexpected error", tc.Value) + } else if len(errors) == 0 && tc.Error { + t.Errorf("MACAddress(%s) did not error", tc.Value) + } + }) + } +} + +func TestValidationPortNumber(t *testing.T) { + cases := map[string]struct { + Value interface{} + Error bool + }{ + "NotInt": { + Value: "kt", + Error: true, + }, + "Negative": { + Value: -1, + Error: true, + }, + "Zero": { + Value: 0, + Error: true, + }, + "One": { + Value: 1, + Error: false, + }, + "Valid": { + Value: 8477, + Error: false, + }, + "MaxPort": { + Value: 65535, + Error: false, + }, + "OneToHigh": { + Value: 65536, + Error: true, + }, + "HugeNumber": { + Value: 7000000, + Error: true, + }, + } + + for tn, tc := range cases { + t.Run(tn, func(t *testing.T) { + _, errors := PortNumber(tc.Value, tn) + + if len(errors) > 0 && !tc.Error { + t.Errorf("PortNumber(%s) produced an unexpected error", tc.Value) + } else if len(errors) == 0 && tc.Error { + t.Errorf("PortNumber(%s) did not error", tc.Value) + } + }) + } +} + +func TestValidationPortNumberOrZero(t *testing.T) { + cases := map[string]struct { + Value interface{} + Error bool + }{ + "NotInt": { + Value: "kt", + Error: true, + }, + "Negative": { + Value: -1, + Error: true, + }, + "Zero": { + Value: 0, + Error: false, + }, + "One": { + Value: 1, + Error: false, + }, + "Valid": { + Value: 8477, + Error: false, + }, + "MaxPort": { + Value: 65535, + Error: false, + }, + "OneToHigh": { + Value: 65536, + Error: true, + }, + "HugeNumber": { + Value: 7000000, + Error: true, + }, + } + + for tn, tc := range cases { + t.Run(tn, func(t *testing.T) { + _, errors := PortNumberOrZero(tc.Value, tn) + + if len(errors) > 0 && !tc.Error { + t.Errorf("PortNumberOrZero(%s) produced an unexpected error", tc.Value) + } else if len(errors) == 0 && tc.Error { + t.Errorf("PortNumberOrZero(%s) did not error", tc.Value) + } + }) + } +} diff --git a/helper/validation/time.go b/helper/validation/time.go index 30010ceb91d..d2d4d3c0510 100644 --- a/helper/validation/time.go +++ b/helper/validation/time.go @@ -3,13 +3,57 @@ package validation import ( "fmt" "time" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" ) -// ValidateRFC3339TimeString is a ValidateFunc that ensures a string parses -// as time.RFC3339 format -func ValidateRFC3339TimeString(v interface{}, k string) (ws []string, errors []error) { - if _, err := time.Parse(time.RFC3339, v.(string)); err != nil { - errors = append(errors, fmt.Errorf("%q: invalid RFC3339 timestamp", k)) +func StringIsDayOfTheWeek(ignoreCase bool) schema.SchemaValidateFunc { + return StringInSlice([]string{ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday", + }, ignoreCase) +} + +func StringIsMonth(ignoreCase bool) schema.SchemaValidateFunc { + return StringInSlice([]string{ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + }, ignoreCase) +} + +// ValidateRFC3339TimeString is a ValidateFunc that ensures a string parses as time.RFC3339 format +func StringIsRFC3339Time(i interface{}, k string) (warnings []string, errors []error) { + v, ok := i.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %q to be string", k)) + return } - return + + if _, err := time.Parse(time.RFC3339, v); err != nil { + errors = append(errors, fmt.Errorf("expected %q to be a valid RFC3339 date, got %q: %+v", k, i, err)) + } + + return warnings, errors +} + +// ValidateRFC3339TimeString is a ValidateFunc that ensures a string parses as time.RFC3339 format +// +// Deprecated: use StringIsRFC3339Time() instead +func ValidateRFC3339TimeString(v interface{}, k string) (ws []string, errors []error) { + return StringIsRFC3339Time(v, k) } diff --git a/helper/validation/time_test.go b/helper/validation/time_test.go index ac641783e72..eb2531afb5b 100644 --- a/helper/validation/time_test.go +++ b/helper/validation/time_test.go @@ -1,58 +1,69 @@ package validation import ( - "regexp" "testing" ) -func TestValidateRFC3339TimeString(t *testing.T) { - runTestCases(t, []testCase{ - { - val: "2018-03-01T00:00:00Z", - f: ValidateRFC3339TimeString, - }, - { - val: "2018-03-01T00:00:00-05:00", - f: ValidateRFC3339TimeString, - }, - { - val: "2018-03-01T00:00:00+05:00", - f: ValidateRFC3339TimeString, - }, - { - val: "03/01/2018", - f: ValidateRFC3339TimeString, - expectedErr: regexp.MustCompile(regexp.QuoteMeta(`invalid RFC3339 timestamp`)), - }, - { - val: "03-01-2018", - f: ValidateRFC3339TimeString, - expectedErr: regexp.MustCompile(regexp.QuoteMeta(`invalid RFC3339 timestamp`)), - }, - { - val: "2018-03-01", - f: ValidateRFC3339TimeString, - expectedErr: regexp.MustCompile(regexp.QuoteMeta(`invalid RFC3339 timestamp`)), - }, - { - val: "2018-03-01T", - f: ValidateRFC3339TimeString, - expectedErr: regexp.MustCompile(regexp.QuoteMeta(`invalid RFC3339 timestamp`)), - }, - { - val: "2018-03-01T00:00:00", - f: ValidateRFC3339TimeString, - expectedErr: regexp.MustCompile(regexp.QuoteMeta(`invalid RFC3339 timestamp`)), - }, - { - val: "2018-03-01T00:00:00Z05:00", - f: ValidateRFC3339TimeString, - expectedErr: regexp.MustCompile(regexp.QuoteMeta(`invalid RFC3339 timestamp`)), - }, - { - val: "2018-03-01T00:00:00Z-05:00", - f: ValidateRFC3339TimeString, - expectedErr: regexp.MustCompile(regexp.QuoteMeta(`invalid RFC3339 timestamp`)), - }, - }) +func TestValidationStringIsRFC3339Time(t *testing.T) { + cases := map[string]struct { + Value interface{} + Error bool + }{ + "NotString": { + Value: 7, + Error: true, + }, + "ValidDate": { + Value: "2018-03-01T00:00:00Z", + Error: false, + }, + "ValidDateTime": { + Value: "2018-03-01T00:00:00-05:00", + Error: false, + }, + "ValidDateTime2": { + Value: "2018-03-01T00:00:00+05:00", + Error: false, + }, + "InvalidDateWithSlashes": { + Value: "03/01/2018", + Error: true, + }, + "InvalidDateWithDashes": { + Value: "03-01-2018", + Error: true, + }, + "InvalidDateWithDashes2": { + Value: "2018-03-01", + Error: true, + }, + "InvalidDateWithT": { + Value: "2018-03-01T", + Error: true, + }, + "DateTimeWithoutZone": { + Value: "2018-03-01T00:00:00", + Error: true, + }, + "DateTimeWithZZone": { + Value: "2018-03-01T00:00:00Z05:00", + Error: true, + }, + "DateTimeWithZZoneNeg": { + Value: "2018-03-01T00:00:00Z-05:00", + Error: true, + }, + } + + for tn, tc := range cases { + t.Run(tn, func(t *testing.T) { + _, errors := StringIsRFC3339Time(tc.Value, tn) + + if len(errors) > 0 && !tc.Error { + t.Errorf("StringIsRFC3339Time(%s) produced an unexpected error", tc.Value) + } else if len(errors) == 0 && tc.Error { + t.Errorf("StringIsRFC3339Time(%s) did not error", tc.Value) + } + }) + } } diff --git a/helper/validation/web.go b/helper/validation/web.go new file mode 100644 index 00000000000..d17eb0e78ef --- /dev/null +++ b/helper/validation/web.go @@ -0,0 +1,52 @@ +package validation + +import ( + "fmt" + "net/url" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +func URLIsHTTPS(i interface{}, k string) (_ []string, errors []error) { + return URLWithScheme([]string{"https"})(i, k) +} + +func URLIsHTTPOrHTTPS(i interface{}, k string) (_ []string, errors []error) { + return URLWithScheme([]string{"http", "https"})(i, k) +} + +func URLWithScheme(validSchemes []string) schema.SchemaValidateFunc { + return func(i interface{}, k string) (_ []string, errors []error) { + v, ok := i.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %q to be string", k)) + return + } + + if v == "" { + errors = append(errors, fmt.Errorf("expected %q url to not be empty", k)) + return + } + + u, err := url.Parse(v) + if err != nil { + errors = append(errors, fmt.Errorf("%q url is in an invalid format: %q (%+v)", k, v, err)) + return + } + + if u.Host == "" { + errors = append(errors, fmt.Errorf("%q url has no host: %q", k, v)) + return + } + + for _, s := range validSchemes { + if u.Scheme == s { + return //last check so just return + } + } + + errors = append(errors, fmt.Errorf("expected %q url %q to have a schema of: %q", k, v, strings.Join(validSchemes, ","))) + return + } +} diff --git a/helper/validation/web_test.go b/helper/validation/web_test.go new file mode 100644 index 00000000000..b8484e6b4a4 --- /dev/null +++ b/helper/validation/web_test.go @@ -0,0 +1,101 @@ +package validation + +import ( + "testing" +) + +func TestURLIsHTTPS(t *testing.T) { + cases := map[string]struct { + Value interface{} + Error bool + }{ + "NotString": { + Value: 7, + Error: true, + }, + "Empty": { + Value: "", + Error: true, + }, + "NotUrl": { + Value: "this is not a url", + Error: true, + }, + "BareUrl": { + Value: "www.example.com", + Error: true, + }, + "FtpUrl": { + Value: "ftp://www.example.com", + Error: true, + }, + "HttpUrl": { + Value: "http://www.example.com", + Error: true, + }, + "HttpsUrl": { + Value: "https://www.example.com", + Error: false, + }, + } + + for tn, tc := range cases { + t.Run(tn, func(t *testing.T) { + _, errors := URLIsHTTPS(tc.Value, tn) + + if len(errors) > 0 && !tc.Error { + t.Errorf("URLIsHTTPS(%s) produced an unexpected error", tc.Value) + } else if len(errors) == 0 && tc.Error { + t.Errorf("URLIsHTTPS(%s) did not error", tc.Value) + } + }) + } +} + +func TestURLIsHTTPOrHTTPS(t *testing.T) { + cases := map[string]struct { + Value interface{} + Error bool + }{ + "NotString": { + Value: 7, + Error: true, + }, + "Empty": { + Value: "", + Error: true, + }, + "NotUrl": { + Value: "this is not a url", + Error: true, + }, + "BareUrl": { + Value: "www.example.com", + Error: true, + }, + "FtpUrl": { + Value: "ftp://www.example.com", + Error: true, + }, + "HttpUrl": { + Value: "http://www.example.com", + Error: false, + }, + "HttpsUrl": { + Value: "https://www.example.com", + Error: false, + }, + } + + for tn, tc := range cases { + t.Run(tn, func(t *testing.T) { + _, errors := URLIsHTTPOrHTTPS(tc.Value, tn) + + if len(errors) > 0 && !tc.Error { + t.Errorf("URLIsHTTPOrHTTPS(%s) produced an unexpected error", tc.Value) + } else if len(errors) == 0 && tc.Error { + t.Errorf("URLIsHTTPOrHTTPS(%s) did not error", tc.Value) + } + }) + } +} From af70e5e316ba8448255f5c133427b020ed91623a Mon Sep 17 00:00:00 2001 From: kt Date: Tue, 14 Jan 2020 22:58:18 -0800 Subject: [PATCH 05/14] Fix errant find replace --- helper/validation/network.go | 6 +++--- helper/validation/network_test.go | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/helper/validation/network.go b/helper/validation/network.go index b9140af7ef6..1f20f39a3ca 100644 --- a/helper/validation/network.go +++ b/helper/validation/network.go @@ -21,7 +21,7 @@ func SingleIP() schema.SchemaValidateFunc { ip := net.ParseIP(v) if ip == nil { - es = append(es, fmt.Errorf("expected %s to contain a valid Value, got: %s", k, v)) + es = append(es, fmt.Errorf("expected %s to contain a valid IP, got: %s", k, v)) } return } @@ -70,14 +70,14 @@ func IPRange() schema.SchemaValidateFunc { ips := strings.Split(v, "-") if len(ips) != 2 { es = append(es, fmt.Errorf( - "expected %s to contain a valid Value range, got: %s", k, v)) + "expected %s to contain a valid IP range, got: %s", k, v)) return } ip1 := net.ParseIP(ips[0]) ip2 := net.ParseIP(ips[1]) if ip1 == nil || ip2 == nil || bytes.Compare(ip1, ip2) > 0 { es = append(es, fmt.Errorf( - "expected %s to contain a valid Value range, got: %s", k, v)) + "expected %s to contain a valid IP range, got: %s", k, v)) } return } diff --git a/helper/validation/network_test.go b/helper/validation/network_test.go index 462b7d7f85c..f2af1d9fdfc 100644 --- a/helper/validation/network_test.go +++ b/helper/validation/network_test.go @@ -170,17 +170,17 @@ func TestValidationSingleIP(t *testing.T) { { val: "1.1.1", f: SingleIP(), - expectedErr: regexp.MustCompile(regexp.QuoteMeta("expected test_property to contain a valid Value, got:")), + expectedErr: regexp.MustCompile(regexp.QuoteMeta("expected test_property to contain a valid IP, got:")), }, { val: "1.1.1.0/20", f: SingleIP(), - expectedErr: regexp.MustCompile(regexp.QuoteMeta("expected test_property to contain a valid Value, got:")), + expectedErr: regexp.MustCompile(regexp.QuoteMeta("expected test_property to contain a valid IP, got:")), }, { val: "256.1.1.1", f: SingleIP(), - expectedErr: regexp.MustCompile(regexp.QuoteMeta("expected test_property to contain a valid Value, got:")), + expectedErr: regexp.MustCompile(regexp.QuoteMeta("expected test_property to contain a valid IP, got:")), }, }) } @@ -194,12 +194,12 @@ func TestValidationIPRange(t *testing.T) { { val: "172.10.10.20", f: IPRange(), - expectedErr: regexp.MustCompile(regexp.QuoteMeta("expected test_property to contain a valid Value range, got:")), + expectedErr: regexp.MustCompile(regexp.QuoteMeta("expected test_property to contain a valid IP range, got:")), }, { val: "172.10.10.20-172.10.10.12", f: IPRange(), - expectedErr: regexp.MustCompile(regexp.QuoteMeta("expected test_property to contain a valid Value range, got:")), + expectedErr: regexp.MustCompile(regexp.QuoteMeta("expected test_property to contain a valid IP range, got:")), }, }) } From 2504e757e2bbb469e4e8827791ef75755cebb12a Mon Sep 17 00:00:00 2001 From: kt Date: Wed, 15 Jan 2020 05:16:48 -0800 Subject: [PATCH 06/14] Added Is to function names --- helper/validation/network.go | 12 +++++----- helper/validation/network_test.go | 40 +++++++++++++++---------------- helper/validation/web.go | 6 ++--- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/helper/validation/network.go b/helper/validation/network.go index 1f20f39a3ca..22606370431 100644 --- a/helper/validation/network.go +++ b/helper/validation/network.go @@ -27,7 +27,7 @@ func SingleIP() schema.SchemaValidateFunc { } } -func IPv6Address(i interface{}, k string) (warnings []string, errors []error) { +func IsIPv6Address(i interface{}, k string) (warnings []string, errors []error) { v, ok := i.(string) if !ok { errors = append(errors, fmt.Errorf("expected type of %q to be string", k)) @@ -42,7 +42,7 @@ func IPv6Address(i interface{}, k string) (warnings []string, errors []error) { return warnings, errors } -func IPv4Address(i interface{}, k string) (warnings []string, errors []error) { +func IsIPv4Address(i interface{}, k string) (warnings []string, errors []error) { v, ok := i.(string) if !ok { errors = append(errors, fmt.Errorf("expected type of %q to be string", k)) @@ -83,7 +83,7 @@ func IPRange() schema.SchemaValidateFunc { } } -func CIDR(i interface{}, k string) (warnings []string, errors []error) { +func IsCIDR(i interface{}, k string) (warnings []string, errors []error) { v, ok := i.(string) if !ok { errors = append(errors, fmt.Errorf("expected type of %s to be string", k)) @@ -132,7 +132,7 @@ func CIDRNetwork(min, max int) schema.SchemaValidateFunc { } } -func MACAddress(i interface{}, k string) (warnings []string, errors []error) { +func IsMACAddress(i interface{}, k string) (warnings []string, errors []error) { v, ok := i.(string) if !ok { errors = append(errors, fmt.Errorf("expected type of %q to be string", k)) @@ -146,7 +146,7 @@ func MACAddress(i interface{}, k string) (warnings []string, errors []error) { return warnings, errors } -func PortNumber(i interface{}, k string) (warnings []string, errors []error) { +func IsPortNumber(i interface{}, k string) (warnings []string, errors []error) { v, ok := i.(int) if !ok { errors = append(errors, fmt.Errorf("expected type of %q to be int", k)) @@ -160,7 +160,7 @@ func PortNumber(i interface{}, k string) (warnings []string, errors []error) { return warnings, errors } -func PortNumberOrZero(i interface{}, k string) (warnings []string, errors []error) { +func IsPortNumberOrZero(i interface{}, k string) (warnings []string, errors []error) { v, ok := i.(int) if !ok { errors = append(errors, fmt.Errorf("expected type of %q to be int", k)) diff --git a/helper/validation/network_test.go b/helper/validation/network_test.go index f2af1d9fdfc..edb7250be50 100644 --- a/helper/validation/network_test.go +++ b/helper/validation/network_test.go @@ -38,18 +38,18 @@ func TestValidateCIDR(t *testing.T) { for tn, tc := range cases { t.Run(tn, func(t *testing.T) { - _, errors := CIDR(tc.Value, tn) + _, errors := IsCIDR(tc.Value, tn) if len(errors) > 0 && !tc.Error { - t.Errorf("CIDR(%s) produced an unexpected error", tc.Value) + t.Errorf("IsCIDR(%s) produced an unexpected error", tc.Value) } else if len(errors) == 0 && tc.Error { - t.Errorf("CIDR(%s) did not error", tc.Value) + t.Errorf("IsCIDR(%s) did not error", tc.Value) } }) } } -func TestValidateIPv6Address(t *testing.T) { +func TestValidateIsIPv6Address(t *testing.T) { cases := map[string]struct { Value string Error bool @@ -94,18 +94,18 @@ func TestValidateIPv6Address(t *testing.T) { for tn, tc := range cases { t.Run(tn, func(t *testing.T) { - _, errors := IPv6Address(tc.Value, tn) + _, errors := IsIPv6Address(tc.Value, tn) if len(errors) > 0 && !tc.Error { - t.Errorf("IPv6Address(%s) produced an unexpected error", tc.Value) + t.Errorf("IsIPv6Address(%s) produced an unexpected error", tc.Value) } else if len(errors) == 0 && tc.Error { - t.Errorf("IPv6Address(%s) did not error", tc.Value) + t.Errorf("IsIPv6Address(%s) did not error", tc.Value) } }) } } -func TestValidateIPv4Address(t *testing.T) { +func TestValidateIsIPv4Address(t *testing.T) { cases := map[string]struct { Value string Error bool @@ -150,12 +150,12 @@ func TestValidateIPv4Address(t *testing.T) { for tn, tc := range cases { t.Run(tn, func(t *testing.T) { - _, errors := IPv4Address(tc.Value, tn) + _, errors := IsIPv4Address(tc.Value, tn) if len(errors) > 0 && !tc.Error { - t.Errorf("IPv4Address(%s) produced an unexpected error", tc.Value) + t.Errorf("IsIPv4Address(%s) produced an unexpected error", tc.Value) } else if len(errors) == 0 && tc.Error { - t.Errorf("IPv4Address(%s) did not error", tc.Value) + t.Errorf("IsIPv4Address(%s) did not error", tc.Value) } }) } @@ -245,12 +245,12 @@ func TestValidationMACAddress(t *testing.T) { for tn, tc := range cases { t.Run(tn, func(t *testing.T) { - _, errors := MACAddress(tc.Value, tn) + _, errors := IsMACAddress(tc.Value, tn) if len(errors) > 0 && !tc.Error { - t.Errorf("MACAddress(%s) produced an unexpected error", tc.Value) + t.Errorf("IsMACAddress(%s) produced an unexpected error", tc.Value) } else if len(errors) == 0 && tc.Error { - t.Errorf("MACAddress(%s) did not error", tc.Value) + t.Errorf("IsMACAddress(%s) did not error", tc.Value) } }) } @@ -297,12 +297,12 @@ func TestValidationPortNumber(t *testing.T) { for tn, tc := range cases { t.Run(tn, func(t *testing.T) { - _, errors := PortNumber(tc.Value, tn) + _, errors := IsPortNumber(tc.Value, tn) if len(errors) > 0 && !tc.Error { - t.Errorf("PortNumber(%s) produced an unexpected error", tc.Value) + t.Errorf("IsPortNumber(%s) produced an unexpected error", tc.Value) } else if len(errors) == 0 && tc.Error { - t.Errorf("PortNumber(%s) did not error", tc.Value) + t.Errorf("IsPortNumber(%s) did not error", tc.Value) } }) } @@ -349,12 +349,12 @@ func TestValidationPortNumberOrZero(t *testing.T) { for tn, tc := range cases { t.Run(tn, func(t *testing.T) { - _, errors := PortNumberOrZero(tc.Value, tn) + _, errors := IsPortNumberOrZero(tc.Value, tn) if len(errors) > 0 && !tc.Error { - t.Errorf("PortNumberOrZero(%s) produced an unexpected error", tc.Value) + t.Errorf("IsPortNumberOrZero(%s) produced an unexpected error", tc.Value) } else if len(errors) == 0 && tc.Error { - t.Errorf("PortNumberOrZero(%s) did not error", tc.Value) + t.Errorf("IsPortNumberOrZero(%s) did not error", tc.Value) } }) } diff --git a/helper/validation/web.go b/helper/validation/web.go index d17eb0e78ef..8853688aee5 100644 --- a/helper/validation/web.go +++ b/helper/validation/web.go @@ -9,14 +9,14 @@ import ( ) func URLIsHTTPS(i interface{}, k string) (_ []string, errors []error) { - return URLWithScheme([]string{"https"})(i, k) + return IsURLWithScheme([]string{"https"})(i, k) } func URLIsHTTPOrHTTPS(i interface{}, k string) (_ []string, errors []error) { - return URLWithScheme([]string{"http", "https"})(i, k) + return IsURLWithScheme([]string{"http", "https"})(i, k) } -func URLWithScheme(validSchemes []string) schema.SchemaValidateFunc { +func IsURLWithScheme(validSchemes []string) schema.SchemaValidateFunc { return func(i interface{}, k string) (_ []string, errors []error) { v, ok := i.(string) if !ok { From 43d0e8f1a9f966cada9c9f9ef73cedd38f003495 Mon Sep 17 00:00:00 2001 From: kt Date: Wed, 15 Jan 2020 06:11:39 -0800 Subject: [PATCH 07/14] Removed String from network and time functions --- helper/validation/time.go | 10 +++++----- helper/validation/time_test.go | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/helper/validation/time.go b/helper/validation/time.go index d2d4d3c0510..b7bf5f3eabc 100644 --- a/helper/validation/time.go +++ b/helper/validation/time.go @@ -7,7 +7,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/helper/schema" ) -func StringIsDayOfTheWeek(ignoreCase bool) schema.SchemaValidateFunc { +func IsDayOfTheWeek(ignoreCase bool) schema.SchemaValidateFunc { return StringInSlice([]string{ "Monday", "Tuesday", @@ -19,7 +19,7 @@ func StringIsDayOfTheWeek(ignoreCase bool) schema.SchemaValidateFunc { }, ignoreCase) } -func StringIsMonth(ignoreCase bool) schema.SchemaValidateFunc { +func IsMonth(ignoreCase bool) schema.SchemaValidateFunc { return StringInSlice([]string{ "January", "February", @@ -37,7 +37,7 @@ func StringIsMonth(ignoreCase bool) schema.SchemaValidateFunc { } // ValidateRFC3339TimeString is a ValidateFunc that ensures a string parses as time.RFC3339 format -func StringIsRFC3339Time(i interface{}, k string) (warnings []string, errors []error) { +func IsRFC3339Time(i interface{}, k string) (warnings []string, errors []error) { v, ok := i.(string) if !ok { errors = append(errors, fmt.Errorf("expected type of %q to be string", k)) @@ -53,7 +53,7 @@ func StringIsRFC3339Time(i interface{}, k string) (warnings []string, errors []e // ValidateRFC3339TimeString is a ValidateFunc that ensures a string parses as time.RFC3339 format // -// Deprecated: use StringIsRFC3339Time() instead +// Deprecated: use IsRFC3339Time() instead func ValidateRFC3339TimeString(v interface{}, k string) (ws []string, errors []error) { - return StringIsRFC3339Time(v, k) + return IsRFC3339Time(v, k) } diff --git a/helper/validation/time_test.go b/helper/validation/time_test.go index eb2531afb5b..ab0159f678a 100644 --- a/helper/validation/time_test.go +++ b/helper/validation/time_test.go @@ -57,12 +57,12 @@ func TestValidationStringIsRFC3339Time(t *testing.T) { for tn, tc := range cases { t.Run(tn, func(t *testing.T) { - _, errors := StringIsRFC3339Time(tc.Value, tn) + _, errors := IsRFC3339Time(tc.Value, tn) if len(errors) > 0 && !tc.Error { - t.Errorf("StringIsRFC3339Time(%s) produced an unexpected error", tc.Value) + t.Errorf("IsRFC3339Time(%s) produced an unexpected error", tc.Value) } else if len(errors) == 0 && tc.Error { - t.Errorf("StringIsRFC3339Time(%s) did not error", tc.Value) + t.Errorf("IsRFC3339Time(%s) did not error", tc.Value) } }) } From d00d319072dfd8fb68861954981c34cd68e0f34d Mon Sep 17 00:00:00 2001 From: kt Date: Thu, 16 Jan 2020 08:44:10 -0800 Subject: [PATCH 08/14] Address PR comments --- helper/validation/network.go | 10 +++++++--- helper/validation/time.go | 4 +++- helper/validation/web.go | 7 +++++-- helper/validation/web_test.go | 12 ++++++------ 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/helper/validation/network.go b/helper/validation/network.go index 22606370431..a26d8caacf5 100644 --- a/helper/validation/network.go +++ b/helper/validation/network.go @@ -26,7 +26,7 @@ func SingleIP() schema.SchemaValidateFunc { return } } - +// IsIPv6Address is a SchemaValidateFunc which tests if the provided value is of type string and a valid IPv6 address func IsIPv6Address(i interface{}, k string) (warnings []string, errors []error) { v, ok := i.(string) if !ok { @@ -42,6 +42,7 @@ func IsIPv6Address(i interface{}, k string) (warnings []string, errors []error) return warnings, errors } +// IsIPv4Address is a SchemaValidateFunc which tests if the provided value is of type string and a valid IPv4 address func IsIPv4Address(i interface{}, k string) (warnings []string, errors []error) { v, ok := i.(string) if !ok { @@ -57,8 +58,7 @@ func IsIPv4Address(i interface{}, k string) (warnings []string, errors []error) return warnings, errors } -// IPRange returns a SchemaValidateFunc which tests if the provided value -// is of type string, and in valid Value range notation +// IPRange returns a SchemaValidateFunc which tests if the provided value is of type string, and in valid IP range func IPRange() schema.SchemaValidateFunc { return func(i interface{}, k string) (s []string, es []error) { v, ok := i.(string) @@ -83,6 +83,7 @@ func IPRange() schema.SchemaValidateFunc { } } +// IsCIDR is a SchemaValidateFunc which tests if the provided value is of type string and a valid CIDR func IsCIDR(i interface{}, k string) (warnings []string, errors []error) { v, ok := i.(string) if !ok { @@ -132,6 +133,7 @@ func CIDRNetwork(min, max int) schema.SchemaValidateFunc { } } +// IsMACAddress is a SchemaValidateFunc which tests if the provided value is of type string and a valid MAC address func IsMACAddress(i interface{}, k string) (warnings []string, errors []error) { v, ok := i.(string) if !ok { @@ -146,6 +148,7 @@ func IsMACAddress(i interface{}, k string) (warnings []string, errors []error) { return warnings, errors } +// IsPortNumber is a SchemaValidateFunc which tests if the provided value is of type string and a valid TCP Port Number func IsPortNumber(i interface{}, k string) (warnings []string, errors []error) { v, ok := i.(int) if !ok { @@ -160,6 +163,7 @@ func IsPortNumber(i interface{}, k string) (warnings []string, errors []error) { return warnings, errors } +// IsPortNumberOrZero is a SchemaValidateFunc which tests if the provided value is of type string and a valid TCP Port Number or zero func IsPortNumberOrZero(i interface{}, k string) (warnings []string, errors []error) { v, ok := i.(int) if !ok { diff --git a/helper/validation/time.go b/helper/validation/time.go index b7bf5f3eabc..2a27ae0ebd7 100644 --- a/helper/validation/time.go +++ b/helper/validation/time.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/helper/schema" ) +// IsDayOfTheWeek id a SchemaValidateFunc which tests if the provided value is of type string and a valid english day of the week func IsDayOfTheWeek(ignoreCase bool) schema.SchemaValidateFunc { return StringInSlice([]string{ "Monday", @@ -19,6 +20,7 @@ func IsDayOfTheWeek(ignoreCase bool) schema.SchemaValidateFunc { }, ignoreCase) } +// IsMonth id a SchemaValidateFunc which tests if the provided value is of type string and a valid english month func IsMonth(ignoreCase bool) schema.SchemaValidateFunc { return StringInSlice([]string{ "January", @@ -36,7 +38,7 @@ func IsMonth(ignoreCase bool) schema.SchemaValidateFunc { }, ignoreCase) } -// ValidateRFC3339TimeString is a ValidateFunc that ensures a string parses as time.RFC3339 format +// IsRFC3339Time is a SchemaValidateFunc which tests if the provided value is of type string and a valid RFC33349Time func IsRFC3339Time(i interface{}, k string) (warnings []string, errors []error) { v, ok := i.(string) if !ok { diff --git a/helper/validation/web.go b/helper/validation/web.go index 8853688aee5..8f0d05ce40c 100644 --- a/helper/validation/web.go +++ b/helper/validation/web.go @@ -8,14 +8,17 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/helper/schema" ) -func URLIsHTTPS(i interface{}, k string) (_ []string, errors []error) { +// IsURLWithHTTPS is a SchemaValidateFunc which tests if the provided value is of type string and a valid IPv6 address +func IsURLWithHTTPS(i interface{}, k string) (_ []string, errors []error) { return IsURLWithScheme([]string{"https"})(i, k) } -func URLIsHTTPOrHTTPS(i interface{}, k string) (_ []string, errors []error) { +// IsURLWithHTTPorHTTPS is a SchemaValidateFunc which tests if the provided value is of type string and a valid IPv6 address +func IsURLWithHTTPorHTTPS(i interface{}, k string) (_ []string, errors []error) { return IsURLWithScheme([]string{"http", "https"})(i, k) } +// IsURLWithScheme is a SchemaValidateFunc which tests if the provided value is of type string and a valid IPv6 address func IsURLWithScheme(validSchemes []string) schema.SchemaValidateFunc { return func(i interface{}, k string) (_ []string, errors []error) { v, ok := i.(string) diff --git a/helper/validation/web_test.go b/helper/validation/web_test.go index b8484e6b4a4..403817d23e9 100644 --- a/helper/validation/web_test.go +++ b/helper/validation/web_test.go @@ -41,12 +41,12 @@ func TestURLIsHTTPS(t *testing.T) { for tn, tc := range cases { t.Run(tn, func(t *testing.T) { - _, errors := URLIsHTTPS(tc.Value, tn) + _, errors := IsURLWithHTTPS(tc.Value, tn) if len(errors) > 0 && !tc.Error { - t.Errorf("URLIsHTTPS(%s) produced an unexpected error", tc.Value) + t.Errorf("IsURLWithHTTPS(%s) produced an unexpected error", tc.Value) } else if len(errors) == 0 && tc.Error { - t.Errorf("URLIsHTTPS(%s) did not error", tc.Value) + t.Errorf("IsURLWithHTTPS(%s) did not error", tc.Value) } }) } @@ -89,12 +89,12 @@ func TestURLIsHTTPOrHTTPS(t *testing.T) { for tn, tc := range cases { t.Run(tn, func(t *testing.T) { - _, errors := URLIsHTTPOrHTTPS(tc.Value, tn) + _, errors := IsURLWithHTTPorHTTPS(tc.Value, tn) if len(errors) > 0 && !tc.Error { - t.Errorf("URLIsHTTPOrHTTPS(%s) produced an unexpected error", tc.Value) + t.Errorf("IsURLWithHTTPorHTTPS(%s) produced an unexpected error", tc.Value) } else if len(errors) == 0 && tc.Error { - t.Errorf("URLIsHTTPOrHTTPS(%s) did not error", tc.Value) + t.Errorf("IsURLWithHTTPorHTTPS(%s) did not error", tc.Value) } }) } From c4e0f07bf86d51fc138d9a9dcad70f8c9e2fa2ea Mon Sep 17 00:00:00 2001 From: kt Date: Thu, 16 Jan 2020 08:47:59 -0800 Subject: [PATCH 09/14] make fmt --- helper/validation/network.go | 1 + 1 file changed, 1 insertion(+) diff --git a/helper/validation/network.go b/helper/validation/network.go index a26d8caacf5..e5e092a9e17 100644 --- a/helper/validation/network.go +++ b/helper/validation/network.go @@ -26,6 +26,7 @@ func SingleIP() schema.SchemaValidateFunc { return } } + // IsIPv6Address is a SchemaValidateFunc which tests if the provided value is of type string and a valid IPv6 address func IsIPv6Address(i interface{}, k string) (warnings []string, errors []error) { v, ok := i.(string) From 03bfef2c90067698d0c320a3addb465a22d9f811 Mon Sep 17 00:00:00 2001 From: kt Date: Thu, 16 Jan 2020 08:56:34 -0800 Subject: [PATCH 10/14] Further PR comments --- helper/validation/int.go | 2 +- helper/validation/network.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/helper/validation/int.go b/helper/validation/int.go index 47014cbd123..63dd4911636 100644 --- a/helper/validation/int.go +++ b/helper/validation/int.go @@ -104,7 +104,7 @@ func IntInSlice(valid []int) schema.SchemaValidateFunc { } } -// IntInSlice returns a SchemaValidateFunc which tests if the provided value +// IntNotInSlice returns a SchemaValidateFunc which tests if the provided value // is of type int and matches the value of an element in the valid slice func IntNotInSlice(valid []int) schema.SchemaValidateFunc { return func(i interface{}, k string) (_ []string, errors []error) { diff --git a/helper/validation/network.go b/helper/validation/network.go index e5e092a9e17..4edc118e859 100644 --- a/helper/validation/network.go +++ b/helper/validation/network.go @@ -157,7 +157,7 @@ func IsPortNumber(i interface{}, k string) (warnings []string, errors []error) { return } - if v < 1 || 65535 < v { + if 1 < v || v > 65335 { errors = append(errors, fmt.Errorf("expected %q to be a valid port number, got: %v", k, v)) } @@ -172,7 +172,7 @@ func IsPortNumberOrZero(i interface{}, k string) (warnings []string, errors []er return } - if v < 0 || 65535 < v { + if 0 < v || v > 65335 { errors = append(errors, fmt.Errorf("expected %q to be a valid port number or 0, got: %v", k, v)) } From f86cf8d635d073025506de841759672279be477f Mon Sep 17 00:00:00 2001 From: kt Date: Thu, 16 Jan 2020 09:01:20 -0800 Subject: [PATCH 11/14] adjust max port value --- helper/validation/network.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helper/validation/network.go b/helper/validation/network.go index 4edc118e859..9b7479d93ee 100644 --- a/helper/validation/network.go +++ b/helper/validation/network.go @@ -157,7 +157,7 @@ func IsPortNumber(i interface{}, k string) (warnings []string, errors []error) { return } - if 1 < v || v > 65335 { + if 1 > v || v > 65535 { errors = append(errors, fmt.Errorf("expected %q to be a valid port number, got: %v", k, v)) } @@ -172,7 +172,7 @@ func IsPortNumberOrZero(i interface{}, k string) (warnings []string, errors []er return } - if 0 < v || v > 65335 { + if 0 > v || v > 65535 { errors = append(errors, fmt.Errorf("expected %q to be a valid port number or 0, got: %v", k, v)) } From e481cb941dbe7c2acd1a40106901f52e48558f7e Mon Sep 17 00:00:00 2001 From: kt Date: Thu, 16 Jan 2020 09:13:46 -0800 Subject: [PATCH 12/14] rename tests --- helper/validation/network_test.go | 8 ++++---- helper/validation/time_test.go | 2 +- helper/validation/web_test.go | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/helper/validation/network_test.go b/helper/validation/network_test.go index edb7250be50..7725477fdc5 100644 --- a/helper/validation/network_test.go +++ b/helper/validation/network_test.go @@ -5,7 +5,7 @@ import ( "testing" ) -func TestValidateCIDR(t *testing.T) { +func TestValidateIsCIDR(t *testing.T) { cases := map[string]struct { Value string Error bool @@ -204,7 +204,7 @@ func TestValidationIPRange(t *testing.T) { }) } -func TestValidationMACAddress(t *testing.T) { +func TestValidationIsMACAddress(t *testing.T) { cases := map[string]struct { Value interface{} Error bool @@ -256,7 +256,7 @@ func TestValidationMACAddress(t *testing.T) { } } -func TestValidationPortNumber(t *testing.T) { +func TestValidationIsPortNumber(t *testing.T) { cases := map[string]struct { Value interface{} Error bool @@ -308,7 +308,7 @@ func TestValidationPortNumber(t *testing.T) { } } -func TestValidationPortNumberOrZero(t *testing.T) { +func TestValidationIsPortNumberOrZero(t *testing.T) { cases := map[string]struct { Value interface{} Error bool diff --git a/helper/validation/time_test.go b/helper/validation/time_test.go index ab0159f678a..e453ee4cb5f 100644 --- a/helper/validation/time_test.go +++ b/helper/validation/time_test.go @@ -4,7 +4,7 @@ import ( "testing" ) -func TestValidationStringIsRFC3339Time(t *testing.T) { +func TestValidationIsRFC3339Time(t *testing.T) { cases := map[string]struct { Value interface{} Error bool diff --git a/helper/validation/web_test.go b/helper/validation/web_test.go index 403817d23e9..e729e4e953b 100644 --- a/helper/validation/web_test.go +++ b/helper/validation/web_test.go @@ -4,7 +4,7 @@ import ( "testing" ) -func TestURLIsHTTPS(t *testing.T) { +func TestValidationIsURLWithHTTPS(t *testing.T) { cases := map[string]struct { Value interface{} Error bool @@ -52,7 +52,7 @@ func TestURLIsHTTPS(t *testing.T) { } } -func TestURLIsHTTPOrHTTPS(t *testing.T) { +func TestValidationIsURLWithHTTPorHTTPS(t *testing.T) { cases := map[string]struct { Value interface{} Error bool From 4770c6a53cb53774069a35d77e7ea56e34ab2d15 Mon Sep 17 00:00:00 2001 From: kt Date: Thu, 16 Jan 2020 09:24:52 -0800 Subject: [PATCH 13/14] Update web.go --- helper/validation/web.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/helper/validation/web.go b/helper/validation/web.go index 8f0d05ce40c..ab5e98dfece 100644 --- a/helper/validation/web.go +++ b/helper/validation/web.go @@ -8,17 +8,17 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/helper/schema" ) -// IsURLWithHTTPS is a SchemaValidateFunc which tests if the provided value is of type string and a valid IPv6 address +// IsURLWithHTTPS is a SchemaValidateFunc which tests if the provided value is of type string and a valid HTTPS URL address func IsURLWithHTTPS(i interface{}, k string) (_ []string, errors []error) { return IsURLWithScheme([]string{"https"})(i, k) } -// IsURLWithHTTPorHTTPS is a SchemaValidateFunc which tests if the provided value is of type string and a valid IPv6 address +// IsURLWithHTTPorHTTPS is a SchemaValidateFunc which tests if the provided value is of type string and a valid HTTP or HTTPS URL address func IsURLWithHTTPorHTTPS(i interface{}, k string) (_ []string, errors []error) { return IsURLWithScheme([]string{"http", "https"})(i, k) } -// IsURLWithScheme is a SchemaValidateFunc which tests if the provided value is of type string and a valid IPv6 address +// IsURLWithScheme is a SchemaValidateFunc which tests if the provided value is of type string and a valid URL with the provided schemas address func IsURLWithScheme(validSchemes []string) schema.SchemaValidateFunc { return func(i interface{}, k string) (_ []string, errors []error) { v, ok := i.(string) From cc4d279af34b28793b40a434f4fdc073e5d12f66 Mon Sep 17 00:00:00 2001 From: kt Date: Thu, 16 Jan 2020 09:27:09 -0800 Subject: [PATCH 14/14] Update web.go --- helper/validation/web.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/helper/validation/web.go b/helper/validation/web.go index ab5e98dfece..b40606fa5a0 100644 --- a/helper/validation/web.go +++ b/helper/validation/web.go @@ -8,17 +8,17 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/helper/schema" ) -// IsURLWithHTTPS is a SchemaValidateFunc which tests if the provided value is of type string and a valid HTTPS URL address +// IsURLWithHTTPS is a SchemaValidateFunc which tests if the provided value is of type string and a valid HTTPS URL func IsURLWithHTTPS(i interface{}, k string) (_ []string, errors []error) { return IsURLWithScheme([]string{"https"})(i, k) } -// IsURLWithHTTPorHTTPS is a SchemaValidateFunc which tests if the provided value is of type string and a valid HTTP or HTTPS URL address +// IsURLWithHTTPorHTTPS is a SchemaValidateFunc which tests if the provided value is of type string and a valid HTTP or HTTPS URL func IsURLWithHTTPorHTTPS(i interface{}, k string) (_ []string, errors []error) { return IsURLWithScheme([]string{"http", "https"})(i, k) } -// IsURLWithScheme is a SchemaValidateFunc which tests if the provided value is of type string and a valid URL with the provided schemas address +// IsURLWithScheme is a SchemaValidateFunc which tests if the provided value is of type string and a valid URL with the provided schemas func IsURLWithScheme(validSchemes []string) schema.SchemaValidateFunc { return func(i interface{}, k string) (_ []string, errors []error) { v, ok := i.(string)