github.com/opentofu/opentofu@v1.7.1/internal/lang/blocktoattr/fixup_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 blocktoattr 7 8 import ( 9 "testing" 10 11 "github.com/hashicorp/hcl/v2" 12 "github.com/hashicorp/hcl/v2/ext/dynblock" 13 "github.com/hashicorp/hcl/v2/hcldec" 14 "github.com/hashicorp/hcl/v2/hclsyntax" 15 hcljson "github.com/hashicorp/hcl/v2/json" 16 "github.com/opentofu/opentofu/internal/configs/configschema" 17 "github.com/zclconf/go-cty/cty" 18 ) 19 20 func TestFixUpBlockAttrs(t *testing.T) { 21 fooSchema := &configschema.Block{ 22 Attributes: map[string]*configschema.Attribute{ 23 "foo": { 24 Type: cty.List(cty.Object(map[string]cty.Type{ 25 "bar": cty.String, 26 })), 27 Optional: true, 28 }, 29 }, 30 } 31 32 tests := map[string]struct { 33 src string 34 json bool 35 schema *configschema.Block 36 want cty.Value 37 wantErrs bool 38 }{ 39 "empty": { 40 src: ``, 41 schema: &configschema.Block{}, 42 want: cty.EmptyObjectVal, 43 }, 44 "empty JSON": { 45 src: `{}`, 46 json: true, 47 schema: &configschema.Block{}, 48 want: cty.EmptyObjectVal, 49 }, 50 "unset": { 51 src: ``, 52 schema: fooSchema, 53 want: cty.ObjectVal(map[string]cty.Value{ 54 "foo": cty.NullVal(fooSchema.Attributes["foo"].Type), 55 }), 56 }, 57 "unset JSON": { 58 src: `{}`, 59 json: true, 60 schema: fooSchema, 61 want: cty.ObjectVal(map[string]cty.Value{ 62 "foo": cty.NullVal(fooSchema.Attributes["foo"].Type), 63 }), 64 }, 65 "no fixup required, with one value": { 66 src: ` 67 foo = [ 68 { 69 bar = "baz" 70 }, 71 ] 72 `, 73 schema: fooSchema, 74 want: cty.ObjectVal(map[string]cty.Value{ 75 "foo": cty.ListVal([]cty.Value{ 76 cty.ObjectVal(map[string]cty.Value{ 77 "bar": cty.StringVal("baz"), 78 }), 79 }), 80 }), 81 }, 82 "no fixup required, with two values": { 83 src: ` 84 foo = [ 85 { 86 bar = "baz" 87 }, 88 { 89 bar = "boop" 90 }, 91 ] 92 `, 93 schema: fooSchema, 94 want: cty.ObjectVal(map[string]cty.Value{ 95 "foo": cty.ListVal([]cty.Value{ 96 cty.ObjectVal(map[string]cty.Value{ 97 "bar": cty.StringVal("baz"), 98 }), 99 cty.ObjectVal(map[string]cty.Value{ 100 "bar": cty.StringVal("boop"), 101 }), 102 }), 103 }), 104 }, 105 "no fixup required, with values, JSON": { 106 src: `{"foo": [{"bar": "baz"}]}`, 107 json: true, 108 schema: fooSchema, 109 want: cty.ObjectVal(map[string]cty.Value{ 110 "foo": cty.ListVal([]cty.Value{ 111 cty.ObjectVal(map[string]cty.Value{ 112 "bar": cty.StringVal("baz"), 113 }), 114 }), 115 }), 116 }, 117 "no fixup required, empty": { 118 src: ` 119 foo = [] 120 `, 121 schema: fooSchema, 122 want: cty.ObjectVal(map[string]cty.Value{ 123 "foo": cty.ListValEmpty(fooSchema.Attributes["foo"].Type.ElementType()), 124 }), 125 }, 126 "no fixup required, empty, JSON": { 127 src: `{"foo":[]}`, 128 json: true, 129 schema: fooSchema, 130 want: cty.ObjectVal(map[string]cty.Value{ 131 "foo": cty.ListValEmpty(fooSchema.Attributes["foo"].Type.ElementType()), 132 }), 133 }, 134 "fixup one block": { 135 src: ` 136 foo { 137 bar = "baz" 138 } 139 `, 140 schema: fooSchema, 141 want: cty.ObjectVal(map[string]cty.Value{ 142 "foo": cty.ListVal([]cty.Value{ 143 cty.ObjectVal(map[string]cty.Value{ 144 "bar": cty.StringVal("baz"), 145 }), 146 }), 147 }), 148 }, 149 "fixup one block omitting attribute": { 150 src: ` 151 foo {} 152 `, 153 schema: fooSchema, 154 want: cty.ObjectVal(map[string]cty.Value{ 155 "foo": cty.ListVal([]cty.Value{ 156 cty.ObjectVal(map[string]cty.Value{ 157 "bar": cty.NullVal(cty.String), 158 }), 159 }), 160 }), 161 }, 162 "fixup two blocks": { 163 src: ` 164 foo { 165 bar = baz 166 } 167 foo { 168 bar = "boop" 169 } 170 `, 171 schema: fooSchema, 172 want: cty.ObjectVal(map[string]cty.Value{ 173 "foo": cty.ListVal([]cty.Value{ 174 cty.ObjectVal(map[string]cty.Value{ 175 "bar": cty.StringVal("baz value"), 176 }), 177 cty.ObjectVal(map[string]cty.Value{ 178 "bar": cty.StringVal("boop"), 179 }), 180 }), 181 }), 182 }, 183 "interaction with dynamic block generation": { 184 src: ` 185 dynamic "foo" { 186 for_each = ["baz", beep] 187 content { 188 bar = foo.value 189 } 190 } 191 `, 192 schema: fooSchema, 193 want: cty.ObjectVal(map[string]cty.Value{ 194 "foo": cty.ListVal([]cty.Value{ 195 cty.ObjectVal(map[string]cty.Value{ 196 "bar": cty.StringVal("baz"), 197 }), 198 cty.ObjectVal(map[string]cty.Value{ 199 "bar": cty.StringVal("beep value"), 200 }), 201 }), 202 }), 203 }, 204 "dynamic block with empty iterator": { 205 src: ` 206 dynamic "foo" { 207 for_each = [] 208 content { 209 bar = foo.value 210 } 211 } 212 `, 213 schema: fooSchema, 214 want: cty.ObjectVal(map[string]cty.Value{ 215 "foo": cty.NullVal(fooSchema.Attributes["foo"].Type), 216 }), 217 }, 218 "both attribute and block syntax": { 219 src: ` 220 foo = [] 221 foo { 222 bar = "baz" 223 } 224 `, 225 schema: fooSchema, 226 wantErrs: true, // Unsupported block type (user must be consistent about whether they consider foo to be a block type or an attribute) 227 want: cty.ObjectVal(map[string]cty.Value{ 228 "foo": cty.ListVal([]cty.Value{ 229 cty.ObjectVal(map[string]cty.Value{ 230 "bar": cty.StringVal("baz"), 231 }), 232 cty.ObjectVal(map[string]cty.Value{ 233 "bar": cty.StringVal("boop"), 234 }), 235 }), 236 }), 237 }, 238 "fixup inside block": { 239 src: ` 240 container { 241 foo { 242 bar = "baz" 243 } 244 foo { 245 bar = "boop" 246 } 247 } 248 container { 249 foo { 250 bar = beep 251 } 252 } 253 `, 254 schema: &configschema.Block{ 255 BlockTypes: map[string]*configschema.NestedBlock{ 256 "container": { 257 Nesting: configschema.NestingList, 258 Block: *fooSchema, 259 }, 260 }, 261 }, 262 want: cty.ObjectVal(map[string]cty.Value{ 263 "container": cty.ListVal([]cty.Value{ 264 cty.ObjectVal(map[string]cty.Value{ 265 "foo": cty.ListVal([]cty.Value{ 266 cty.ObjectVal(map[string]cty.Value{ 267 "bar": cty.StringVal("baz"), 268 }), 269 cty.ObjectVal(map[string]cty.Value{ 270 "bar": cty.StringVal("boop"), 271 }), 272 }), 273 }), 274 cty.ObjectVal(map[string]cty.Value{ 275 "foo": cty.ListVal([]cty.Value{ 276 cty.ObjectVal(map[string]cty.Value{ 277 "bar": cty.StringVal("beep value"), 278 }), 279 }), 280 }), 281 }), 282 }), 283 }, 284 "fixup inside attribute-as-block": { 285 src: ` 286 container { 287 foo { 288 bar = "baz" 289 } 290 foo { 291 bar = "boop" 292 } 293 } 294 container { 295 foo { 296 bar = beep 297 } 298 } 299 `, 300 schema: &configschema.Block{ 301 Attributes: map[string]*configschema.Attribute{ 302 "container": { 303 Type: cty.List(cty.Object(map[string]cty.Type{ 304 "foo": cty.List(cty.Object(map[string]cty.Type{ 305 "bar": cty.String, 306 })), 307 })), 308 Optional: true, 309 }, 310 }, 311 }, 312 want: cty.ObjectVal(map[string]cty.Value{ 313 "container": cty.ListVal([]cty.Value{ 314 cty.ObjectVal(map[string]cty.Value{ 315 "foo": cty.ListVal([]cty.Value{ 316 cty.ObjectVal(map[string]cty.Value{ 317 "bar": cty.StringVal("baz"), 318 }), 319 cty.ObjectVal(map[string]cty.Value{ 320 "bar": cty.StringVal("boop"), 321 }), 322 }), 323 }), 324 cty.ObjectVal(map[string]cty.Value{ 325 "foo": cty.ListVal([]cty.Value{ 326 cty.ObjectVal(map[string]cty.Value{ 327 "bar": cty.StringVal("beep value"), 328 }), 329 }), 330 }), 331 }), 332 }), 333 }, 334 "nested fixup with dynamic block generation": { 335 src: ` 336 container { 337 dynamic "foo" { 338 for_each = ["baz", beep] 339 content { 340 bar = foo.value 341 } 342 } 343 } 344 `, 345 schema: &configschema.Block{ 346 BlockTypes: map[string]*configschema.NestedBlock{ 347 "container": { 348 Nesting: configschema.NestingList, 349 Block: *fooSchema, 350 }, 351 }, 352 }, 353 want: cty.ObjectVal(map[string]cty.Value{ 354 "container": cty.ListVal([]cty.Value{ 355 cty.ObjectVal(map[string]cty.Value{ 356 "foo": cty.ListVal([]cty.Value{ 357 cty.ObjectVal(map[string]cty.Value{ 358 "bar": cty.StringVal("baz"), 359 }), 360 cty.ObjectVal(map[string]cty.Value{ 361 "bar": cty.StringVal("beep value"), 362 }), 363 }), 364 }), 365 }), 366 }), 367 }, 368 369 "missing nested block items": { 370 src: ` 371 container { 372 foo { 373 bar = "one" 374 } 375 } 376 `, 377 schema: &configschema.Block{ 378 BlockTypes: map[string]*configschema.NestedBlock{ 379 "container": { 380 Nesting: configschema.NestingList, 381 MinItems: 2, 382 Block: configschema.Block{ 383 Attributes: map[string]*configschema.Attribute{ 384 "foo": { 385 Type: cty.List(cty.Object(map[string]cty.Type{ 386 "bar": cty.String, 387 })), 388 Optional: true, 389 }, 390 }, 391 }, 392 }, 393 }, 394 }, 395 want: cty.ObjectVal(map[string]cty.Value{ 396 "container": cty.ListVal([]cty.Value{ 397 cty.ObjectVal(map[string]cty.Value{ 398 "foo": cty.ListVal([]cty.Value{ 399 cty.ObjectVal(map[string]cty.Value{ 400 "bar": cty.StringVal("baz"), 401 }), 402 }), 403 }), 404 }), 405 }), 406 wantErrs: true, 407 }, 408 "no fixup allowed with NestedType": { 409 src: ` 410 container { 411 foo = "one" 412 } 413 `, 414 schema: &configschema.Block{ 415 Attributes: map[string]*configschema.Attribute{ 416 "container": { 417 NestedType: &configschema.Object{ 418 Nesting: configschema.NestingList, 419 Attributes: map[string]*configschema.Attribute{ 420 "foo": { 421 Type: cty.String, 422 }, 423 }, 424 }, 425 }, 426 }, 427 }, 428 want: cty.ObjectVal(map[string]cty.Value{ 429 "container": cty.NullVal(cty.List( 430 cty.Object(map[string]cty.Type{ 431 "foo": cty.String, 432 }), 433 )), 434 }), 435 wantErrs: true, 436 }, 437 "no fixup allowed new types": { 438 src: ` 439 container { 440 foo = "one" 441 } 442 `, 443 schema: &configschema.Block{ 444 Attributes: map[string]*configschema.Attribute{ 445 // This could be a ConfigModeAttr fixup 446 "container": { 447 Type: cty.List(cty.Object(map[string]cty.Type{ 448 "foo": cty.String, 449 })), 450 }, 451 // But the presence of this type means it must have been 452 // declared by a new SDK 453 "new_type": { 454 Type: cty.Object(map[string]cty.Type{ 455 "boo": cty.String, 456 }), 457 }, 458 }, 459 }, 460 want: cty.ObjectVal(map[string]cty.Value{ 461 "container": cty.NullVal(cty.List( 462 cty.Object(map[string]cty.Type{ 463 "foo": cty.String, 464 }), 465 )), 466 }), 467 wantErrs: true, 468 }, 469 } 470 471 ctx := &hcl.EvalContext{ 472 Variables: map[string]cty.Value{ 473 "bar": cty.StringVal("bar value"), 474 "baz": cty.StringVal("baz value"), 475 "beep": cty.StringVal("beep value"), 476 }, 477 } 478 479 for name, test := range tests { 480 t.Run(name, func(t *testing.T) { 481 var f *hcl.File 482 var diags hcl.Diagnostics 483 if test.json { 484 f, diags = hcljson.Parse([]byte(test.src), "test.tf.json") 485 } else { 486 f, diags = hclsyntax.ParseConfig([]byte(test.src), "test.tf", hcl.Pos{Line: 1, Column: 1}) 487 } 488 if diags.HasErrors() { 489 for _, diag := range diags { 490 t.Errorf("unexpected diagnostic: %s", diag) 491 } 492 t.FailNow() 493 } 494 495 // We'll expand dynamic blocks in the body first, to mimic how 496 // we process this fixup when using the main "lang" package API. 497 spec := test.schema.DecoderSpec() 498 body := dynblock.Expand(f.Body, ctx) 499 500 body = FixUpBlockAttrs(body, test.schema) 501 got, diags := hcldec.Decode(body, spec, ctx) 502 503 if test.wantErrs { 504 if !diags.HasErrors() { 505 t.Errorf("succeeded, but want error\ngot: %#v", got) 506 } 507 508 // check that our wrapped body returns the correct context by 509 // verifying the Subject is valid. 510 for _, d := range diags { 511 if d.Subject.Filename == "" { 512 t.Errorf("empty diagnostic subject: %#v", d.Subject) 513 } 514 } 515 return 516 } 517 518 if !test.want.RawEquals(got) { 519 t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.want) 520 } 521 for _, diag := range diags { 522 t.Errorf("unexpected diagnostic: %s", diag) 523 } 524 }) 525 } 526 }