@@ -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 ) {
690903t .Parallel ()
691904for _ ,tc := range []struct {
@@ -798,6 +1011,25 @@ func TestValueValidatesType(t *testing.T) {
7981011Value :`[]` ,
7991012MinDisabled :true ,
8001013MaxDisabled :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}} {
8021034tc := tc
8031035t .Run (tc .Name ,func (t * testing.T ) {