github.com/opentofu/opentofu@v1.7.1/internal/tofu/eval_variable_test.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package tofu
     7  
     8  import (
     9  	"fmt"
    10  	"strings"
    11  	"testing"
    12  
    13  	"github.com/hashicorp/hcl/v2"
    14  	"github.com/zclconf/go-cty/cty"
    15  
    16  	"github.com/opentofu/opentofu/internal/addrs"
    17  	"github.com/opentofu/opentofu/internal/checks"
    18  	"github.com/opentofu/opentofu/internal/lang"
    19  	"github.com/opentofu/opentofu/internal/lang/marks"
    20  	"github.com/opentofu/opentofu/internal/tfdiags"
    21  )
    22  
    23  func TestPrepareFinalInputVariableValue(t *testing.T) {
    24  	// This is just a concise way to define a bunch of *configs.Variable
    25  	// objects to use in our tests below. We're only going to decode this
    26  	// config, not fully evaluate it.
    27  	cfgSrc := `
    28  		variable "nullable_required" {
    29  		}
    30  		variable "nullable_optional_default_string" {
    31  			default = "hello"
    32  		}
    33  		variable "nullable_optional_default_null" {
    34  			default = null
    35  		}
    36  		variable "constrained_string_nullable_required" {
    37  			type = string
    38  		}
    39  		variable "constrained_string_nullable_optional_default_string" {
    40  			type    = string
    41  			default = "hello"
    42  		}
    43  		variable "constrained_string_nullable_optional_default_bool" {
    44  			type    = string
    45  			default = true
    46  		}
    47  		variable "constrained_string_nullable_optional_default_null" {
    48  			type    = string
    49  			default = null
    50  		}
    51  		variable "required" {
    52  			nullable = false
    53  		}
    54  		variable "optional_default_string" {
    55  			nullable = false
    56  			default  = "hello"
    57  		}
    58  		variable "constrained_string_required" {
    59  			nullable = false
    60  			type     = string
    61  		}
    62  		variable "constrained_string_optional_default_string" {
    63  			nullable = false
    64  			type     = string
    65  			default  = "hello"
    66  		}
    67  		variable "constrained_string_optional_default_bool" {
    68  			nullable = false
    69  			type     = string
    70  			default  = true
    71  		}
    72  		variable "constrained_string_sensitive_required" {
    73  			sensitive = true
    74  			nullable  = false
    75  			type      = string
    76  		}
    77  		variable "complex_type_with_nested_default_optional" {
    78  			type = set(object({
    79  				name      = string
    80  				schedules = set(object({
    81  					name               = string
    82  					cold_storage_after = optional(number, 10)
    83  				}))
    84    			}))
    85  		}
    86  		variable "complex_type_with_nested_complex_types" {
    87  			type = object({
    88  				name                       = string
    89  				nested_object              = object({
    90  					name  = string
    91  					value = optional(string, "foo")
    92  				})
    93  				nested_object_with_default = optional(object({
    94  					name  = string
    95  					value = optional(string, "bar")
    96  				}), {
    97  					name = "nested_object_with_default"
    98  				})
    99  			})
   100  		}
   101  		// https://github.com/hashicorp/terraform/issues/32152
   102  		// This variable was originally added to test that optional attribute
   103  		// metadata is stripped from empty default collections. Essentially, you
   104  		// should be able to mix and match custom and default values for the
   105  		// optional_list attribute.
   106          variable "complex_type_with_empty_default_and_nested_optional" {
   107  			type = list(object({
   108  				name          = string
   109  				optional_list = optional(list(object({
   110  					string          = string
   111  					optional_string = optional(string)
   112  				})), [])
   113  			}))
   114          }
   115   		// https://github.com/hashicorp/terraform/issues/32160#issuecomment-1302783910
   116  		// These variables were added to test the specific use case from this
   117  		// GitHub comment.
   118  		variable "empty_object_with_optional_nested_object_with_optional_bool" {
   119  			type = object({
   120  				thing = optional(object({
   121  					flag = optional(bool, false)
   122  				}))
   123  			})
   124  			default = {}
   125  		}
   126  		variable "populated_object_with_optional_nested_object_with_optional_bool" {
   127  			type = object({
   128  				thing = optional(object({
   129  					flag = optional(bool, false)
   130  				}))
   131  			})
   132  			default = {
   133  				thing = {}
   134  			}
   135  		}
   136  		variable "empty_object_with_default_nested_object_with_optional_bool" {
   137  			type = object({
   138  				thing = optional(object({
   139  					flag = optional(bool, false)
   140  				}), {})
   141  			})
   142  			default = {}
   143  		}
   144  		// https://github.com/hashicorp/terraform/issues/32160
   145  		// This variable was originally added to test that optional objects do
   146  		// get created containing only their defaults. Instead they should be
   147  		// left empty. We do not expect nested_object to be created just because
   148  		// optional_string has a default value.
   149  		variable "object_with_nested_object_with_required_and_optional_attributes" {
   150  			type = object({
   151  				nested_object = optional(object({
   152  					string          = string
   153  					optional_string = optional(string, "optional")
   154  				}))
   155  			})
   156  		}
   157  		// https://github.com/hashicorp/terraform/issues/32157
   158  		// Similar to above, we want to see that merging combinations of the
   159  		// nested_object into a single collection doesn't crash because of
   160  		// inconsistent elements.
   161  		variable "list_with_nested_object_with_required_and_optional_attributes" {
   162  			type = list(object({
   163  				nested_object = optional(object({
   164  					string          = string
   165  					optional_string = optional(string, "optional")
   166  				}))
   167  			}))
   168  		}
   169  		// https://github.com/hashicorp/terraform/issues/32109
   170  		// This variable was originally introduced to test the behaviour of 
   171  		// the dynamic type constraint. You should be able to use the 'any' 
   172  		// constraint and introduce empty, null, and populated values into the
   173  		// list.
   174  		variable "list_with_nested_list_of_any" {
   175  			type = list(object({
   176  				a = string
   177  				b = optional(list(any))
   178  			}))
   179  			default = [
   180  				{
   181  					a = "a"
   182  				},
   183  				{
   184  					a = "b"
   185  					b = [1]
   186  				}
   187  			]
   188  		}
   189  		// https://github.com/hashicorp/terraform/issues/32396
   190  		// This variable was originally introduced to test the behaviour of the
   191          // dynamic type constraint. You should be able to set primitive types in
   192          // the list consistently.
   193          variable "list_with_nested_collections_dynamic_with_default" {
   194  			type = list(
   195  				object({
   196  					name = optional(string, "default")
   197  					taints = optional(list(map(any)), [])
   198  				})
   199  			)
   200  		}
   201          // https://github.com/hashicorp/terraform/issues/32752
   202  		// This variable was introduced to make sure the evaluation doesn't 
   203          // crash even when the types are wrong.
   204          variable "invalid_nested_type" {
   205              type = map(
   206                  object({
   207  					rules = map(
   208  						object({
   209  							destination_addresses = optional(list(string), [])
   210  						})
   211  					)
   212                  })
   213              )
   214  			default = {}
   215          }
   216  	`
   217  	cfg := testModuleInline(t, map[string]string{
   218  		"main.tf": cfgSrc,
   219  	})
   220  	variableConfigs := cfg.Module.Variables
   221  
   222  	// Because we loaded our pseudo-module from a temporary file, the
   223  	// declaration source ranges will have unpredictable filenames. We'll
   224  	// fix that here just to make things easier below.
   225  	for _, vc := range variableConfigs {
   226  		vc.DeclRange.Filename = "main.tf"
   227  	}
   228  
   229  	tests := []struct {
   230  		varName string
   231  		given   cty.Value
   232  		want    cty.Value
   233  		wantErr string
   234  	}{
   235  		// nullable_required
   236  		{
   237  			"nullable_required",
   238  			cty.NilVal,
   239  			cty.UnknownVal(cty.DynamicPseudoType),
   240  			`Required variable not set: The variable "nullable_required" is required, but is not set.`,
   241  		},
   242  		{
   243  			"nullable_required",
   244  			cty.NullVal(cty.DynamicPseudoType),
   245  			cty.NullVal(cty.DynamicPseudoType),
   246  			``, // "required" for a nullable variable means only that it must be set, even if it's set to null
   247  		},
   248  		{
   249  			"nullable_required",
   250  			cty.StringVal("ahoy"),
   251  			cty.StringVal("ahoy"),
   252  			``,
   253  		},
   254  		{
   255  			"nullable_required",
   256  			cty.UnknownVal(cty.String),
   257  			cty.UnknownVal(cty.String),
   258  			``,
   259  		},
   260  
   261  		// nullable_optional_default_string
   262  		{
   263  			"nullable_optional_default_string",
   264  			cty.NilVal,
   265  			cty.StringVal("hello"), // the declared default value
   266  			``,
   267  		},
   268  		{
   269  			"nullable_optional_default_string",
   270  			cty.NullVal(cty.DynamicPseudoType),
   271  			cty.NullVal(cty.DynamicPseudoType), // nullable variables can be really set to null, masking the default
   272  			``,
   273  		},
   274  		{
   275  			"nullable_optional_default_string",
   276  			cty.StringVal("ahoy"),
   277  			cty.StringVal("ahoy"),
   278  			``,
   279  		},
   280  		{
   281  			"nullable_optional_default_string",
   282  			cty.UnknownVal(cty.String),
   283  			cty.UnknownVal(cty.String),
   284  			``,
   285  		},
   286  
   287  		// nullable_optional_default_null
   288  		{
   289  			"nullable_optional_default_null",
   290  			cty.NilVal,
   291  			cty.NullVal(cty.DynamicPseudoType), // the declared default value
   292  			``,
   293  		},
   294  		{
   295  			"nullable_optional_default_null",
   296  			cty.NullVal(cty.String),
   297  			cty.NullVal(cty.String), // nullable variables can be really set to null, masking the default
   298  			``,
   299  		},
   300  		{
   301  			"nullable_optional_default_null",
   302  			cty.StringVal("ahoy"),
   303  			cty.StringVal("ahoy"),
   304  			``,
   305  		},
   306  		{
   307  			"nullable_optional_default_null",
   308  			cty.UnknownVal(cty.String),
   309  			cty.UnknownVal(cty.String),
   310  			``,
   311  		},
   312  
   313  		// constrained_string_nullable_required
   314  		{
   315  			"constrained_string_nullable_required",
   316  			cty.NilVal,
   317  			cty.UnknownVal(cty.String),
   318  			`Required variable not set: The variable "constrained_string_nullable_required" is required, but is not set.`,
   319  		},
   320  		{
   321  			"constrained_string_nullable_required",
   322  			cty.NullVal(cty.DynamicPseudoType),
   323  			cty.NullVal(cty.String), // the null value still gets converted to match the type constraint
   324  			``,                      // "required" for a nullable variable means only that it must be set, even if it's set to null
   325  		},
   326  		{
   327  			"constrained_string_nullable_required",
   328  			cty.StringVal("ahoy"),
   329  			cty.StringVal("ahoy"),
   330  			``,
   331  		},
   332  		{
   333  			"constrained_string_nullable_required",
   334  			cty.UnknownVal(cty.String),
   335  			cty.UnknownVal(cty.String),
   336  			``,
   337  		},
   338  
   339  		// constrained_string_nullable_optional_default_string
   340  		{
   341  			"constrained_string_nullable_optional_default_string",
   342  			cty.NilVal,
   343  			cty.StringVal("hello"), // the declared default value
   344  			``,
   345  		},
   346  		{
   347  			"constrained_string_nullable_optional_default_string",
   348  			cty.NullVal(cty.DynamicPseudoType),
   349  			cty.NullVal(cty.String), // nullable variables can be really set to null, masking the default
   350  			``,
   351  		},
   352  		{
   353  			"constrained_string_nullable_optional_default_string",
   354  			cty.StringVal("ahoy"),
   355  			cty.StringVal("ahoy"),
   356  			``,
   357  		},
   358  		{
   359  			"constrained_string_nullable_optional_default_string",
   360  			cty.UnknownVal(cty.String),
   361  			cty.UnknownVal(cty.String),
   362  			``,
   363  		},
   364  
   365  		// constrained_string_nullable_optional_default_bool
   366  		{
   367  			"constrained_string_nullable_optional_default_bool",
   368  			cty.NilVal,
   369  			cty.StringVal("true"), // the declared default value, automatically converted to match type constraint
   370  			``,
   371  		},
   372  		{
   373  			"constrained_string_nullable_optional_default_bool",
   374  			cty.NullVal(cty.DynamicPseudoType),
   375  			cty.NullVal(cty.String), // nullable variables can be really set to null, masking the default
   376  			``,
   377  		},
   378  		{
   379  			"constrained_string_nullable_optional_default_bool",
   380  			cty.StringVal("ahoy"),
   381  			cty.StringVal("ahoy"),
   382  			``,
   383  		},
   384  		{
   385  			"constrained_string_nullable_optional_default_bool",
   386  			cty.UnknownVal(cty.String),
   387  			cty.UnknownVal(cty.String),
   388  			``,
   389  		},
   390  
   391  		// constrained_string_nullable_optional_default_null
   392  		{
   393  			"constrained_string_nullable_optional_default_null",
   394  			cty.NilVal,
   395  			cty.NullVal(cty.String),
   396  			``,
   397  		},
   398  		{
   399  			"constrained_string_nullable_optional_default_null",
   400  			cty.NullVal(cty.DynamicPseudoType),
   401  			cty.NullVal(cty.String),
   402  			``,
   403  		},
   404  		{
   405  			"constrained_string_nullable_optional_default_null",
   406  			cty.StringVal("ahoy"),
   407  			cty.StringVal("ahoy"),
   408  			``,
   409  		},
   410  		{
   411  			"constrained_string_nullable_optional_default_null",
   412  			cty.UnknownVal(cty.String),
   413  			cty.UnknownVal(cty.String),
   414  			``,
   415  		},
   416  
   417  		// required
   418  		{
   419  			"required",
   420  			cty.NilVal,
   421  			cty.UnknownVal(cty.DynamicPseudoType),
   422  			`Required variable not set: The variable "required" is required, but is not set.`,
   423  		},
   424  		{
   425  			"required",
   426  			cty.NullVal(cty.DynamicPseudoType),
   427  			cty.UnknownVal(cty.DynamicPseudoType),
   428  			`Required variable not set: Unsuitable value for var.required set from outside of the configuration: required variable may not be set to null.`,
   429  		},
   430  		{
   431  			"required",
   432  			cty.StringVal("ahoy"),
   433  			cty.StringVal("ahoy"),
   434  			``,
   435  		},
   436  		{
   437  			"required",
   438  			cty.UnknownVal(cty.String),
   439  			cty.UnknownVal(cty.String),
   440  			``,
   441  		},
   442  
   443  		// optional_default_string
   444  		{
   445  			"optional_default_string",
   446  			cty.NilVal,
   447  			cty.StringVal("hello"), // the declared default value
   448  			``,
   449  		},
   450  		{
   451  			"optional_default_string",
   452  			cty.NullVal(cty.DynamicPseudoType),
   453  			cty.StringVal("hello"), // the declared default value
   454  			``,
   455  		},
   456  		{
   457  			"optional_default_string",
   458  			cty.StringVal("ahoy"),
   459  			cty.StringVal("ahoy"),
   460  			``,
   461  		},
   462  		{
   463  			"optional_default_string",
   464  			cty.UnknownVal(cty.String),
   465  			cty.UnknownVal(cty.String),
   466  			``,
   467  		},
   468  
   469  		// constrained_string_required
   470  		{
   471  			"constrained_string_required",
   472  			cty.NilVal,
   473  			cty.UnknownVal(cty.String),
   474  			`Required variable not set: The variable "constrained_string_required" is required, but is not set.`,
   475  		},
   476  		{
   477  			"constrained_string_required",
   478  			cty.NullVal(cty.DynamicPseudoType),
   479  			cty.UnknownVal(cty.String),
   480  			`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.`,
   481  		},
   482  		{
   483  			"constrained_string_required",
   484  			cty.StringVal("ahoy"),
   485  			cty.StringVal("ahoy"),
   486  			``,
   487  		},
   488  		{
   489  			"constrained_string_required",
   490  			cty.UnknownVal(cty.String),
   491  			cty.UnknownVal(cty.String),
   492  			``,
   493  		},
   494  
   495  		// constrained_string_optional_default_string
   496  		{
   497  			"constrained_string_optional_default_string",
   498  			cty.NilVal,
   499  			cty.StringVal("hello"), // the declared default value
   500  			``,
   501  		},
   502  		{
   503  			"constrained_string_optional_default_string",
   504  			cty.NullVal(cty.DynamicPseudoType),
   505  			cty.StringVal("hello"), // the declared default value
   506  			``,
   507  		},
   508  		{
   509  			"constrained_string_optional_default_string",
   510  			cty.StringVal("ahoy"),
   511  			cty.StringVal("ahoy"),
   512  			``,
   513  		},
   514  		{
   515  			"constrained_string_optional_default_string",
   516  			cty.UnknownVal(cty.String),
   517  			cty.UnknownVal(cty.String),
   518  			``,
   519  		},
   520  
   521  		// constrained_string_optional_default_bool
   522  		{
   523  			"constrained_string_optional_default_bool",
   524  			cty.NilVal,
   525  			cty.StringVal("true"), // the declared default value, automatically converted to match type constraint
   526  			``,
   527  		},
   528  		{
   529  			"constrained_string_optional_default_bool",
   530  			cty.NullVal(cty.DynamicPseudoType),
   531  			cty.StringVal("true"), // the declared default value, automatically converted to match type constraint
   532  			``,
   533  		},
   534  		{
   535  			"constrained_string_optional_default_bool",
   536  			cty.StringVal("ahoy"),
   537  			cty.StringVal("ahoy"),
   538  			``,
   539  		},
   540  		{
   541  			"constrained_string_optional_default_bool",
   542  			cty.UnknownVal(cty.String),
   543  			cty.UnknownVal(cty.String),
   544  			``,
   545  		},
   546  		{
   547  			"list_with_nested_collections_dynamic_with_default",
   548  			cty.TupleVal([]cty.Value{
   549  				cty.ObjectVal(map[string]cty.Value{
   550  					"name": cty.StringVal("default"),
   551  				}),
   552  				cty.ObjectVal(map[string]cty.Value{
   553  					"name": cty.StringVal("complex"),
   554  					"taints": cty.ListVal([]cty.Value{
   555  						cty.MapVal(map[string]cty.Value{
   556  							"key":   cty.StringVal("my_key"),
   557  							"value": cty.StringVal("my_value"),
   558  						}),
   559  					}),
   560  				}),
   561  			}),
   562  			cty.ListVal([]cty.Value{
   563  				cty.ObjectVal(map[string]cty.Value{
   564  					"name":   cty.StringVal("default"),
   565  					"taints": cty.ListValEmpty(cty.Map(cty.String)),
   566  				}),
   567  				cty.ObjectVal(map[string]cty.Value{
   568  					"name": cty.StringVal("complex"),
   569  					"taints": cty.ListVal([]cty.Value{
   570  						cty.MapVal(map[string]cty.Value{
   571  							"key":   cty.StringVal("my_key"),
   572  							"value": cty.StringVal("my_value"),
   573  						}),
   574  					}),
   575  				}),
   576  			}),
   577  			``,
   578  		},
   579  
   580  		// complex types
   581  
   582  		{
   583  			"complex_type_with_nested_default_optional",
   584  			cty.SetVal([]cty.Value{
   585  				cty.ObjectVal(map[string]cty.Value{
   586  					"name": cty.StringVal("test1"),
   587  					"schedules": cty.SetVal([]cty.Value{
   588  						cty.MapVal(map[string]cty.Value{
   589  							"name": cty.StringVal("daily"),
   590  						}),
   591  					}),
   592  				}),
   593  				cty.ObjectVal(map[string]cty.Value{
   594  					"name": cty.StringVal("test2"),
   595  					"schedules": cty.SetVal([]cty.Value{
   596  						cty.MapVal(map[string]cty.Value{
   597  							"name": cty.StringVal("daily"),
   598  						}),
   599  						cty.MapVal(map[string]cty.Value{
   600  							"name":               cty.StringVal("weekly"),
   601  							"cold_storage_after": cty.StringVal("0"),
   602  						}),
   603  					}),
   604  				}),
   605  			}),
   606  			cty.SetVal([]cty.Value{
   607  				cty.ObjectVal(map[string]cty.Value{
   608  					"name": cty.StringVal("test1"),
   609  					"schedules": cty.SetVal([]cty.Value{
   610  						cty.ObjectVal(map[string]cty.Value{
   611  							"name":               cty.StringVal("daily"),
   612  							"cold_storage_after": cty.NumberIntVal(10),
   613  						}),
   614  					}),
   615  				}),
   616  				cty.ObjectVal(map[string]cty.Value{
   617  					"name": cty.StringVal("test2"),
   618  					"schedules": cty.SetVal([]cty.Value{
   619  						cty.ObjectVal(map[string]cty.Value{
   620  							"name":               cty.StringVal("daily"),
   621  							"cold_storage_after": cty.NumberIntVal(10),
   622  						}),
   623  						cty.ObjectVal(map[string]cty.Value{
   624  							"name":               cty.StringVal("weekly"),
   625  							"cold_storage_after": cty.NumberIntVal(0),
   626  						}),
   627  					}),
   628  				}),
   629  			}),
   630  			``,
   631  		},
   632  		{
   633  			"complex_type_with_nested_complex_types",
   634  			cty.ObjectVal(map[string]cty.Value{
   635  				"name": cty.StringVal("object"),
   636  				"nested_object": cty.ObjectVal(map[string]cty.Value{
   637  					"name": cty.StringVal("nested_object"),
   638  				}),
   639  			}),
   640  			cty.ObjectVal(map[string]cty.Value{
   641  				"name": cty.StringVal("object"),
   642  				"nested_object": cty.ObjectVal(map[string]cty.Value{
   643  					"name":  cty.StringVal("nested_object"),
   644  					"value": cty.StringVal("foo"),
   645  				}),
   646  				"nested_object_with_default": cty.ObjectVal(map[string]cty.Value{
   647  					"name":  cty.StringVal("nested_object_with_default"),
   648  					"value": cty.StringVal("bar"),
   649  				}),
   650  			}),
   651  			``,
   652  		},
   653  		{
   654  			"complex_type_with_empty_default_and_nested_optional",
   655  			cty.ListVal([]cty.Value{
   656  				cty.ObjectVal(map[string]cty.Value{
   657  					"name": cty.StringVal("abc"),
   658  					"optional_list": cty.ListVal([]cty.Value{
   659  						cty.ObjectVal(map[string]cty.Value{
   660  							"string":          cty.StringVal("child"),
   661  							"optional_string": cty.NullVal(cty.String),
   662  						}),
   663  					}),
   664  				}),
   665  				cty.ObjectVal(map[string]cty.Value{
   666  					"name": cty.StringVal("def"),
   667  					"optional_list": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{
   668  						"string":          cty.String,
   669  						"optional_string": cty.String,
   670  					}))),
   671  				}),
   672  			}),
   673  			cty.ListVal([]cty.Value{
   674  				cty.ObjectVal(map[string]cty.Value{
   675  					"name": cty.StringVal("abc"),
   676  					"optional_list": cty.ListVal([]cty.Value{
   677  						cty.ObjectVal(map[string]cty.Value{
   678  							"string":          cty.StringVal("child"),
   679  							"optional_string": cty.NullVal(cty.String),
   680  						}),
   681  					}),
   682  				}),
   683  				cty.ObjectVal(map[string]cty.Value{
   684  					"name": cty.StringVal("def"),
   685  					"optional_list": cty.ListValEmpty(cty.Object(map[string]cty.Type{
   686  						"string":          cty.String,
   687  						"optional_string": cty.String,
   688  					})),
   689  				}),
   690  			}),
   691  			``,
   692  		},
   693  		{
   694  			"object_with_nested_object_with_required_and_optional_attributes",
   695  			cty.EmptyObjectVal,
   696  			cty.ObjectVal(map[string]cty.Value{
   697  				"nested_object": cty.NullVal(cty.Object(map[string]cty.Type{
   698  					"string":          cty.String,
   699  					"optional_string": cty.String,
   700  				})),
   701  			}),
   702  			``,
   703  		},
   704  		{
   705  			"empty_object_with_optional_nested_object_with_optional_bool",
   706  			cty.NilVal,
   707  			cty.ObjectVal(map[string]cty.Value{
   708  				"thing": cty.NullVal(cty.Object(map[string]cty.Type{
   709  					"flag": cty.Bool,
   710  				})),
   711  			}),
   712  			``,
   713  		},
   714  		{
   715  			"populated_object_with_optional_nested_object_with_optional_bool",
   716  			cty.NilVal,
   717  			cty.ObjectVal(map[string]cty.Value{
   718  				"thing": cty.ObjectVal(map[string]cty.Value{
   719  					"flag": cty.False,
   720  				}),
   721  			}),
   722  			``,
   723  		},
   724  		{
   725  			"empty_object_with_default_nested_object_with_optional_bool",
   726  			cty.NilVal,
   727  			cty.ObjectVal(map[string]cty.Value{
   728  				"thing": cty.ObjectVal(map[string]cty.Value{
   729  					"flag": cty.False,
   730  				}),
   731  			}),
   732  			``,
   733  		},
   734  		{
   735  			"list_with_nested_object_with_required_and_optional_attributes",
   736  			cty.ListVal([]cty.Value{
   737  				cty.ObjectVal(map[string]cty.Value{
   738  					"nested_object": cty.ObjectVal(map[string]cty.Value{
   739  						"string":          cty.StringVal("string"),
   740  						"optional_string": cty.NullVal(cty.String),
   741  					}),
   742  				}),
   743  				cty.ObjectVal(map[string]cty.Value{
   744  					"nested_object": cty.NullVal(cty.Object(map[string]cty.Type{
   745  						"string":          cty.String,
   746  						"optional_string": cty.String,
   747  					})),
   748  				}),
   749  			}),
   750  			cty.ListVal([]cty.Value{
   751  				cty.ObjectVal(map[string]cty.Value{
   752  					"nested_object": cty.ObjectVal(map[string]cty.Value{
   753  						"string":          cty.StringVal("string"),
   754  						"optional_string": cty.StringVal("optional"),
   755  					}),
   756  				}),
   757  				cty.ObjectVal(map[string]cty.Value{
   758  					"nested_object": cty.NullVal(cty.Object(map[string]cty.Type{
   759  						"string":          cty.String,
   760  						"optional_string": cty.String,
   761  					})),
   762  				}),
   763  			}),
   764  			``,
   765  		},
   766  		{
   767  			"list_with_nested_list_of_any",
   768  			cty.NilVal,
   769  			cty.ListVal([]cty.Value{
   770  				cty.ObjectVal(map[string]cty.Value{
   771  					"a": cty.StringVal("a"),
   772  					"b": cty.NullVal(cty.List(cty.Number)),
   773  				}),
   774  				cty.ObjectVal(map[string]cty.Value{
   775  					"a": cty.StringVal("b"),
   776  					"b": cty.ListVal([]cty.Value{
   777  						cty.NumberIntVal(1),
   778  					}),
   779  				}),
   780  			}),
   781  			``,
   782  		},
   783  		{
   784  			"list_with_nested_collections_dynamic_with_default",
   785  			cty.TupleVal([]cty.Value{
   786  				cty.ObjectVal(map[string]cty.Value{
   787  					"name": cty.StringVal("default"),
   788  				}),
   789  				cty.ObjectVal(map[string]cty.Value{
   790  					"name": cty.StringVal("complex"),
   791  					"taints": cty.ListVal([]cty.Value{
   792  						cty.MapVal(map[string]cty.Value{
   793  							"key":   cty.StringVal("my_key"),
   794  							"value": cty.StringVal("my_value"),
   795  						}),
   796  					}),
   797  				}),
   798  			}),
   799  			cty.ListVal([]cty.Value{
   800  				cty.ObjectVal(map[string]cty.Value{
   801  					"name":   cty.StringVal("default"),
   802  					"taints": cty.ListValEmpty(cty.Map(cty.String)),
   803  				}),
   804  				cty.ObjectVal(map[string]cty.Value{
   805  					"name": cty.StringVal("complex"),
   806  					"taints": cty.ListVal([]cty.Value{
   807  						cty.MapVal(map[string]cty.Value{
   808  							"key":   cty.StringVal("my_key"),
   809  							"value": cty.StringVal("my_value"),
   810  						}),
   811  					}),
   812  				}),
   813  			}),
   814  			``,
   815  		},
   816  		{
   817  			"invalid_nested_type",
   818  			cty.MapVal(map[string]cty.Value{
   819  				"mysql": cty.ObjectVal(map[string]cty.Value{
   820  					"rules": cty.ObjectVal(map[string]cty.Value{
   821  						"destination_addresses": cty.ListVal([]cty.Value{cty.StringVal("192.168.0.1")}),
   822  					}),
   823  				}),
   824  			}),
   825  			cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{
   826  				"rules": cty.Map(cty.Object(map[string]cty.Type{
   827  					"destination_addresses": cty.List(cty.String),
   828  				})),
   829  			}))),
   830  			`Invalid value for input variable: Unsuitable value for var.invalid_nested_type set from outside of the configuration: incorrect map element type: attribute "rules": element "destination_addresses": object required.`,
   831  		},
   832  
   833  		// sensitive
   834  		{
   835  			"constrained_string_sensitive_required",
   836  			cty.UnknownVal(cty.String),
   837  			cty.UnknownVal(cty.String),
   838  			``,
   839  		},
   840  	}
   841  
   842  	for _, test := range tests {
   843  		t.Run(fmt.Sprintf("%s %#v", test.varName, test.given), func(t *testing.T) {
   844  			varAddr := addrs.InputVariable{Name: test.varName}.Absolute(addrs.RootModuleInstance)
   845  			varCfg := variableConfigs[test.varName]
   846  			if varCfg == nil {
   847  				t.Fatalf("invalid variable name %q", test.varName)
   848  			}
   849  
   850  			t.Logf(
   851  				"test case\nvariable:    %s\nconstraint:  %#v\ndefault:     %#v\nnullable:    %#v\ngiven value: %#v",
   852  				varAddr,
   853  				varCfg.Type,
   854  				varCfg.Default,
   855  				varCfg.Nullable,
   856  				test.given,
   857  			)
   858  
   859  			rawVal := &InputValue{
   860  				Value:      test.given,
   861  				SourceType: ValueFromCaller,
   862  			}
   863  
   864  			got, diags := prepareFinalInputVariableValue(
   865  				varAddr, rawVal, varCfg,
   866  			)
   867  
   868  			if test.wantErr != "" {
   869  				if !diags.HasErrors() {
   870  					t.Errorf("unexpected success\nwant error: %s", test.wantErr)
   871  				} else if got, want := diags.Err().Error(), test.wantErr; got != want {
   872  					t.Errorf("wrong error\ngot:  %s\nwant: %s", got, want)
   873  				}
   874  			} else {
   875  				if diags.HasErrors() {
   876  					t.Errorf("unexpected error\ngot: %s", diags.Err().Error())
   877  				}
   878  			}
   879  
   880  			// NOTE: should still have returned some reasonable value even if there was an error
   881  			if !test.want.RawEquals(got) {
   882  				t.Fatalf("wrong result\ngot:  %#v\nwant: %#v", got, test.want)
   883  			}
   884  		})
   885  	}
   886  
   887  	t.Run("SourceType error message variants", func(t *testing.T) {
   888  		tests := []struct {
   889  			SourceType  ValueSourceType
   890  			SourceRange tfdiags.SourceRange
   891  			WantTypeErr string
   892  			WantNullErr string
   893  		}{
   894  			{
   895  				ValueFromUnknown,
   896  				tfdiags.SourceRange{},
   897  				`Invalid value for input variable: Unsuitable value for var.constrained_string_required set from outside of the configuration: string required.`,
   898  				`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.`,
   899  			},
   900  			{
   901  				ValueFromConfig,
   902  				tfdiags.SourceRange{
   903  					Filename: "example.tf",
   904  					Start:    tfdiags.SourcePos(hcl.InitialPos),
   905  					End:      tfdiags.SourcePos(hcl.InitialPos),
   906  				},
   907  				`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.`,
   908  				`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.`,
   909  			},
   910  			{
   911  				ValueFromAutoFile,
   912  				tfdiags.SourceRange{
   913  					Filename: "example.auto.tfvars",
   914  					Start:    tfdiags.SourcePos(hcl.InitialPos),
   915  					End:      tfdiags.SourcePos(hcl.InitialPos),
   916  				},
   917  				`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.`,
   918  				`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.`,
   919  			},
   920  			{
   921  				ValueFromNamedFile,
   922  				tfdiags.SourceRange{
   923  					Filename: "example.tfvars",
   924  					Start:    tfdiags.SourcePos(hcl.InitialPos),
   925  					End:      tfdiags.SourcePos(hcl.InitialPos),
   926  				},
   927  				`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.`,
   928  				`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.`,
   929  			},
   930  			{
   931  				ValueFromCLIArg,
   932  				tfdiags.SourceRange{},
   933  				`Invalid value for input variable: Unsuitable value for var.constrained_string_required set using -var="constrained_string_required=...": string required.`,
   934  				`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.`,
   935  			},
   936  			{
   937  				ValueFromEnvVar,
   938  				tfdiags.SourceRange{},
   939  				`Invalid value for input variable: Unsuitable value for var.constrained_string_required set using the TF_VAR_constrained_string_required environment variable: string required.`,
   940  				`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.`,
   941  			},
   942  			{
   943  				ValueFromInput,
   944  				tfdiags.SourceRange{},
   945  				`Invalid value for input variable: Unsuitable value for var.constrained_string_required set using an interactive prompt: string required.`,
   946  				`Required variable not set: Unsuitable value for var.constrained_string_required set using an interactive prompt: required variable may not be set to null.`,
   947  			},
   948  			{
   949  				// NOTE: This isn't actually a realistic case for this particular
   950  				// function, because if we have a value coming from a plan then
   951  				// we must be in the apply step, and we shouldn't be able to
   952  				// get past the plan step if we have invalid variable values,
   953  				// and during planning we'll always have other source types.
   954  				ValueFromPlan,
   955  				tfdiags.SourceRange{},
   956  				`Invalid value for input variable: Unsuitable value for var.constrained_string_required set from outside of the configuration: string required.`,
   957  				`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.`,
   958  			},
   959  			{
   960  				ValueFromCaller,
   961  				tfdiags.SourceRange{},
   962  				`Invalid value for input variable: Unsuitable value for var.constrained_string_required set from outside of the configuration: string required.`,
   963  				`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.`,
   964  			},
   965  		}
   966  
   967  		for _, test := range tests {
   968  			t.Run(fmt.Sprintf("%s %s", test.SourceType, test.SourceRange.StartString()), func(t *testing.T) {
   969  				varAddr := addrs.InputVariable{Name: "constrained_string_required"}.Absolute(addrs.RootModuleInstance)
   970  				varCfg := variableConfigs[varAddr.Variable.Name]
   971  				t.Run("type error", func(t *testing.T) {
   972  					rawVal := &InputValue{
   973  						Value:       cty.EmptyObjectVal,
   974  						SourceType:  test.SourceType,
   975  						SourceRange: test.SourceRange,
   976  					}
   977  
   978  					_, diags := prepareFinalInputVariableValue(
   979  						varAddr, rawVal, varCfg,
   980  					)
   981  					if !diags.HasErrors() {
   982  						t.Fatalf("unexpected success; want error")
   983  					}
   984  
   985  					if got, want := diags.Err().Error(), test.WantTypeErr; got != want {
   986  						t.Errorf("wrong error\ngot:  %s\nwant: %s", got, want)
   987  					}
   988  				})
   989  				t.Run("null error", func(t *testing.T) {
   990  					rawVal := &InputValue{
   991  						Value:       cty.NullVal(cty.DynamicPseudoType),
   992  						SourceType:  test.SourceType,
   993  						SourceRange: test.SourceRange,
   994  					}
   995  
   996  					_, diags := prepareFinalInputVariableValue(
   997  						varAddr, rawVal, varCfg,
   998  					)
   999  					if !diags.HasErrors() {
  1000  						t.Fatalf("unexpected success; want error")
  1001  					}
  1002  
  1003  					if got, want := diags.Err().Error(), test.WantNullErr; got != want {
  1004  						t.Errorf("wrong error\ngot:  %s\nwant: %s", got, want)
  1005  					}
  1006  				})
  1007  			})
  1008  		}
  1009  	})
  1010  
  1011  	t.Run("SensitiveVariable error message variants, with source variants", func(t *testing.T) {
  1012  		tests := []struct {
  1013  			SourceType  ValueSourceType
  1014  			SourceRange tfdiags.SourceRange
  1015  			WantTypeErr string
  1016  			HideSubject bool
  1017  		}{
  1018  			{
  1019  				ValueFromUnknown,
  1020  				tfdiags.SourceRange{},
  1021  				"Invalid value for input variable: Unsuitable value for var.constrained_string_sensitive_required set from outside of the configuration: string required.",
  1022  				false,
  1023  			},
  1024  			{
  1025  				ValueFromConfig,
  1026  				tfdiags.SourceRange{
  1027  					Filename: "example.tfvars",
  1028  					Start:    tfdiags.SourcePos(hcl.InitialPos),
  1029  					End:      tfdiags.SourcePos(hcl.InitialPos),
  1030  				},
  1031  				`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.`,
  1032  				true,
  1033  			},
  1034  		}
  1035  
  1036  		for _, test := range tests {
  1037  			t.Run(fmt.Sprintf("%s %s", test.SourceType, test.SourceRange.StartString()), func(t *testing.T) {
  1038  				varAddr := addrs.InputVariable{Name: "constrained_string_sensitive_required"}.Absolute(addrs.RootModuleInstance)
  1039  				varCfg := variableConfigs[varAddr.Variable.Name]
  1040  				t.Run("type error", func(t *testing.T) {
  1041  					rawVal := &InputValue{
  1042  						Value:       cty.EmptyObjectVal,
  1043  						SourceType:  test.SourceType,
  1044  						SourceRange: test.SourceRange,
  1045  					}
  1046  
  1047  					_, diags := prepareFinalInputVariableValue(
  1048  						varAddr, rawVal, varCfg,
  1049  					)
  1050  					if !diags.HasErrors() {
  1051  						t.Fatalf("unexpected success; want error")
  1052  					}
  1053  
  1054  					if got, want := diags.Err().Error(), test.WantTypeErr; got != want {
  1055  						t.Errorf("wrong error\ngot:  %s\nwant: %s", got, want)
  1056  					}
  1057  
  1058  					if test.HideSubject {
  1059  						if got, want := diags[0].Source().Subject.StartString(), test.SourceRange.StartString(); got == want {
  1060  							t.Errorf("Subject start should have been hidden, but was %s", got)
  1061  						}
  1062  					}
  1063  				})
  1064  			})
  1065  		}
  1066  	})
  1067  }
  1068  
  1069  // These tests cover the JSON syntax configuration edge case handling,
  1070  // the background of which is described in detail in comments in the
  1071  // evalVariableValidations function. Future versions of OpenTofu may
  1072  // be able to remove this behaviour altogether.
  1073  func TestEvalVariableValidations_jsonErrorMessageEdgeCase(t *testing.T) {
  1074  	cfgSrc := `{
  1075    "variable": {
  1076      "valid": {
  1077        "type": "string",
  1078        "validation": {
  1079          "condition": "${var.valid != \"bar\"}",
  1080          "error_message": "Valid template string ${var.valid}"
  1081        }
  1082      },
  1083      "invalid": {
  1084        "type": "string",
  1085        "validation": {
  1086          "condition": "${var.invalid != \"bar\"}",
  1087          "error_message": "Invalid template string ${"
  1088        }
  1089      }
  1090    }
  1091  }
  1092  `
  1093  	cfg := testModuleInline(t, map[string]string{
  1094  		"main.tf.json": cfgSrc,
  1095  	})
  1096  	variableConfigs := cfg.Module.Variables
  1097  
  1098  	// Because we loaded our pseudo-module from a temporary file, the
  1099  	// declaration source ranges will have unpredictable filenames. We'll
  1100  	// fix that here just to make things easier below.
  1101  	for _, vc := range variableConfigs {
  1102  		vc.DeclRange.Filename = "main.tf.json"
  1103  		for _, v := range vc.Validations {
  1104  			v.DeclRange.Filename = "main.tf.json"
  1105  		}
  1106  	}
  1107  
  1108  	tests := []struct {
  1109  		varName  string
  1110  		given    cty.Value
  1111  		wantErr  []string
  1112  		wantWarn []string
  1113  		status   checks.Status
  1114  	}{
  1115  		// Valid variable validation declaration, assigned value which passes
  1116  		// the condition generates no diagnostics.
  1117  		{
  1118  			varName: "valid",
  1119  			given:   cty.StringVal("foo"),
  1120  			status:  checks.StatusPass,
  1121  		},
  1122  		// Assigning a value which fails the condition generates an error
  1123  		// message with the expression successfully evaluated.
  1124  		{
  1125  			varName: "valid",
  1126  			given:   cty.StringVal("bar"),
  1127  			wantErr: []string{
  1128  				"Invalid value for variable",
  1129  				"Valid template string bar",
  1130  			},
  1131  			status: checks.StatusFail,
  1132  		},
  1133  		// Invalid variable validation declaration due to an unparseable
  1134  		// template string. Assigning a value which passes the condition
  1135  		// results in a warning about the error message.
  1136  		{
  1137  			varName: "invalid",
  1138  			given:   cty.StringVal("foo"),
  1139  			wantWarn: []string{
  1140  				"Validation error message expression is invalid",
  1141  				"Missing expression; Expected the start of an expression, but found the end of the file.",
  1142  			},
  1143  			status: checks.StatusPass,
  1144  		},
  1145  		// Assigning a value which fails the condition generates an error
  1146  		// message including the configured string interpreted as a literal
  1147  		// value, and the same warning diagnostic as above.
  1148  		{
  1149  			varName: "invalid",
  1150  			given:   cty.StringVal("bar"),
  1151  			wantErr: []string{
  1152  				"Invalid value for variable",
  1153  				"Invalid template string ${",
  1154  			},
  1155  			wantWarn: []string{
  1156  				"Validation error message expression is invalid",
  1157  				"Missing expression; Expected the start of an expression, but found the end of the file.",
  1158  			},
  1159  			status: checks.StatusFail,
  1160  		},
  1161  	}
  1162  
  1163  	for _, test := range tests {
  1164  		t.Run(fmt.Sprintf("%s %#v", test.varName, test.given), func(t *testing.T) {
  1165  			varAddr := addrs.InputVariable{Name: test.varName}.Absolute(addrs.RootModuleInstance)
  1166  			varCfg := variableConfigs[test.varName]
  1167  			if varCfg == nil {
  1168  				t.Fatalf("invalid variable name %q", test.varName)
  1169  			}
  1170  
  1171  			// Build a mock context to allow the function under test to
  1172  			// retrieve the variable value and evaluate the expressions
  1173  			ctx := &MockEvalContext{}
  1174  
  1175  			// We need a minimal scope to allow basic functions to be passed to
  1176  			// the HCL scope
  1177  			ctx.EvaluationScopeScope = &lang.Scope{}
  1178  			ctx.GetVariableValueFunc = func(addr addrs.AbsInputVariableInstance) cty.Value {
  1179  				if got, want := addr.String(), varAddr.String(); got != want {
  1180  					t.Errorf("incorrect argument to GetVariableValue: got %s, want %s", got, want)
  1181  				}
  1182  				return test.given
  1183  			}
  1184  			ctx.ChecksState = checks.NewState(cfg)
  1185  			ctx.ChecksState.ReportCheckableObjects(varAddr.ConfigCheckable(), addrs.MakeSet[addrs.Checkable](varAddr))
  1186  
  1187  			gotDiags := evalVariableValidations(
  1188  				varAddr, varCfg, nil, ctx,
  1189  			)
  1190  
  1191  			if ctx.ChecksState.ObjectCheckStatus(varAddr) != test.status {
  1192  				t.Errorf("expected check result %s but instead %s", test.status, ctx.ChecksState.ObjectCheckStatus(varAddr))
  1193  			}
  1194  
  1195  			if len(test.wantErr) == 0 && len(test.wantWarn) == 0 {
  1196  				if len(gotDiags) > 0 {
  1197  					t.Errorf("no diags expected, got %s", gotDiags.Err().Error())
  1198  				}
  1199  			} else {
  1200  			wantErrs:
  1201  				for _, want := range test.wantErr {
  1202  					for _, diag := range gotDiags {
  1203  						if diag.Severity() != tfdiags.Error {
  1204  							continue
  1205  						}
  1206  						desc := diag.Description()
  1207  						if strings.Contains(desc.Summary, want) || strings.Contains(desc.Detail, want) {
  1208  							continue wantErrs
  1209  						}
  1210  					}
  1211  					t.Errorf("no error diagnostics found containing %q\ngot: %s", want, gotDiags.Err().Error())
  1212  				}
  1213  
  1214  			wantWarns:
  1215  				for _, want := range test.wantWarn {
  1216  					for _, diag := range gotDiags {
  1217  						if diag.Severity() != tfdiags.Warning {
  1218  							continue
  1219  						}
  1220  						desc := diag.Description()
  1221  						if strings.Contains(desc.Summary, want) || strings.Contains(desc.Detail, want) {
  1222  							continue wantWarns
  1223  						}
  1224  					}
  1225  					t.Errorf("no warning diagnostics found containing %q\ngot: %s", want, gotDiags.Err().Error())
  1226  				}
  1227  			}
  1228  		})
  1229  	}
  1230  }
  1231  
  1232  func TestEvalVariableValidations_sensitiveValues(t *testing.T) {
  1233  	cfgSrc := `
  1234  variable "foo" {
  1235    type      = string
  1236    sensitive = true
  1237    default   = "boop"
  1238  
  1239    validation {
  1240      condition     = length(var.foo) == 4
  1241  	error_message = "Foo must be 4 characters, not ${length(var.foo)}"
  1242    }
  1243  }
  1244  
  1245  variable "bar" {
  1246    type      = string
  1247    sensitive = true
  1248    default   = "boop"
  1249  
  1250    validation {
  1251      condition     = length(var.bar) == 4
  1252  	error_message = "Bar must be 4 characters, not ${nonsensitive(length(var.bar))}."
  1253    }
  1254  }
  1255  `
  1256  	cfg := testModuleInline(t, map[string]string{
  1257  		"main.tf": cfgSrc,
  1258  	})
  1259  	variableConfigs := cfg.Module.Variables
  1260  
  1261  	// Because we loaded our pseudo-module from a temporary file, the
  1262  	// declaration source ranges will have unpredictable filenames. We'll
  1263  	// fix that here just to make things easier below.
  1264  	for _, vc := range variableConfigs {
  1265  		vc.DeclRange.Filename = "main.tf"
  1266  		for _, v := range vc.Validations {
  1267  			v.DeclRange.Filename = "main.tf"
  1268  		}
  1269  	}
  1270  
  1271  	tests := []struct {
  1272  		varName string
  1273  		given   cty.Value
  1274  		wantErr []string
  1275  		status  checks.Status
  1276  	}{
  1277  		// Validations pass on a sensitive variable with an error message which
  1278  		// would generate a sensitive value
  1279  		{
  1280  			varName: "foo",
  1281  			given:   cty.StringVal("boop"),
  1282  			status:  checks.StatusPass,
  1283  		},
  1284  		// Assigning a value which fails the condition generates a sensitive
  1285  		// error message, which is elided and generates another error
  1286  		{
  1287  			varName: "foo",
  1288  			given:   cty.StringVal("bap"),
  1289  			wantErr: []string{
  1290  				"Invalid value for variable",
  1291  				"The error message included a sensitive value, so it will not be displayed.",
  1292  				"Error message refers to sensitive values",
  1293  			},
  1294  			status: checks.StatusFail,
  1295  		},
  1296  		// Validations pass on a sensitive variable with a correctly defined
  1297  		// error message
  1298  		{
  1299  			varName: "bar",
  1300  			given:   cty.StringVal("boop"),
  1301  			status:  checks.StatusPass,
  1302  		},
  1303  		// Assigning a value which fails the condition generates a nonsensitive
  1304  		// error message, which is displayed
  1305  		{
  1306  			varName: "bar",
  1307  			given:   cty.StringVal("bap"),
  1308  			wantErr: []string{
  1309  				"Invalid value for variable",
  1310  				"Bar must be 4 characters, not 3.",
  1311  			},
  1312  			status: checks.StatusFail,
  1313  		},
  1314  	}
  1315  
  1316  	for _, test := range tests {
  1317  		t.Run(fmt.Sprintf("%s %#v", test.varName, test.given), func(t *testing.T) {
  1318  			varAddr := addrs.InputVariable{Name: test.varName}.Absolute(addrs.RootModuleInstance)
  1319  			varCfg := variableConfigs[test.varName]
  1320  			if varCfg == nil {
  1321  				t.Fatalf("invalid variable name %q", test.varName)
  1322  			}
  1323  
  1324  			// Build a mock context to allow the function under test to
  1325  			// retrieve the variable value and evaluate the expressions
  1326  			ctx := &MockEvalContext{}
  1327  
  1328  			// We need a minimal scope to allow basic functions to be passed to
  1329  			// the HCL scope
  1330  			ctx.EvaluationScopeScope = &lang.Scope{}
  1331  			ctx.GetVariableValueFunc = func(addr addrs.AbsInputVariableInstance) cty.Value {
  1332  				if got, want := addr.String(), varAddr.String(); got != want {
  1333  					t.Errorf("incorrect argument to GetVariableValue: got %s, want %s", got, want)
  1334  				}
  1335  				if varCfg.Sensitive {
  1336  					return test.given.Mark(marks.Sensitive)
  1337  				} else {
  1338  					return test.given
  1339  				}
  1340  			}
  1341  			ctx.ChecksState = checks.NewState(cfg)
  1342  			ctx.ChecksState.ReportCheckableObjects(varAddr.ConfigCheckable(), addrs.MakeSet[addrs.Checkable](varAddr))
  1343  
  1344  			gotDiags := evalVariableValidations(
  1345  				varAddr, varCfg, nil, ctx,
  1346  			)
  1347  
  1348  			if ctx.ChecksState.ObjectCheckStatus(varAddr) != test.status {
  1349  				t.Errorf("expected check result %s but instead %s", test.status, ctx.ChecksState.ObjectCheckStatus(varAddr))
  1350  			}
  1351  
  1352  			if len(test.wantErr) == 0 {
  1353  				if len(gotDiags) > 0 {
  1354  					t.Errorf("no diags expected, got %s", gotDiags.Err().Error())
  1355  				}
  1356  			} else {
  1357  			wantErrs:
  1358  				for _, want := range test.wantErr {
  1359  					for _, diag := range gotDiags {
  1360  						if diag.Severity() != tfdiags.Error {
  1361  							continue
  1362  						}
  1363  						desc := diag.Description()
  1364  						if strings.Contains(desc.Summary, want) || strings.Contains(desc.Detail, want) {
  1365  							continue wantErrs
  1366  						}
  1367  					}
  1368  					t.Errorf("no error diagnostics found containing %q\ngot: %s", want, gotDiags.Err().Error())
  1369  				}
  1370  			}
  1371  		})
  1372  	}
  1373  }