@@ -2,7 +2,9 @@ package provider_test
22
33import (
44 "fmt"
5+ "os"
56 "regexp"
7+ "strconv"
68 "strings"
79 "testing"
810
@@ -686,6 +688,217 @@ 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+ table , err := os .ReadFile ("testdata/parameter_table.md" )
708+ require .NoError (t , err )
709+
710+ type row struct {
711+ Name string
712+ Types []string
713+ InputValue string
714+ Default string
715+ Options []string
716+ Validation * provider.Validation
717+ OutputValue string
718+ Optional bool
719+ Error * regexp.Regexp
720+ }
721+
722+ rows := make ([]row , 0 )
723+ lines := strings .Split (string (table ), "\n " )
724+ validMinMax := regexp .MustCompile ("^[0-9]*-[0-9]*$" )
725+ for _ , line := range lines [2 :] {
726+ columns := strings .Split (line , "|" )
727+ columns = columns [1 : len (columns )- 1 ]
728+ for i := range columns {
729+ // Trim the whitespace from all columns
730+ columns [i ] = strings .TrimSpace (columns [i ])
731+ }
732+
733+ if columns [0 ] == "" {
734+ continue // Skip rows with empty names
735+ }
736+
737+ optional , err := strconv .ParseBool (columns [8 ])
738+ if columns [8 ] != "" {
739+ // Value does not matter if not specified
740+ require .NoError (t , err )
741+ }
742+
743+ var rerr * regexp.Regexp
744+ if columns [9 ] != "" {
745+ rerr , err = regexp .Compile (columns [9 ])
746+ if err != nil {
747+ t .Fatalf ("failed to parse error column %q: %v" , columns [9 ], err )
748+ }
749+ }
750+ var options []string
751+ if columns [4 ] != "" {
752+ options = strings .Split (columns [4 ], "," )
753+ }
754+
755+ var validation * provider.Validation
756+ if columns [5 ] != "" {
757+ // Min-Max validation should look like:
758+ // 1-10 :: min=1, max=10
759+ // -10 :: max=10
760+ // 1- :: min=1
761+ if validMinMax .MatchString (columns [5 ]) {
762+ parts := strings .Split (columns [5 ], "-" )
763+ min , _ := strconv .ParseInt (parts [0 ], 10 , 64 )
764+ max , _ := strconv .ParseInt (parts [1 ], 10 , 64 )
765+ validation = & provider.Validation {
766+ Min : int (min ),
767+ MinDisabled : parts [0 ] == "" ,
768+ Max : int (max ),
769+ MaxDisabled : parts [1 ] == "" ,
770+ Monotonic : "" ,
771+ Regex : "" ,
772+ Error : "{min} < {value} < {max}" ,
773+ }
774+ } else {
775+ validation = & provider.Validation {
776+ Min : 0 ,
777+ MinDisabled : true ,
778+ Max : 0 ,
779+ MaxDisabled : true ,
780+ Monotonic : "" ,
781+ Regex : columns [5 ],
782+ Error : "regex error" ,
783+ }
784+ }
785+ }
786+
787+ rows = append (rows , row {
788+ Name : columns [0 ],
789+ Types : strings .Split (columns [1 ], "," ),
790+ InputValue : columns [2 ],
791+ Default : columns [3 ],
792+ Options : options ,
793+ Validation : validation ,
794+ OutputValue : columns [7 ],
795+ Optional : optional ,
796+ Error : rerr ,
797+ })
798+ }
799+
800+ stringLiteral := func (s string ) string {
801+ if s == "" {
802+ return `""`
803+ }
804+ return fmt .Sprintf ("%q" , s )
805+ }
806+
807+ for rowIndex , row := range rows {
808+ for _ , rt := range row .Types {
809+ //nolint:paralleltest,tparallel // Parameters load values from env vars
810+ t .Run (fmt .Sprintf ("%d|%s:%s" , rowIndex , row .Name , rt ), func (t * testing.T ) {
811+ if row .InputValue != "" {
812+ t .Setenv (provider .ParameterEnvironmentVariable ("parameter" ), row .InputValue )
813+ }
814+
815+ if row .Error != nil {
816+ if row .OutputValue != "" {
817+ t .Errorf ("output value %q should not be set if error is set" , row .OutputValue )
818+ }
819+ }
820+
821+ var cfg strings.Builder
822+ cfg .WriteString ("data \" coder_parameter\" \" parameter\" {\n " )
823+ cfg .WriteString ("\t name = \" parameter\" \n " )
824+ if rt == "multi-select" || rt == "tag-select" {
825+ cfg .WriteString (fmt .Sprintf ("\t type = \" %s\" \n " , "list(string)" ))
826+ cfg .WriteString (fmt .Sprintf ("\t form_type = \" %s\" \n " , rt ))
827+ } else {
828+ cfg .WriteString (fmt .Sprintf ("\t type = \" %s\" \n " , rt ))
829+ }
830+ if row .Default != "" {
831+ cfg .WriteString (fmt .Sprintf ("\t default = %s\n " , stringLiteral (row .Default )))
832+ }
833+
834+ for _ , opt := range row .Options {
835+ cfg .WriteString ("\t option {\n " )
836+ cfg .WriteString (fmt .Sprintf ("\t \t name = %s\n " , stringLiteral (opt )))
837+ cfg .WriteString (fmt .Sprintf ("\t \t value = %s\n " , stringLiteral (opt )))
838+ cfg .WriteString ("\t }\n " )
839+ }
840+
841+ if row .Validation != nil {
842+ cfg .WriteString ("\t validation {\n " )
843+ if ! row .Validation .MinDisabled {
844+ cfg .WriteString (fmt .Sprintf ("\t \t min = %d\n " , row .Validation .Min ))
845+ }
846+ if ! row .Validation .MaxDisabled {
847+ cfg .WriteString (fmt .Sprintf ("\t \t max = %d\n " , row .Validation .Max ))
848+ }
849+ if row .Validation .Monotonic != "" {
850+ cfg .WriteString (fmt .Sprintf ("\t \t monotonic = \" %s\" \n " , row .Validation .Monotonic ))
851+ }
852+ if row .Validation .Regex != "" {
853+ cfg .WriteString (fmt .Sprintf ("\t \t regex = %q\n " , row .Validation .Regex ))
854+ }
855+ cfg .WriteString (fmt .Sprintf ("\t \t error = %q\n " , row .Validation .Error ))
856+ cfg .WriteString ("\t }\n " )
857+ }
858+
859+ cfg .WriteString ("}\n " )
860+
861+ resource .Test (t , resource.TestCase {
862+ ProviderFactories : coderFactory (),
863+ IsUnitTest : true ,
864+ Steps : []resource.TestStep {{
865+ Config : cfg .String (),
866+ ExpectError : row .Error ,
867+ Check : func (state * terraform.State ) error {
868+ require .Len (t , state .Modules , 1 )
869+ require .Len (t , state .Modules [0 ].Resources , 1 )
870+ param := state .Modules [0 ].Resources ["data.coder_parameter.parameter" ]
871+ require .NotNil (t , param )
872+
873+ if row .Default == "" {
874+ _ , ok := param .Primary .Attributes ["default" ]
875+ require .False (t , ok , "default should not be set" )
876+ } else {
877+ require .Equal (t , strings .Trim (row .Default , `"` ), param .Primary .Attributes ["default" ])
878+ }
879+
880+ if row .OutputValue == "" {
881+ _ , ok := param .Primary .Attributes ["value" ]
882+ require .False (t , ok , "output value should not be set" )
883+ } else {
884+ require .Equal (t , strings .Trim (row .OutputValue , `"` ), param .Primary .Attributes ["value" ])
885+ }
886+
887+ for key , expected := range map [string ]string {
888+ "optional" : strconv .FormatBool (row .Optional ),
889+ } {
890+ require .Equal (t , expected , param .Primary .Attributes [key ], "optional" )
891+ }
892+
893+ return nil
894+ },
895+ }},
896+ })
897+ })
898+ }
899+ }
900+ }
901+
689902func TestValueValidatesType (t * testing.T ) {
690903 t .Parallel ()
691904 for _ , tc := range []struct {
@@ -798,6 +1011,25 @@ func TestValueValidatesType(t *testing.T) {
7981011 Value : `[]` ,
7991012 MinDisabled : true ,
8001013 MaxDisabled : true ,
1014+ }, {
1015+ Name : "ValidListOfStrings" ,
1016+ Type : "list(string)" ,
1017+ Value : `["first","second","third"]` ,
1018+ MinDisabled : true ,
1019+ MaxDisabled : true ,
1020+ }, {
1021+ Name : "InvalidListOfStrings" ,
1022+ Type : "list(string)" ,
1023+ Value : `["first","second","third"` ,
1024+ MinDisabled : true ,
1025+ MaxDisabled : true ,
1026+ Error : regexp .MustCompile ("is not valid list of strings" ),
1027+ }, {
1028+ Name : "EmptyListOfStrings" ,
1029+ Type : "list(string)" ,
1030+ Value : `[]` ,
1031+ MinDisabled : true ,
1032+ MaxDisabled : true ,
8011033 }} {
8021034 tc := tc
8031035 t .Run (tc .Name , func (t * testing.T ) {
0 commit comments