Skip to content

Commit 7b1d8e6

Browse files
committed
Merge remote-tracking branch 'origin/main' into jjs/364
2 parents eff062b + 4e7da25 commit 7b1d8e6

File tree

2 files changed

+314
-0
lines changed

2 files changed

+314
-0
lines changed

provider/parameter_test.go

+235
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package provider_test
22

33
import (
44
"fmt"
5+
"os"
56
"regexp"
7+
"strconv"
68
"strings"
79
"testing"
810

@@ -686,6 +688,220 @@ data "coder_parameter" "region" {
686688
}
687689
}
688690

691+
// TestParameterValidationEnforcement tests various parameter states and the
692+
// validation enforcement that should be applied to them. The table is described
693+
// by a markdown table. This is done so that the test cases can be more easily
694+
// edited and read.
695+
//
696+
// Copy and paste the table to https://www.tablesgenerator.com/markdown_tables for easier editing
697+
//
698+
//nolint:paralleltest,tparallel // Parameters load values from env vars
699+
func TestParameterValidationEnforcement(t *testing.T) {
700+
// Some interesting observations:
701+
// - Validation logic does not apply to the value of 'options'
702+
// - [NumDefInvOpt] So an invalid option can be present and selected, but would fail
703+
// - Validation logic does not apply to the default if a value is given
704+
// - [NumIns/DefInv] So the default can be invalid if an input value is valid.
705+
// The value is therefore not really optional, but it is marked as such.
706+
// - [NumInsNotOptsVal | NumsInsNotOpts] values do not need to be in the option set?
707+
// - [NumInsNotNum] number params do not require the value to be a number
708+
// - [LStrInsNotList] list(string) do not require the value to be a list(string)
709+
// - Same with [MulInsNotListOpts]
710+
table, err := os.ReadFile("testdata/parameter_table.md")
711+
require.NoError(t, err)
712+
713+
type row struct {
714+
Name string
715+
Types []string
716+
InputValue string
717+
Default string
718+
Options []string
719+
Validation *provider.Validation
720+
OutputValue string
721+
Optional bool
722+
Error *regexp.Regexp
723+
}
724+
725+
rows := make([]row, 0)
726+
lines := strings.Split(string(table), "\n")
727+
validMinMax := regexp.MustCompile("^[0-9]*-[0-9]*$")
728+
for _, line := range lines[2:] {
729+
columns := strings.Split(line, "|")
730+
columns = columns[1 : len(columns)-1]
731+
for i := range columns {
732+
// Trim the whitespace from all columns
733+
columns[i] = strings.TrimSpace(columns[i])
734+
}
735+
736+
if columns[0] == "" {
737+
continue // Skip rows with empty names
738+
}
739+
740+
optional, err := strconv.ParseBool(columns[8])
741+
if columns[8] != "" {
742+
// Value does not matter if not specified
743+
require.NoError(t, err)
744+
}
745+
746+
var rerr *regexp.Regexp
747+
if columns[9] != "" {
748+
rerr, err = regexp.Compile(columns[9])
749+
if err != nil {
750+
t.Fatalf("failed to parse error column %q: %v", columns[9], err)
751+
}
752+
}
753+
var options []string
754+
if columns[4] != "" {
755+
options = strings.Split(columns[4], ",")
756+
}
757+
758+
var validation *provider.Validation
759+
if columns[5] != "" {
760+
// Min-Max validation should look like:
761+
// 1-10 :: min=1, max=10
762+
// -10 :: max=10
763+
// 1- :: min=1
764+
if validMinMax.MatchString(columns[5]) {
765+
parts := strings.Split(columns[5], "-")
766+
min, _ := strconv.ParseInt(parts[0], 10, 64)
767+
max, _ := strconv.ParseInt(parts[1], 10, 64)
768+
validation = &provider.Validation{
769+
Min: int(min),
770+
MinDisabled: parts[0] == "",
771+
Max: int(max),
772+
MaxDisabled: parts[1] == "",
773+
Monotonic: "",
774+
Regex: "",
775+
Error: "{min} < {value} < {max}",
776+
}
777+
} else {
778+
validation = &provider.Validation{
779+
Min: 0,
780+
MinDisabled: true,
781+
Max: 0,
782+
MaxDisabled: true,
783+
Monotonic: "",
784+
Regex: columns[5],
785+
Error: "regex error",
786+
}
787+
}
788+
}
789+
790+
rows = append(rows, row{
791+
Name: columns[0],
792+
Types: strings.Split(columns[1], ","),
793+
InputValue: columns[2],
794+
Default: columns[3],
795+
Options: options,
796+
Validation: validation,
797+
OutputValue: columns[7],
798+
Optional: optional,
799+
Error: rerr,
800+
})
801+
}
802+
803+
stringLiteral := func(s string) string {
804+
if s == "" {
805+
return `""`
806+
}
807+
return fmt.Sprintf("%q", s)
808+
}
809+
810+
for rowIndex, row := range rows {
811+
for _, rt := range row.Types {
812+
//nolint:paralleltest,tparallel // Parameters load values from env vars
813+
t.Run(fmt.Sprintf("%d|%s:%s", rowIndex, row.Name, rt), func(t *testing.T) {
814+
if row.InputValue != "" {
815+
t.Setenv(provider.ParameterEnvironmentVariable("parameter"), row.InputValue)
816+
}
817+
818+
if row.Error != nil {
819+
if row.OutputValue != "" {
820+
t.Errorf("output value %q should not be set if error is set", row.OutputValue)
821+
}
822+
}
823+
824+
var cfg strings.Builder
825+
cfg.WriteString("data \"coder_parameter\" \"parameter\" {\n")
826+
cfg.WriteString("\tname = \"parameter\"\n")
827+
if rt == "multi-select" || rt == "tag-select" {
828+
cfg.WriteString(fmt.Sprintf("\ttype = \"%s\"\n", "list(string)"))
829+
cfg.WriteString(fmt.Sprintf("\tform_type = \"%s\"\n", rt))
830+
} else {
831+
cfg.WriteString(fmt.Sprintf("\ttype = \"%s\"\n", rt))
832+
}
833+
if row.Default != "" {
834+
cfg.WriteString(fmt.Sprintf("\tdefault = %s\n", stringLiteral(row.Default)))
835+
}
836+
837+
for _, opt := range row.Options {
838+
cfg.WriteString("\toption {\n")
839+
cfg.WriteString(fmt.Sprintf("\t\tname = %s\n", stringLiteral(opt)))
840+
cfg.WriteString(fmt.Sprintf("\t\tvalue = %s\n", stringLiteral(opt)))
841+
cfg.WriteString("\t}\n")
842+
}
843+
844+
if row.Validation != nil {
845+
cfg.WriteString("\tvalidation {\n")
846+
if !row.Validation.MinDisabled {
847+
cfg.WriteString(fmt.Sprintf("\t\tmin = %d\n", row.Validation.Min))
848+
}
849+
if !row.Validation.MaxDisabled {
850+
cfg.WriteString(fmt.Sprintf("\t\tmax = %d\n", row.Validation.Max))
851+
}
852+
if row.Validation.Monotonic != "" {
853+
cfg.WriteString(fmt.Sprintf("\t\tmonotonic = \"%s\"\n", row.Validation.Monotonic))
854+
}
855+
if row.Validation.Regex != "" {
856+
cfg.WriteString(fmt.Sprintf("\t\tregex = %q\n", row.Validation.Regex))
857+
}
858+
cfg.WriteString(fmt.Sprintf("\t\terror = %q\n", row.Validation.Error))
859+
cfg.WriteString("\t}\n")
860+
}
861+
862+
cfg.WriteString("}\n")
863+
864+
resource.Test(t, resource.TestCase{
865+
ProviderFactories: coderFactory(),
866+
IsUnitTest: true,
867+
Steps: []resource.TestStep{{
868+
Config: cfg.String(),
869+
ExpectError: row.Error,
870+
Check: func(state *terraform.State) error {
871+
require.Len(t, state.Modules, 1)
872+
require.Len(t, state.Modules[0].Resources, 1)
873+
param := state.Modules[0].Resources["data.coder_parameter.parameter"]
874+
require.NotNil(t, param)
875+
876+
if row.Default == "" {
877+
_, ok := param.Primary.Attributes["default"]
878+
require.False(t, ok, "default should not be set")
879+
} else {
880+
require.Equal(t, strings.Trim(row.Default, `"`), param.Primary.Attributes["default"])
881+
}
882+
883+
if row.OutputValue == "" {
884+
_, ok := param.Primary.Attributes["value"]
885+
require.False(t, ok, "output value should not be set")
886+
} else {
887+
require.Equal(t, strings.Trim(row.OutputValue, `"`), param.Primary.Attributes["value"])
888+
}
889+
890+
for key, expected := range map[string]string{
891+
"optional": strconv.FormatBool(row.Optional),
892+
} {
893+
require.Equal(t, expected, param.Primary.Attributes[key], "optional")
894+
}
895+
896+
return nil
897+
},
898+
}},
899+
})
900+
})
901+
}
902+
}
903+
}
904+
689905
func TestValueValidatesType(t *testing.T) {
690906
t.Parallel()
691907
for _, tc := range []struct {
@@ -798,6 +1014,25 @@ func TestValueValidatesType(t *testing.T) {
7981014
Value: `[]`,
7991015
MinDisabled: true,
8001016
MaxDisabled: true,
1017+
}, {
1018+
Name: "ValidListOfStrings",
1019+
Type: "list(string)",
1020+
Value: `["first","second","third"]`,
1021+
MinDisabled: true,
1022+
MaxDisabled: true,
1023+
}, {
1024+
Name: "InvalidListOfStrings",
1025+
Type: "list(string)",
1026+
Value: `["first","second","third"`,
1027+
MinDisabled: true,
1028+
MaxDisabled: true,
1029+
Error: regexp.MustCompile("is not valid list of strings"),
1030+
}, {
1031+
Name: "EmptyListOfStrings",
1032+
Type: "list(string)",
1033+
Value: `[]`,
1034+
MinDisabled: true,
1035+
MaxDisabled: true,
8011036
}} {
8021037
tc := tc
8031038
t.Run(tc.Name, func(t *testing.T) {

provider/testdata/parameter_table.md

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
| Name | Type | Input | Default | Options | Validation | -> | Output | Optional | Error |
2+
|----------------------|---------------|-----------|---------|-------------------|------------|----|--------|----------|--------------|
3+
| | Empty Vals | | | | | | | | |
4+
| Empty | string,number | | | | | | "" | false | |
5+
| EmptyDupeOps | string,number | | | 1,1,1 | | | | | unique |
6+
| EmptyList | list(string) | | | | | | "" | false | |
7+
| EmptyListDupeOpts | list(string) | | | ["a"],["a"] | | | | | unique |
8+
| EmptyMulti | tag-select | | | | | | "" | false | |
9+
| EmptyOpts | string,number | | | 1,2,3 | | | "" | false | |
10+
| EmptyRegex | string | | | | world | | | | regex error |
11+
| EmptyMin | number | | | | 1-10 | | | | 1 < < 10 |
12+
| EmptyMinOpt | number | | | 1,2,3 | 2-5 | | | | 2 < < 5 |
13+
| EmptyRegexOpt | string | | | "hello","goodbye" | goodbye | | | | regex error |
14+
| EmptyRegexOk | string | | | | .* | | "" | false | |
15+
| | | | | | | | | | |
16+
| | Default Set | No inputs | | | | | | | |
17+
| NumDef | number | | 5 | | | | 5 | true | |
18+
| NumDefVal | number | | 5 | | 3-7 | | 5 | true | |
19+
| NumDefInv | number | | 5 | | 10- | | | | 10 < 5 < 0 |
20+
| NumDefOpts | number | | 5 | 1,3,5,7 | 2-6 | | 5 | true | |
21+
| NumDefNotOpts | number | | 5 | 1,3,7,9 | 2-6 | | | | valid option |
22+
| NumDefInvOpt | number | | 5 | 1,3,5,7 | 6-10 | | | | 6 < 5 < 10 |
23+
| NumDefNotNum | number | | a | | | | | | a number |
24+
| NumDefOptsNotNum | number | | 1 | 1,a,2 | | | | | a number |
25+
| | | | | | | | | | |
26+
| StrDef | string | | hello | | | | hello | true | |
27+
| StrDefInv | string | | hello | | world | | | | regex error |
28+
| StrDefOpts | string | | a | a,b,c | | | a | true | |
29+
| StrDefNotOpts | string | | a | b,c,d | | | | | valid option |
30+
| StrDefValOpts | string | | a | a,b,c,d,e,f | [a-c] | | a | true | |
31+
| StrDefInvOpt | string | | d | a,b,c,d,e,f | [a-c] | | | | regex error |
32+
| | | | | | | | | | |
33+
| LStrDef | list(string) | | ["a"] | | | | ["a"] | true | |
34+
| LStrDefOpts | list(string) | | ["a"] | ["a"], ["b"] | | | ["a"] | true | |
35+
| LStrDefNotOpts | list(string) | | ["a"] | ["b"], ["c"] | | | | | valid option |
36+
| | | | | | | | | | |
37+
| MulDef | tag-select | | ["a"] | | | | ["a"] | true | |
38+
| MulDefOpts | multi-select | | ["a"] | a,b | | | ["a"] | true | |
39+
| MulDefNotOpts | multi-select | | ["a"] | b,c | | | | | valid option |
40+
| | | | | | | | | | |
41+
| | Input Vals | | | | | | | | |
42+
| NumIns | number | 3 | | | | | 3 | false | |
43+
| NumInsNotNum | number | a | | | | | a | false | |
44+
| NumInsNotNumInv | number | a | | | 1-3 | | | | 1 < a < 3 |
45+
| NumInsDef | number | 3 | 5 | | | | 3 | true | |
46+
| NumIns/DefInv | number | 3 | 5 | | 1-3 | | 3 | true | |
47+
| NumIns=DefInv | number | 5 | 5 | | 1-3 | | | | 1 < 5 < 3 |
48+
| NumInsOpts | number | 3 | 5 | 1,2,3,4,5 | 1-3 | | 3 | true | |
49+
| NumInsNotOptsVal | number | 3 | 5 | 1,2,4,5 | 1-3 | | 3 | true | |
50+
| NumInsNotOptsInv | number | 3 | 5 | 1,2,4,5 | 1-2 | | | true | 1 < 3 < 2 |
51+
| NumInsNotOpts | number | 3 | 5 | 1,2,4,5 | | | 3 | true | |
52+
| NumInsNotOpts/NoDef | number | 3 | | 1,2,4,5 | | | 3 | false | |
53+
| | | | | | | | | | |
54+
| StrIns | string | c | | | | | c | false | |
55+
| StrInsDupeOpts | string | c | | a,b,c,c | | | | | unique |
56+
| StrInsDef | string | c | e | | | | c | true | |
57+
| StrIns/DefInv | string | c | e | | [a-c] | | c | true | |
58+
| StrIns=DefInv | string | e | e | | [a-c] | | | | regex error |
59+
| StrInsOpts | string | c | e | a,b,c,d,e | [a-c] | | c | true | |
60+
| StrInsNotOptsVal | string | c | e | a,b,d,e | [a-c] | | c | true | |
61+
| StrInsNotOptsInv | string | c | e | a,b,d,e | [a-b] | | | | regex error |
62+
| StrInsNotOpts | string | c | e | a,b,d,e | | | c | true | |
63+
| StrInsNotOpts/NoDef | string | c | | a,b,d,e | | | c | false | |
64+
| StrInsBadVal | string | c | | a,b,c,d,e | 1-10 | | | | min cannot |
65+
| | | | | | | | | | |
66+
| | list(string) | | | | | | | | |
67+
| LStrIns | list(string) | ["c"] | | | | | ["c"] | false | |
68+
| LStrInsNotList | list(string) | c | | | | | c | false | |
69+
| LStrInsDef | list(string) | ["c"] | ["e"] | | | | ["c"] | true | |
70+
| LStrIns/DefInv | list(string) | ["c"] | ["e"] | | [a-c] | | | | regex cannot |
71+
| LStrInsOpts | list(string) | ["c"] | ["e"] | ["c"],["d"],["e"] | | | ["c"] | true | |
72+
| LStrInsNotOpts | list(string) | ["c"] | ["e"] | ["d"],["e"] | | | ["c"] | true | |
73+
| LStrInsNotOpts/NoDef | list(string) | ["c"] | | ["d"],["e"] | | | ["c"] | false | |
74+
| | | | | | | | | | |
75+
| MulInsOpts | multi-select | ["c"] | ["e"] | c,d,e | | | ["c"] | true | |
76+
| MulInsNotListOpts | multi-select | c | ["e"] | c,d,e | | | c | true | |
77+
| MulInsNotOpts | multi-select | ["c"] | ["e"] | d,e | | | ["c"] | true | |
78+
| MulInsNotOpts/NoDef | multi-select | ["c"] | | d,e | | | ["c"] | false | |
79+
| MulInsInvOpts | multi-select | ["c"] | ["e"] | c,d,e | [a-c] | | | | regex cannot |

0 commit comments

Comments
 (0)