@@ -2,7 +2,9 @@ package provider_test
2
2
3
3
import (
4
4
"fmt"
5
+ "os"
5
6
"regexp"
7
+ "strconv"
6
8
"strings"
7
9
"testing"
8
10
@@ -686,6 +688,220 @@ data "coder_parameter" "region" {
686
688
}
687
689
}
688
690
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 ("\t name = \" parameter\" \n " )
827
+ if rt == "multi-select" || rt == "tag-select" {
828
+ cfg .WriteString (fmt .Sprintf ("\t type = \" %s\" \n " , "list(string)" ))
829
+ cfg .WriteString (fmt .Sprintf ("\t form_type = \" %s\" \n " , rt ))
830
+ } else {
831
+ cfg .WriteString (fmt .Sprintf ("\t type = \" %s\" \n " , rt ))
832
+ }
833
+ if row .Default != "" {
834
+ cfg .WriteString (fmt .Sprintf ("\t default = %s\n " , stringLiteral (row .Default )))
835
+ }
836
+
837
+ for _ , opt := range row .Options {
838
+ cfg .WriteString ("\t option {\n " )
839
+ cfg .WriteString (fmt .Sprintf ("\t \t name = %s\n " , stringLiteral (opt )))
840
+ cfg .WriteString (fmt .Sprintf ("\t \t value = %s\n " , stringLiteral (opt )))
841
+ cfg .WriteString ("\t }\n " )
842
+ }
843
+
844
+ if row .Validation != nil {
845
+ cfg .WriteString ("\t validation {\n " )
846
+ if ! row .Validation .MinDisabled {
847
+ cfg .WriteString (fmt .Sprintf ("\t \t min = %d\n " , row .Validation .Min ))
848
+ }
849
+ if ! row .Validation .MaxDisabled {
850
+ cfg .WriteString (fmt .Sprintf ("\t \t max = %d\n " , row .Validation .Max ))
851
+ }
852
+ if row .Validation .Monotonic != "" {
853
+ cfg .WriteString (fmt .Sprintf ("\t \t monotonic = \" %s\" \n " , row .Validation .Monotonic ))
854
+ }
855
+ if row .Validation .Regex != "" {
856
+ cfg .WriteString (fmt .Sprintf ("\t \t regex = %q\n " , row .Validation .Regex ))
857
+ }
858
+ cfg .WriteString (fmt .Sprintf ("\t \t error = %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
+
689
905
func TestValueValidatesType (t * testing.T ) {
690
906
t .Parallel ()
691
907
for _ , tc := range []struct {
@@ -798,6 +1014,25 @@ func TestValueValidatesType(t *testing.T) {
798
1014
Value : `[]` ,
799
1015
MinDisabled : true ,
800
1016
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 ,
801
1036
}} {
802
1037
tc := tc
803
1038
t .Run (tc .Name , func (t * testing.T ) {
0 commit comments