github.com/opentofu/opentofu@v1.7.1/internal/tofu/eval_for_each_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  	"reflect"
    10  	"strings"
    11  	"testing"
    12  
    13  	"github.com/davecgh/go-spew/spew"
    14  	"github.com/hashicorp/hcl/v2"
    15  	"github.com/hashicorp/hcl/v2/hcltest"
    16  	"github.com/opentofu/opentofu/internal/lang/marks"
    17  	"github.com/opentofu/opentofu/internal/tfdiags"
    18  	"github.com/zclconf/go-cty/cty"
    19  )
    20  
    21  func TestEvaluateForEachExpression_valid(t *testing.T) {
    22  	tests := map[string]struct {
    23  		Expr       hcl.Expression
    24  		ForEachMap map[string]cty.Value
    25  	}{
    26  		"empty set": {
    27  			hcltest.MockExprLiteral(cty.SetValEmpty(cty.String)),
    28  			map[string]cty.Value{},
    29  		},
    30  		"multi-value string set": {
    31  			hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.StringVal("a"), cty.StringVal("b")})),
    32  			map[string]cty.Value{
    33  				"a": cty.StringVal("a"),
    34  				"b": cty.StringVal("b"),
    35  			},
    36  		},
    37  		"empty map": {
    38  			hcltest.MockExprLiteral(cty.MapValEmpty(cty.Bool)),
    39  			map[string]cty.Value{},
    40  		},
    41  		"map": {
    42  			hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{
    43  				"a": cty.BoolVal(true),
    44  				"b": cty.BoolVal(false),
    45  			})),
    46  			map[string]cty.Value{
    47  				"a": cty.BoolVal(true),
    48  				"b": cty.BoolVal(false),
    49  			},
    50  		},
    51  		"map containing unknown values": {
    52  			hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{
    53  				"a": cty.UnknownVal(cty.Bool),
    54  				"b": cty.UnknownVal(cty.Bool),
    55  			})),
    56  			map[string]cty.Value{
    57  				"a": cty.UnknownVal(cty.Bool),
    58  				"b": cty.UnknownVal(cty.Bool),
    59  			},
    60  		},
    61  		"map containing sensitive values, but strings are literal": {
    62  			hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{
    63  				"a": cty.BoolVal(true).Mark(marks.Sensitive),
    64  				"b": cty.BoolVal(false),
    65  			})),
    66  			map[string]cty.Value{
    67  				"a": cty.BoolVal(true).Mark(marks.Sensitive),
    68  				"b": cty.BoolVal(false),
    69  			},
    70  		},
    71  	}
    72  
    73  	for name, test := range tests {
    74  		t.Run(name, func(t *testing.T) {
    75  			ctx := &MockEvalContext{}
    76  			ctx.installSimpleEval()
    77  			forEachMap, diags := evaluateForEachExpression(test.Expr, ctx)
    78  
    79  			if len(diags) != 0 {
    80  				t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
    81  			}
    82  
    83  			if !reflect.DeepEqual(forEachMap, test.ForEachMap) {
    84  				t.Errorf(
    85  					"wrong map value\ngot:  %swant: %s",
    86  					spew.Sdump(forEachMap), spew.Sdump(test.ForEachMap),
    87  				)
    88  			}
    89  
    90  		})
    91  	}
    92  }
    93  
    94  func TestEvaluateForEachExpression_errors(t *testing.T) {
    95  	tests := map[string]struct {
    96  		Expr                               hcl.Expression
    97  		Summary, DetailSubstring           string
    98  		CausedByUnknown, CausedBySensitive bool
    99  	}{
   100  		"null set": {
   101  			hcltest.MockExprLiteral(cty.NullVal(cty.Set(cty.String))),
   102  			"Invalid for_each argument",
   103  			`the given "for_each" argument value is null`,
   104  			false, false,
   105  		},
   106  		"string": {
   107  			hcltest.MockExprLiteral(cty.StringVal("i am definitely a set")),
   108  			"Invalid for_each argument",
   109  			"must be a map, or set of strings, and you have provided a value of type string",
   110  			false, false,
   111  		},
   112  		"list": {
   113  			hcltest.MockExprLiteral(cty.ListVal([]cty.Value{cty.StringVal("a"), cty.StringVal("a")})),
   114  			"Invalid for_each argument",
   115  			"must be a map, or set of strings, and you have provided a value of type list",
   116  			false, false,
   117  		},
   118  		"tuple": {
   119  			hcltest.MockExprLiteral(cty.TupleVal([]cty.Value{cty.StringVal("a"), cty.StringVal("b")})),
   120  			"Invalid for_each argument",
   121  			"must be a map, or set of strings, and you have provided a value of type tuple",
   122  			false, false,
   123  		},
   124  		"unknown string set": {
   125  			hcltest.MockExprLiteral(cty.UnknownVal(cty.Set(cty.String))),
   126  			"Invalid for_each argument",
   127  			"set includes values derived from resource attributes that cannot be determined until apply",
   128  			true, false,
   129  		},
   130  		"unknown map": {
   131  			hcltest.MockExprLiteral(cty.UnknownVal(cty.Map(cty.Bool))),
   132  			"Invalid for_each argument",
   133  			"map includes keys derived from resource attributes that cannot be determined until apply",
   134  			true, false,
   135  		},
   136  		"marked map": {
   137  			hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{
   138  				"a": cty.BoolVal(true),
   139  				"b": cty.BoolVal(false),
   140  			}).Mark(marks.Sensitive)),
   141  			"Invalid for_each argument",
   142  			"Sensitive values, or values derived from sensitive values, cannot be used as for_each arguments. If used, the sensitive value could be exposed as a resource instance key.",
   143  			false, true,
   144  		},
   145  		"set containing booleans": {
   146  			hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.BoolVal(true)})),
   147  			"Invalid for_each set argument",
   148  			"supports sets of strings, but you have provided a set containing type bool",
   149  			false, false,
   150  		},
   151  		"set containing null": {
   152  			hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.NullVal(cty.String)})),
   153  			"Invalid for_each set argument",
   154  			"must not contain null values",
   155  			false, false,
   156  		},
   157  		"set containing unknown value": {
   158  			hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.UnknownVal(cty.String)})),
   159  			"Invalid for_each argument",
   160  			"set includes values derived from resource attributes that cannot be determined until apply",
   161  			true, false,
   162  		},
   163  		"set containing dynamic unknown value": {
   164  			hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.UnknownVal(cty.DynamicPseudoType)})),
   165  			"Invalid for_each argument",
   166  			"set includes values derived from resource attributes that cannot be determined until apply",
   167  			true, false,
   168  		},
   169  		"set containing marked values": {
   170  			hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.StringVal("beep").Mark(marks.Sensitive), cty.StringVal("boop")})),
   171  			"Invalid for_each argument",
   172  			"Sensitive values, or values derived from sensitive values, cannot be used as for_each arguments. If used, the sensitive value could be exposed as a resource instance key.",
   173  			false, true,
   174  		},
   175  	}
   176  
   177  	for name, test := range tests {
   178  		t.Run(name, func(t *testing.T) {
   179  			ctx := &MockEvalContext{}
   180  			ctx.installSimpleEval()
   181  			_, diags := evaluateForEachExpression(test.Expr, ctx)
   182  
   183  			if len(diags) != 1 {
   184  				t.Fatalf("got %d diagnostics; want 1", diags)
   185  			}
   186  			if got, want := diags[0].Severity(), tfdiags.Error; got != want {
   187  				t.Errorf("wrong diagnostic severity %#v; want %#v", got, want)
   188  			}
   189  			if got, want := diags[0].Description().Summary, test.Summary; got != want {
   190  				t.Errorf("wrong diagnostic summary\ngot:  %s\nwant: %s", got, want)
   191  			}
   192  			if got, want := diags[0].Description().Detail, test.DetailSubstring; !strings.Contains(got, want) {
   193  				t.Errorf("wrong diagnostic detail\ngot: %s\nwant substring: %s", got, want)
   194  			}
   195  			if fromExpr := diags[0].FromExpr(); fromExpr != nil {
   196  				if fromExpr.Expression == nil {
   197  					t.Errorf("diagnostic does not refer to an expression")
   198  				}
   199  				if fromExpr.EvalContext == nil {
   200  					t.Errorf("diagnostic does not refer to an EvalContext")
   201  				}
   202  			} else {
   203  				t.Errorf("diagnostic does not support FromExpr\ngot: %s", spew.Sdump(diags[0]))
   204  			}
   205  
   206  			if got, want := tfdiags.DiagnosticCausedByUnknown(diags[0]), test.CausedByUnknown; got != want {
   207  				t.Errorf("wrong result from tfdiags.DiagnosticCausedByUnknown\ngot:  %#v\nwant: %#v", got, want)
   208  			}
   209  			if got, want := tfdiags.DiagnosticCausedBySensitive(diags[0]), test.CausedBySensitive; got != want {
   210  				t.Errorf("wrong result from tfdiags.DiagnosticCausedBySensitive\ngot:  %#v\nwant: %#v", got, want)
   211  			}
   212  		})
   213  	}
   214  }
   215  
   216  func TestEvaluateForEachExpressionKnown(t *testing.T) {
   217  	tests := map[string]hcl.Expression{
   218  		"unknown string set": hcltest.MockExprLiteral(cty.UnknownVal(cty.Set(cty.String))),
   219  		"unknown map":        hcltest.MockExprLiteral(cty.UnknownVal(cty.Map(cty.Bool))),
   220  		"unknown tuple":      hcltest.MockExprLiteral(cty.UnknownVal(cty.Tuple([]cty.Type{cty.String, cty.Number, cty.Bool}))),
   221  	}
   222  
   223  	for name, expr := range tests {
   224  		t.Run(name, func(t *testing.T) {
   225  			ctx := &MockEvalContext{}
   226  			ctx.installSimpleEval()
   227  			forEachVal, diags := evaluateForEachExpressionValue(expr, ctx, true, true)
   228  
   229  			if len(diags) != 0 {
   230  				t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
   231  			}
   232  
   233  			if forEachVal.IsKnown() {
   234  				t.Error("got known, want unknown")
   235  			}
   236  		})
   237  	}
   238  }
   239  
   240  func TestEvaluateForEachExpressionValueTuple(t *testing.T) {
   241  	tests := map[string]struct {
   242  		Expr          hcl.Expression
   243  		AllowTuple    bool
   244  		ExpectedError string
   245  	}{
   246  		"valid tuple": {
   247  			Expr:       hcltest.MockExprLiteral(cty.TupleVal([]cty.Value{cty.StringVal("a"), cty.StringVal("b")})),
   248  			AllowTuple: true,
   249  		},
   250  		"empty tuple": {
   251  			Expr:       hcltest.MockExprLiteral(cty.EmptyTupleVal),
   252  			AllowTuple: true,
   253  		},
   254  		"null tuple": {
   255  			Expr:          hcltest.MockExprLiteral(cty.NullVal(cty.Tuple([]cty.Type{}))),
   256  			AllowTuple:    true,
   257  			ExpectedError: "the given \"for_each\" argument value is null",
   258  		},
   259  		"sensitive tuple": {
   260  			Expr:          hcltest.MockExprLiteral(cty.TupleVal([]cty.Value{cty.StringVal("a"), cty.StringVal("b")}).Mark(marks.Sensitive)),
   261  			AllowTuple:    true,
   262  			ExpectedError: "Sensitive values, or values derived from sensitive values, cannot be used as for_each arguments",
   263  		},
   264  		"allow tuple is off": {
   265  			Expr:          hcltest.MockExprLiteral(cty.TupleVal([]cty.Value{cty.StringVal("a"), cty.StringVal("b")})),
   266  			AllowTuple:    false,
   267  			ExpectedError: "the \"for_each\" argument must be a map, or set of strings, and you have provided a value of type tuple.",
   268  		},
   269  	}
   270  
   271  	for name, test := range tests {
   272  		t.Run(name, func(t *testing.T) {
   273  			ctx := &MockEvalContext{}
   274  			ctx.installSimpleEval()
   275  			_, diags := evaluateForEachExpressionValue(test.Expr, ctx, true, test.AllowTuple)
   276  
   277  			if test.ExpectedError == "" {
   278  				if len(diags) != 0 {
   279  					t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
   280  				}
   281  			} else {
   282  				if got, want := diags[0].Description().Detail, test.ExpectedError; test.ExpectedError != "" && !strings.Contains(got, want) {
   283  					t.Errorf("wrong diagnostic detail\ngot: %s\nwant substring: %s", got, want)
   284  				}
   285  			}
   286  
   287  		})
   288  	}
   289  }