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