github.com/kanishk98/terraform@v1.3.0-dev.0.20220917174235-661ca8088a6a/internal/terraform/eval_variable_test.go (about)

     1  package terraform
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  	"testing"
     7  
     8  	"github.com/hashicorp/hcl/v2"
     9  	"github.com/zclconf/go-cty/cty"
    10  
    11  	"github.com/hashicorp/terraform/internal/addrs"
    12  	"github.com/hashicorp/terraform/internal/lang"
    13  	"github.com/hashicorp/terraform/internal/lang/marks"
    14  	"github.com/hashicorp/terraform/internal/tfdiags"
    15  )
    16  
    17  func TestPrepareFinalInputVariableValue(t *testing.T) {
    18  	// This is just a concise way to define a bunch of *configs.Variable
    19  	// objects to use in our tests below. We're only going to decode this
    20  	// config, not fully evaluate it.
    21  	cfgSrc := `
    22  		variable "nullable_required" {
    23  		}
    24  		variable "nullable_optional_default_string" {
    25  			default = "hello"
    26  		}
    27  		variable "nullable_optional_default_null" {
    28  			default = null
    29  		}
    30  		variable "constrained_string_nullable_required" {
    31  			type = string
    32  		}
    33  		variable "constrained_string_nullable_optional_default_string" {
    34  			type    = string
    35  			default = "hello"
    36  		}
    37  		variable "constrained_string_nullable_optional_default_bool" {
    38  			type    = string
    39  			default = true
    40  		}
    41  		variable "constrained_string_nullable_optional_default_null" {
    42  			type    = string
    43  			default = null
    44  		}
    45  		variable "required" {
    46  			nullable = false
    47  		}
    48  		variable "optional_default_string" {
    49  			nullable = false
    50  			default  = "hello"
    51  		}
    52  		variable "constrained_string_required" {
    53  			nullable = false
    54  			type     = string
    55  		}
    56  		variable "constrained_string_optional_default_string" {
    57  			nullable = false
    58  			type     = string
    59  			default  = "hello"
    60  		}
    61  		variable "constrained_string_optional_default_bool" {
    62  			nullable = false
    63  			type     = string
    64  			default  = true
    65  		}
    66  		variable "constrained_string_sensitive_required" {
    67  			sensitive = true
    68  			nullable  = false
    69  			type      = string
    70  		}
    71  	`
    72  	cfg := testModuleInline(t, map[string]string{
    73  		"main.tf": cfgSrc,
    74  	})
    75  	variableConfigs := cfg.Module.Variables
    76  
    77  	// Because we loaded our pseudo-module from a temporary file, the
    78  	// declaration source ranges will have unpredictable filenames. We'll
    79  	// fix that here just to make things easier below.
    80  	for _, vc := range variableConfigs {
    81  		vc.DeclRange.Filename = "main.tf"
    82  	}
    83  
    84  	tests := []struct {
    85  		varName string
    86  		given   cty.Value
    87  		want    cty.Value
    88  		wantErr string
    89  	}{
    90  		// nullable_required
    91  		{
    92  			"nullable_required",
    93  			cty.NilVal,
    94  			cty.UnknownVal(cty.DynamicPseudoType),
    95  			`Required variable not set: The variable "nullable_required" is required, but is not set.`,
    96  		},
    97  		{
    98  			"nullable_required",
    99  			cty.NullVal(cty.DynamicPseudoType),
   100  			cty.NullVal(cty.DynamicPseudoType),
   101  			``, // "required" for a nullable variable means only that it must be set, even if it's set to null
   102  		},
   103  		{
   104  			"nullable_required",
   105  			cty.StringVal("ahoy"),
   106  			cty.StringVal("ahoy"),
   107  			``,
   108  		},
   109  		{
   110  			"nullable_required",
   111  			cty.UnknownVal(cty.String),
   112  			cty.UnknownVal(cty.String),
   113  			``,
   114  		},
   115  
   116  		// nullable_optional_default_string
   117  		{
   118  			"nullable_optional_default_string",
   119  			cty.NilVal,
   120  			cty.StringVal("hello"), // the declared default value
   121  			``,
   122  		},
   123  		{
   124  			"nullable_optional_default_string",
   125  			cty.NullVal(cty.DynamicPseudoType),
   126  			cty.NullVal(cty.DynamicPseudoType), // nullable variables can be really set to null, masking the default
   127  			``,
   128  		},
   129  		{
   130  			"nullable_optional_default_string",
   131  			cty.StringVal("ahoy"),
   132  			cty.StringVal("ahoy"),
   133  			``,
   134  		},
   135  		{
   136  			"nullable_optional_default_string",
   137  			cty.UnknownVal(cty.String),
   138  			cty.UnknownVal(cty.String),
   139  			``,
   140  		},
   141  
   142  		// nullable_optional_default_null
   143  		{
   144  			"nullable_optional_default_null",
   145  			cty.NilVal,
   146  			cty.NullVal(cty.DynamicPseudoType), // the declared default value
   147  			``,
   148  		},
   149  		{
   150  			"nullable_optional_default_null",
   151  			cty.NullVal(cty.String),
   152  			cty.NullVal(cty.String), // nullable variables can be really set to null, masking the default
   153  			``,
   154  		},
   155  		{
   156  			"nullable_optional_default_null",
   157  			cty.StringVal("ahoy"),
   158  			cty.StringVal("ahoy"),
   159  			``,
   160  		},
   161  		{
   162  			"nullable_optional_default_null",
   163  			cty.UnknownVal(cty.String),
   164  			cty.UnknownVal(cty.String),
   165  			``,
   166  		},
   167  
   168  		// constrained_string_nullable_required
   169  		{
   170  			"constrained_string_nullable_required",
   171  			cty.NilVal,
   172  			cty.UnknownVal(cty.String),
   173  			`Required variable not set: The variable "constrained_string_nullable_required" is required, but is not set.`,
   174  		},
   175  		{
   176  			"constrained_string_nullable_required",
   177  			cty.NullVal(cty.DynamicPseudoType),
   178  			cty.NullVal(cty.String), // the null value still gets converted to match the type constraint
   179  			``,                      // "required" for a nullable variable means only that it must be set, even if it's set to null
   180  		},
   181  		{
   182  			"constrained_string_nullable_required",
   183  			cty.StringVal("ahoy"),
   184  			cty.StringVal("ahoy"),
   185  			``,
   186  		},
   187  		{
   188  			"constrained_string_nullable_required",
   189  			cty.UnknownVal(cty.String),
   190  			cty.UnknownVal(cty.String),
   191  			``,
   192  		},
   193  
   194  		// constrained_string_nullable_optional_default_string
   195  		{
   196  			"constrained_string_nullable_optional_default_string",
   197  			cty.NilVal,
   198  			cty.StringVal("hello"), // the declared default value
   199  			``,
   200  		},
   201  		{
   202  			"constrained_string_nullable_optional_default_string",
   203  			cty.NullVal(cty.DynamicPseudoType),
   204  			cty.NullVal(cty.String), // nullable variables can be really set to null, masking the default
   205  			``,
   206  		},
   207  		{
   208  			"constrained_string_nullable_optional_default_string",
   209  			cty.StringVal("ahoy"),
   210  			cty.StringVal("ahoy"),
   211  			``,
   212  		},
   213  		{
   214  			"constrained_string_nullable_optional_default_string",
   215  			cty.UnknownVal(cty.String),
   216  			cty.UnknownVal(cty.String),
   217  			``,
   218  		},
   219  
   220  		// constrained_string_nullable_optional_default_bool
   221  		{
   222  			"constrained_string_nullable_optional_default_bool",
   223  			cty.NilVal,
   224  			cty.StringVal("true"), // the declared default value, automatically converted to match type constraint
   225  			``,
   226  		},
   227  		{
   228  			"constrained_string_nullable_optional_default_bool",
   229  			cty.NullVal(cty.DynamicPseudoType),
   230  			cty.NullVal(cty.String), // nullable variables can be really set to null, masking the default
   231  			``,
   232  		},
   233  		{
   234  			"constrained_string_nullable_optional_default_bool",
   235  			cty.StringVal("ahoy"),
   236  			cty.StringVal("ahoy"),
   237  			``,
   238  		},
   239  		{
   240  			"constrained_string_nullable_optional_default_bool",
   241  			cty.UnknownVal(cty.String),
   242  			cty.UnknownVal(cty.String),
   243  			``,
   244  		},
   245  
   246  		// constrained_string_nullable_optional_default_null
   247  		{
   248  			"constrained_string_nullable_optional_default_null",
   249  			cty.NilVal,
   250  			cty.NullVal(cty.String),
   251  			``,
   252  		},
   253  		{
   254  			"constrained_string_nullable_optional_default_null",
   255  			cty.NullVal(cty.DynamicPseudoType),
   256  			cty.NullVal(cty.String),
   257  			``,
   258  		},
   259  		{
   260  			"constrained_string_nullable_optional_default_null",
   261  			cty.StringVal("ahoy"),
   262  			cty.StringVal("ahoy"),
   263  			``,
   264  		},
   265  		{
   266  			"constrained_string_nullable_optional_default_null",
   267  			cty.UnknownVal(cty.String),
   268  			cty.UnknownVal(cty.String),
   269  			``,
   270  		},
   271  
   272  		// required
   273  		{
   274  			"required",
   275  			cty.NilVal,
   276  			cty.UnknownVal(cty.DynamicPseudoType),
   277  			`Required variable not set: The variable "required" is required, but is not set.`,
   278  		},
   279  		{
   280  			"required",
   281  			cty.NullVal(cty.DynamicPseudoType),
   282  			cty.UnknownVal(cty.DynamicPseudoType),
   283  			`Required variable not set: Unsuitable value for var.required set from outside of the configuration: required variable may not be set to null.`,
   284  		},
   285  		{
   286  			"required",
   287  			cty.StringVal("ahoy"),
   288  			cty.StringVal("ahoy"),
   289  			``,
   290  		},
   291  		{
   292  			"required",
   293  			cty.UnknownVal(cty.String),
   294  			cty.UnknownVal(cty.String),
   295  			``,
   296  		},
   297  
   298  		// optional_default_string
   299  		{
   300  			"optional_default_string",
   301  			cty.NilVal,
   302  			cty.StringVal("hello"), // the declared default value
   303  			``,
   304  		},
   305  		{
   306  			"optional_default_string",
   307  			cty.NullVal(cty.DynamicPseudoType),
   308  			cty.StringVal("hello"), // the declared default value
   309  			``,
   310  		},
   311  		{
   312  			"optional_default_string",
   313  			cty.StringVal("ahoy"),
   314  			cty.StringVal("ahoy"),
   315  			``,
   316  		},
   317  		{
   318  			"optional_default_string",
   319  			cty.UnknownVal(cty.String),
   320  			cty.UnknownVal(cty.String),
   321  			``,
   322  		},
   323  
   324  		// constrained_string_required
   325  		{
   326  			"constrained_string_required",
   327  			cty.NilVal,
   328  			cty.UnknownVal(cty.String),
   329  			`Required variable not set: The variable "constrained_string_required" is required, but is not set.`,
   330  		},
   331  		{
   332  			"constrained_string_required",
   333  			cty.NullVal(cty.DynamicPseudoType),
   334  			cty.UnknownVal(cty.String),
   335  			`Required variable not set: Unsuitable value for var.constrained_string_required set from outside of the configuration: required variable may not be set to null.`,
   336  		},
   337  		{
   338  			"constrained_string_required",
   339  			cty.StringVal("ahoy"),
   340  			cty.StringVal("ahoy"),
   341  			``,
   342  		},
   343  		{
   344  			"constrained_string_required",
   345  			cty.UnknownVal(cty.String),
   346  			cty.UnknownVal(cty.String),
   347  			``,
   348  		},
   349  
   350  		// constrained_string_optional_default_string
   351  		{
   352  			"constrained_string_optional_default_string",
   353  			cty.NilVal,
   354  			cty.StringVal("hello"), // the declared default value
   355  			``,
   356  		},
   357  		{
   358  			"constrained_string_optional_default_string",
   359  			cty.NullVal(cty.DynamicPseudoType),
   360  			cty.StringVal("hello"), // the declared default value
   361  			``,
   362  		},
   363  		{
   364  			"constrained_string_optional_default_string",
   365  			cty.StringVal("ahoy"),
   366  			cty.StringVal("ahoy"),
   367  			``,
   368  		},
   369  		{
   370  			"constrained_string_optional_default_string",
   371  			cty.UnknownVal(cty.String),
   372  			cty.UnknownVal(cty.String),
   373  			``,
   374  		},
   375  
   376  		// constrained_string_optional_default_bool
   377  		{
   378  			"constrained_string_optional_default_bool",
   379  			cty.NilVal,
   380  			cty.StringVal("true"), // the declared default value, automatically converted to match type constraint
   381  			``,
   382  		},
   383  		{
   384  			"constrained_string_optional_default_bool",
   385  			cty.NullVal(cty.DynamicPseudoType),
   386  			cty.StringVal("true"), // the declared default value, automatically converted to match type constraint
   387  			``,
   388  		},
   389  		{
   390  			"constrained_string_optional_default_bool",
   391  			cty.StringVal("ahoy"),
   392  			cty.StringVal("ahoy"),
   393  			``,
   394  		},
   395  		{
   396  			"constrained_string_optional_default_bool",
   397  			cty.UnknownVal(cty.String),
   398  			cty.UnknownVal(cty.String),
   399  			``,
   400  		},
   401  
   402  		// sensitive
   403  		{
   404  			"constrained_string_sensitive_required",
   405  			cty.UnknownVal(cty.String),
   406  			cty.UnknownVal(cty.String),
   407  			``,
   408  		},
   409  	}
   410  
   411  	for _, test := range tests {
   412  		t.Run(fmt.Sprintf("%s %#v", test.varName, test.given), func(t *testing.T) {
   413  			varAddr := addrs.InputVariable{Name: test.varName}.Absolute(addrs.RootModuleInstance)
   414  			varCfg := variableConfigs[test.varName]
   415  			if varCfg == nil {
   416  				t.Fatalf("invalid variable name %q", test.varName)
   417  			}
   418  
   419  			t.Logf(
   420  				"test case\nvariable:    %s\nconstraint:  %#v\ndefault:     %#v\nnullable:    %#v\ngiven value: %#v",
   421  				varAddr,
   422  				varCfg.Type,
   423  				varCfg.Default,
   424  				varCfg.Nullable,
   425  				test.given,
   426  			)
   427  
   428  			rawVal := &InputValue{
   429  				Value:      test.given,
   430  				SourceType: ValueFromCaller,
   431  			}
   432  
   433  			got, diags := prepareFinalInputVariableValue(
   434  				varAddr, rawVal, varCfg,
   435  			)
   436  
   437  			if test.wantErr != "" {
   438  				if !diags.HasErrors() {
   439  					t.Errorf("unexpected success\nwant error: %s", test.wantErr)
   440  				} else if got, want := diags.Err().Error(), test.wantErr; got != want {
   441  					t.Errorf("wrong error\ngot:  %s\nwant: %s", got, want)
   442  				}
   443  			} else {
   444  				if diags.HasErrors() {
   445  					t.Errorf("unexpected error\ngot: %s", diags.Err().Error())
   446  				}
   447  			}
   448  
   449  			// NOTE: should still have returned some reasonable value even if there was an error
   450  			if !test.want.RawEquals(got) {
   451  				t.Fatalf("wrong result\ngot:  %#v\nwant: %#v", got, test.want)
   452  			}
   453  		})
   454  	}
   455  
   456  	t.Run("SourceType error message variants", func(t *testing.T) {
   457  		tests := []struct {
   458  			SourceType  ValueSourceType
   459  			SourceRange tfdiags.SourceRange
   460  			WantTypeErr string
   461  			WantNullErr string
   462  		}{
   463  			{
   464  				ValueFromUnknown,
   465  				tfdiags.SourceRange{},
   466  				`Invalid value for input variable: Unsuitable value for var.constrained_string_required set from outside of the configuration: string required.`,
   467  				`Required variable not set: Unsuitable value for var.constrained_string_required set from outside of the configuration: required variable may not be set to null.`,
   468  			},
   469  			{
   470  				ValueFromConfig,
   471  				tfdiags.SourceRange{
   472  					Filename: "example.tf",
   473  					Start:    tfdiags.SourcePos(hcl.InitialPos),
   474  					End:      tfdiags.SourcePos(hcl.InitialPos),
   475  				},
   476  				`Invalid value for input variable: The given value is not suitable for var.constrained_string_required declared at main.tf:32,3-41: string required.`,
   477  				`Required variable not set: The given value is not suitable for var.constrained_string_required defined at main.tf:32,3-41: required variable may not be set to null.`,
   478  			},
   479  			{
   480  				ValueFromAutoFile,
   481  				tfdiags.SourceRange{
   482  					Filename: "example.auto.tfvars",
   483  					Start:    tfdiags.SourcePos(hcl.InitialPos),
   484  					End:      tfdiags.SourcePos(hcl.InitialPos),
   485  				},
   486  				`Invalid value for input variable: The given value is not suitable for var.constrained_string_required declared at main.tf:32,3-41: string required.`,
   487  				`Required variable not set: The given value is not suitable for var.constrained_string_required defined at main.tf:32,3-41: required variable may not be set to null.`,
   488  			},
   489  			{
   490  				ValueFromNamedFile,
   491  				tfdiags.SourceRange{
   492  					Filename: "example.tfvars",
   493  					Start:    tfdiags.SourcePos(hcl.InitialPos),
   494  					End:      tfdiags.SourcePos(hcl.InitialPos),
   495  				},
   496  				`Invalid value for input variable: The given value is not suitable for var.constrained_string_required declared at main.tf:32,3-41: string required.`,
   497  				`Required variable not set: The given value is not suitable for var.constrained_string_required defined at main.tf:32,3-41: required variable may not be set to null.`,
   498  			},
   499  			{
   500  				ValueFromCLIArg,
   501  				tfdiags.SourceRange{},
   502  				`Invalid value for input variable: Unsuitable value for var.constrained_string_required set using -var="constrained_string_required=...": string required.`,
   503  				`Required variable not set: Unsuitable value for var.constrained_string_required set using -var="constrained_string_required=...": required variable may not be set to null.`,
   504  			},
   505  			{
   506  				ValueFromEnvVar,
   507  				tfdiags.SourceRange{},
   508  				`Invalid value for input variable: Unsuitable value for var.constrained_string_required set using the TF_VAR_constrained_string_required environment variable: string required.`,
   509  				`Required variable not set: Unsuitable value for var.constrained_string_required set using the TF_VAR_constrained_string_required environment variable: required variable may not be set to null.`,
   510  			},
   511  			{
   512  				ValueFromInput,
   513  				tfdiags.SourceRange{},
   514  				`Invalid value for input variable: Unsuitable value for var.constrained_string_required set using an interactive prompt: string required.`,
   515  				`Required variable not set: Unsuitable value for var.constrained_string_required set using an interactive prompt: required variable may not be set to null.`,
   516  			},
   517  			{
   518  				// NOTE: This isn't actually a realistic case for this particular
   519  				// function, because if we have a value coming from a plan then
   520  				// we must be in the apply step, and we shouldn't be able to
   521  				// get past the plan step if we have invalid variable values,
   522  				// and during planning we'll always have other source types.
   523  				ValueFromPlan,
   524  				tfdiags.SourceRange{},
   525  				`Invalid value for input variable: Unsuitable value for var.constrained_string_required set from outside of the configuration: string required.`,
   526  				`Required variable not set: Unsuitable value for var.constrained_string_required set from outside of the configuration: required variable may not be set to null.`,
   527  			},
   528  			{
   529  				ValueFromCaller,
   530  				tfdiags.SourceRange{},
   531  				`Invalid value for input variable: Unsuitable value for var.constrained_string_required set from outside of the configuration: string required.`,
   532  				`Required variable not set: Unsuitable value for var.constrained_string_required set from outside of the configuration: required variable may not be set to null.`,
   533  			},
   534  		}
   535  
   536  		for _, test := range tests {
   537  			t.Run(fmt.Sprintf("%s %s", test.SourceType, test.SourceRange.StartString()), func(t *testing.T) {
   538  				varAddr := addrs.InputVariable{Name: "constrained_string_required"}.Absolute(addrs.RootModuleInstance)
   539  				varCfg := variableConfigs[varAddr.Variable.Name]
   540  				t.Run("type error", func(t *testing.T) {
   541  					rawVal := &InputValue{
   542  						Value:       cty.EmptyObjectVal,
   543  						SourceType:  test.SourceType,
   544  						SourceRange: test.SourceRange,
   545  					}
   546  
   547  					_, diags := prepareFinalInputVariableValue(
   548  						varAddr, rawVal, varCfg,
   549  					)
   550  					if !diags.HasErrors() {
   551  						t.Fatalf("unexpected success; want error")
   552  					}
   553  
   554  					if got, want := diags.Err().Error(), test.WantTypeErr; got != want {
   555  						t.Errorf("wrong error\ngot:  %s\nwant: %s", got, want)
   556  					}
   557  				})
   558  				t.Run("null error", func(t *testing.T) {
   559  					rawVal := &InputValue{
   560  						Value:       cty.NullVal(cty.DynamicPseudoType),
   561  						SourceType:  test.SourceType,
   562  						SourceRange: test.SourceRange,
   563  					}
   564  
   565  					_, diags := prepareFinalInputVariableValue(
   566  						varAddr, rawVal, varCfg,
   567  					)
   568  					if !diags.HasErrors() {
   569  						t.Fatalf("unexpected success; want error")
   570  					}
   571  
   572  					if got, want := diags.Err().Error(), test.WantNullErr; got != want {
   573  						t.Errorf("wrong error\ngot:  %s\nwant: %s", got, want)
   574  					}
   575  				})
   576  			})
   577  		}
   578  	})
   579  
   580  	t.Run("SensitiveVariable error message variants, with source variants", func(t *testing.T) {
   581  		tests := []struct {
   582  			SourceType  ValueSourceType
   583  			SourceRange tfdiags.SourceRange
   584  			WantTypeErr string
   585  			HideSubject bool
   586  		}{
   587  			{
   588  				ValueFromUnknown,
   589  				tfdiags.SourceRange{},
   590  				"Invalid value for input variable: Unsuitable value for var.constrained_string_sensitive_required set from outside of the configuration: string required.",
   591  				false,
   592  			},
   593  			{
   594  				ValueFromConfig,
   595  				tfdiags.SourceRange{
   596  					Filename: "example.tfvars",
   597  					Start:    tfdiags.SourcePos(hcl.InitialPos),
   598  					End:      tfdiags.SourcePos(hcl.InitialPos),
   599  				},
   600  				`Invalid value for input variable: The given value is not suitable for var.constrained_string_sensitive_required, which is sensitive: string required. Invalid value defined at example.tfvars:1,1-1.`,
   601  				true,
   602  			},
   603  		}
   604  
   605  		for _, test := range tests {
   606  			t.Run(fmt.Sprintf("%s %s", test.SourceType, test.SourceRange.StartString()), func(t *testing.T) {
   607  				varAddr := addrs.InputVariable{Name: "constrained_string_sensitive_required"}.Absolute(addrs.RootModuleInstance)
   608  				varCfg := variableConfigs[varAddr.Variable.Name]
   609  				t.Run("type error", func(t *testing.T) {
   610  					rawVal := &InputValue{
   611  						Value:       cty.EmptyObjectVal,
   612  						SourceType:  test.SourceType,
   613  						SourceRange: test.SourceRange,
   614  					}
   615  
   616  					_, diags := prepareFinalInputVariableValue(
   617  						varAddr, rawVal, varCfg,
   618  					)
   619  					if !diags.HasErrors() {
   620  						t.Fatalf("unexpected success; want error")
   621  					}
   622  
   623  					if got, want := diags.Err().Error(), test.WantTypeErr; got != want {
   624  						t.Errorf("wrong error\ngot:  %s\nwant: %s", got, want)
   625  					}
   626  
   627  					if test.HideSubject {
   628  						if got, want := diags[0].Source().Subject.StartString(), test.SourceRange.StartString(); got == want {
   629  							t.Errorf("Subject start should have been hidden, but was %s", got)
   630  						}
   631  					}
   632  				})
   633  			})
   634  		}
   635  	})
   636  }
   637  
   638  // These tests cover the JSON syntax configuration edge case handling,
   639  // the background of which is described in detail in comments in the
   640  // evalVariableValidations function. Future versions of Terraform may
   641  // be able to remove this behaviour altogether.
   642  func TestEvalVariableValidations_jsonErrorMessageEdgeCase(t *testing.T) {
   643  	cfgSrc := `{
   644    "variable": {
   645      "valid": {
   646        "type": "string",
   647        "validation": {
   648          "condition": "${var.valid != \"bar\"}",
   649          "error_message": "Valid template string ${var.valid}"
   650        }
   651      },
   652      "invalid": {
   653        "type": "string",
   654        "validation": {
   655          "condition": "${var.invalid != \"bar\"}",
   656          "error_message": "Invalid template string ${"
   657        }
   658      }
   659    }
   660  }
   661  `
   662  	cfg := testModuleInline(t, map[string]string{
   663  		"main.tf.json": cfgSrc,
   664  	})
   665  	variableConfigs := cfg.Module.Variables
   666  
   667  	// Because we loaded our pseudo-module from a temporary file, the
   668  	// declaration source ranges will have unpredictable filenames. We'll
   669  	// fix that here just to make things easier below.
   670  	for _, vc := range variableConfigs {
   671  		vc.DeclRange.Filename = "main.tf.json"
   672  		for _, v := range vc.Validations {
   673  			v.DeclRange.Filename = "main.tf.json"
   674  		}
   675  	}
   676  
   677  	tests := []struct {
   678  		varName  string
   679  		given    cty.Value
   680  		wantErr  []string
   681  		wantWarn []string
   682  	}{
   683  		// Valid variable validation declaration, assigned value which passes
   684  		// the condition generates no diagnostics.
   685  		{
   686  			varName: "valid",
   687  			given:   cty.StringVal("foo"),
   688  		},
   689  		// Assigning a value which fails the condition generates an error
   690  		// message with the expression successfully evaluated.
   691  		{
   692  			varName: "valid",
   693  			given:   cty.StringVal("bar"),
   694  			wantErr: []string{
   695  				"Invalid value for variable",
   696  				"Valid template string bar",
   697  			},
   698  		},
   699  		// Invalid variable validation declaration due to an unparseable
   700  		// template string. Assigning a value which passes the condition
   701  		// results in a warning about the error message.
   702  		{
   703  			varName: "invalid",
   704  			given:   cty.StringVal("foo"),
   705  			wantWarn: []string{
   706  				"Validation error message expression is invalid",
   707  				"Missing expression; Expected the start of an expression, but found the end of the file.",
   708  			},
   709  		},
   710  		// Assigning a value which fails the condition generates an error
   711  		// message including the configured string interpreted as a literal
   712  		// value, and the same warning diagnostic as above.
   713  		{
   714  			varName: "invalid",
   715  			given:   cty.StringVal("bar"),
   716  			wantErr: []string{
   717  				"Invalid value for variable",
   718  				"Invalid template string ${",
   719  			},
   720  			wantWarn: []string{
   721  				"Validation error message expression is invalid",
   722  				"Missing expression; Expected the start of an expression, but found the end of the file.",
   723  			},
   724  		},
   725  	}
   726  
   727  	for _, test := range tests {
   728  		t.Run(fmt.Sprintf("%s %#v", test.varName, test.given), func(t *testing.T) {
   729  			varAddr := addrs.InputVariable{Name: test.varName}.Absolute(addrs.RootModuleInstance)
   730  			varCfg := variableConfigs[test.varName]
   731  			if varCfg == nil {
   732  				t.Fatalf("invalid variable name %q", test.varName)
   733  			}
   734  
   735  			// Build a mock context to allow the function under test to
   736  			// retrieve the variable value and evaluate the expressions
   737  			ctx := &MockEvalContext{}
   738  
   739  			// We need a minimal scope to allow basic functions to be passed to
   740  			// the HCL scope
   741  			ctx.EvaluationScopeScope = &lang.Scope{}
   742  			ctx.GetVariableValueFunc = func(addr addrs.AbsInputVariableInstance) cty.Value {
   743  				if got, want := addr.String(), varAddr.String(); got != want {
   744  					t.Errorf("incorrect argument to GetVariableValue: got %s, want %s", got, want)
   745  				}
   746  				return test.given
   747  			}
   748  
   749  			gotDiags := evalVariableValidations(
   750  				varAddr, varCfg, nil, ctx,
   751  			)
   752  
   753  			if len(test.wantErr) == 0 && len(test.wantWarn) == 0 {
   754  				if len(gotDiags) > 0 {
   755  					t.Errorf("no diags expected, got %s", gotDiags.Err().Error())
   756  				}
   757  			} else {
   758  			wantErrs:
   759  				for _, want := range test.wantErr {
   760  					for _, diag := range gotDiags {
   761  						if diag.Severity() != tfdiags.Error {
   762  							continue
   763  						}
   764  						desc := diag.Description()
   765  						if strings.Contains(desc.Summary, want) || strings.Contains(desc.Detail, want) {
   766  							continue wantErrs
   767  						}
   768  					}
   769  					t.Errorf("no error diagnostics found containing %q\ngot: %s", want, gotDiags.Err().Error())
   770  				}
   771  
   772  			wantWarns:
   773  				for _, want := range test.wantWarn {
   774  					for _, diag := range gotDiags {
   775  						if diag.Severity() != tfdiags.Warning {
   776  							continue
   777  						}
   778  						desc := diag.Description()
   779  						if strings.Contains(desc.Summary, want) || strings.Contains(desc.Detail, want) {
   780  							continue wantWarns
   781  						}
   782  					}
   783  					t.Errorf("no warning diagnostics found containing %q\ngot: %s", want, gotDiags.Err().Error())
   784  				}
   785  			}
   786  		})
   787  	}
   788  }
   789  
   790  func TestEvalVariableValidations_sensitiveValues(t *testing.T) {
   791  	cfgSrc := `
   792  variable "foo" {
   793    type      = string
   794    sensitive = true
   795    default   = "boop"
   796  
   797    validation {
   798      condition     = length(var.foo) == 4
   799  	error_message = "Foo must be 4 characters, not ${length(var.foo)}"
   800    }
   801  }
   802  
   803  variable "bar" {
   804    type      = string
   805    sensitive = true
   806    default   = "boop"
   807  
   808    validation {
   809      condition     = length(var.bar) == 4
   810  	error_message = "Bar must be 4 characters, not ${nonsensitive(length(var.bar))}."
   811    }
   812  }
   813  `
   814  	cfg := testModuleInline(t, map[string]string{
   815  		"main.tf": cfgSrc,
   816  	})
   817  	variableConfigs := cfg.Module.Variables
   818  
   819  	// Because we loaded our pseudo-module from a temporary file, the
   820  	// declaration source ranges will have unpredictable filenames. We'll
   821  	// fix that here just to make things easier below.
   822  	for _, vc := range variableConfigs {
   823  		vc.DeclRange.Filename = "main.tf"
   824  		for _, v := range vc.Validations {
   825  			v.DeclRange.Filename = "main.tf"
   826  		}
   827  	}
   828  
   829  	tests := []struct {
   830  		varName string
   831  		given   cty.Value
   832  		wantErr []string
   833  	}{
   834  		// Validations pass on a sensitive variable with an error message which
   835  		// would generate a sensitive value
   836  		{
   837  			varName: "foo",
   838  			given:   cty.StringVal("boop"),
   839  		},
   840  		// Assigning a value which fails the condition generates a sensitive
   841  		// error message, which is elided and generates another error
   842  		{
   843  			varName: "foo",
   844  			given:   cty.StringVal("bap"),
   845  			wantErr: []string{
   846  				"Invalid value for variable",
   847  				"The error message included a sensitive value, so it will not be displayed.",
   848  				"Error message refers to sensitive values",
   849  			},
   850  		},
   851  		// Validations pass on a sensitive variable with a correctly defined
   852  		// error message
   853  		{
   854  			varName: "bar",
   855  			given:   cty.StringVal("boop"),
   856  		},
   857  		// Assigning a value which fails the condition generates a nonsensitive
   858  		// error message, which is displayed
   859  		{
   860  			varName: "bar",
   861  			given:   cty.StringVal("bap"),
   862  			wantErr: []string{
   863  				"Invalid value for variable",
   864  				"Bar must be 4 characters, not 3.",
   865  			},
   866  		},
   867  	}
   868  
   869  	for _, test := range tests {
   870  		t.Run(fmt.Sprintf("%s %#v", test.varName, test.given), func(t *testing.T) {
   871  			varAddr := addrs.InputVariable{Name: test.varName}.Absolute(addrs.RootModuleInstance)
   872  			varCfg := variableConfigs[test.varName]
   873  			if varCfg == nil {
   874  				t.Fatalf("invalid variable name %q", test.varName)
   875  			}
   876  
   877  			// Build a mock context to allow the function under test to
   878  			// retrieve the variable value and evaluate the expressions
   879  			ctx := &MockEvalContext{}
   880  
   881  			// We need a minimal scope to allow basic functions to be passed to
   882  			// the HCL scope
   883  			ctx.EvaluationScopeScope = &lang.Scope{}
   884  			ctx.GetVariableValueFunc = func(addr addrs.AbsInputVariableInstance) cty.Value {
   885  				if got, want := addr.String(), varAddr.String(); got != want {
   886  					t.Errorf("incorrect argument to GetVariableValue: got %s, want %s", got, want)
   887  				}
   888  				if varCfg.Sensitive {
   889  					return test.given.Mark(marks.Sensitive)
   890  				} else {
   891  					return test.given
   892  				}
   893  			}
   894  
   895  			gotDiags := evalVariableValidations(
   896  				varAddr, varCfg, nil, ctx,
   897  			)
   898  
   899  			if len(test.wantErr) == 0 {
   900  				if len(gotDiags) > 0 {
   901  					t.Errorf("no diags expected, got %s", gotDiags.Err().Error())
   902  				}
   903  			} else {
   904  			wantErrs:
   905  				for _, want := range test.wantErr {
   906  					for _, diag := range gotDiags {
   907  						if diag.Severity() != tfdiags.Error {
   908  							continue
   909  						}
   910  						desc := diag.Description()
   911  						if strings.Contains(desc.Summary, want) || strings.Contains(desc.Detail, want) {
   912  							continue wantErrs
   913  						}
   914  					}
   915  					t.Errorf("no error diagnostics found containing %q\ngot: %s", want, gotDiags.Err().Error())
   916  				}
   917  			}
   918  		})
   919  	}
   920  }