@@ -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,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 ("\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+
689905func 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 ) {
0 commit comments