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 }