diff --git a/helper/validation/int.go b/helper/validation/int.go index 54b082feb1..63dd491163 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 } } + +// 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) { + 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 b3358b796e..5fcf93a165 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 a53725a299..9b7479d93e 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,48 @@ 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 IP, 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)) - } +// 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 { + 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 +} +// 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 { + 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 IP range +func IPRange() schema.SchemaValidateFunc { return func(i interface{}, k string) (s []string, es []error) { v, ok := i.(string) if !ok { @@ -53,18 +68,41 @@ 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 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, got: %s", k, v)) + "expected %s to contain a valid IP 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 { +// 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 { + 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 +110,71 @@ 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 IP range, got: %s", k, v)) + "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 %q to contain a network Value with between %d and %d significant bits, got: %d", + k, min, max, sigbits)) + } + + return + } +} + +// 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 { + 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 +} + +// 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 { + errors = append(errors, fmt.Errorf("expected type of %q to be int", k)) + return + } + + if 1 > v || v > 65535 { + errors = append(errors, fmt.Errorf("expected %q to be a valid port number, got: %v", k, v)) + } + + 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 { + errors = append(errors, fmt.Errorf("expected type of %q to be int", k)) + return + } + + if 0 > v || v > 65535 { + 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 25769404c3..7725477fdc 100644 --- a/helper/validation/network_test.go +++ b/helper/validation/network_test.go @@ -5,6 +5,162 @@ import ( "testing" ) +func TestValidateIsCIDR(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 := IsCIDR(tc.Value, tn) + + if len(errors) > 0 && !tc.Error { + t.Errorf("IsCIDR(%s) produced an unexpected error", tc.Value) + } else if len(errors) == 0 && tc.Error { + t.Errorf("IsCIDR(%s) did not error", tc.Value) + } + }) + } +} + +func TestValidateIsIPv6Address(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 := IsIPv6Address(tc.Value, tn) + + if len(errors) > 0 && !tc.Error { + t.Errorf("IsIPv6Address(%s) produced an unexpected error", tc.Value) + } else if len(errors) == 0 && tc.Error { + t.Errorf("IsIPv6Address(%s) did not error", tc.Value) + } + }) + } +} + +func TestValidateIsIPv4Address(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 := IsIPv4Address(tc.Value, tn) + + if len(errors) > 0 && !tc.Error { + t.Errorf("IsIPv4Address(%s) produced an unexpected error", tc.Value) + } else if len(errors) == 0 && tc.Error { + t.Errorf("IsIPv4Address(%s) did not error", tc.Value) + } + }) + } +} + func TestValidationSingleIP(t *testing.T) { runTestCases(t, []testCase{ { @@ -47,3 +203,159 @@ func TestValidationIPRange(t *testing.T) { }, }) } + +func TestValidationIsMACAddress(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 := IsMACAddress(tc.Value, tn) + + if len(errors) > 0 && !tc.Error { + t.Errorf("IsMACAddress(%s) produced an unexpected error", tc.Value) + } else if len(errors) == 0 && tc.Error { + t.Errorf("IsMACAddress(%s) did not error", tc.Value) + } + }) + } +} + +func TestValidationIsPortNumber(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 := IsPortNumber(tc.Value, tn) + + if len(errors) > 0 && !tc.Error { + t.Errorf("IsPortNumber(%s) produced an unexpected error", tc.Value) + } else if len(errors) == 0 && tc.Error { + t.Errorf("IsPortNumber(%s) did not error", tc.Value) + } + }) + } +} + +func TestValidationIsPortNumberOrZero(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 := IsPortNumberOrZero(tc.Value, tn) + + if len(errors) > 0 && !tc.Error { + t.Errorf("IsPortNumberOrZero(%s) produced an unexpected error", tc.Value) + } else if len(errors) == 0 && tc.Error { + t.Errorf("IsPortNumberOrZero(%s) did not error", tc.Value) + } + }) + } +} diff --git a/helper/validation/time.go b/helper/validation/time.go index 30010ceb91..2a27ae0ebd 100644 --- a/helper/validation/time.go +++ b/helper/validation/time.go @@ -3,13 +3,59 @@ 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)) +// 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", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday", + }, 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", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + }, ignoreCase) +} + +// 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 { + 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 IsRFC3339Time() instead +func ValidateRFC3339TimeString(v interface{}, k string) (ws []string, errors []error) { + return IsRFC3339Time(v, k) } diff --git a/helper/validation/time_test.go b/helper/validation/time_test.go index ac641783e7..e453ee4cb5 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 TestValidationIsRFC3339Time(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 := IsRFC3339Time(tc.Value, tn) + + if len(errors) > 0 && !tc.Error { + t.Errorf("IsRFC3339Time(%s) produced an unexpected error", tc.Value) + } else if len(errors) == 0 && tc.Error { + t.Errorf("IsRFC3339Time(%s) did not error", tc.Value) + } + }) + } } diff --git a/helper/validation/web.go b/helper/validation/web.go new file mode 100644 index 0000000000..b40606fa5a --- /dev/null +++ b/helper/validation/web.go @@ -0,0 +1,55 @@ +package validation + +import ( + "fmt" + "net/url" + "strings" + + "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 +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 +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 +func IsURLWithScheme(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 0000000000..e729e4e953 --- /dev/null +++ b/helper/validation/web_test.go @@ -0,0 +1,101 @@ +package validation + +import ( + "testing" +) + +func TestValidationIsURLWithHTTPS(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 := IsURLWithHTTPS(tc.Value, tn) + + if len(errors) > 0 && !tc.Error { + t.Errorf("IsURLWithHTTPS(%s) produced an unexpected error", tc.Value) + } else if len(errors) == 0 && tc.Error { + t.Errorf("IsURLWithHTTPS(%s) did not error", tc.Value) + } + }) + } +} + +func TestValidationIsURLWithHTTPorHTTPS(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 := IsURLWithHTTPorHTTPS(tc.Value, tn) + + if len(errors) > 0 && !tc.Error { + t.Errorf("IsURLWithHTTPorHTTPS(%s) produced an unexpected error", tc.Value) + } else if len(errors) == 0 && tc.Error { + t.Errorf("IsURLWithHTTPorHTTPS(%s) did not error", tc.Value) + } + }) + } +}