github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/configs/configschema/decoder_spec_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package configschema 5 6 import ( 7 "sort" 8 "testing" 9 10 "github.com/apparentlymart/go-dump/dump" 11 "github.com/davecgh/go-spew/spew" 12 "github.com/google/go-cmp/cmp" 13 14 "github.com/hashicorp/hcl/v2" 15 "github.com/hashicorp/hcl/v2/hcldec" 16 "github.com/hashicorp/hcl/v2/hcltest" 17 "github.com/zclconf/go-cty/cty" 18 ) 19 20 func TestBlockDecoderSpec(t *testing.T) { 21 tests := map[string]struct { 22 Schema *Block 23 TestBody hcl.Body 24 Want cty.Value 25 DiagCount int 26 }{ 27 "empty": { 28 &Block{}, 29 hcl.EmptyBody(), 30 cty.EmptyObjectVal, 31 0, 32 }, 33 "nil": { 34 nil, 35 hcl.EmptyBody(), 36 cty.EmptyObjectVal, 37 0, 38 }, 39 "attributes": { 40 &Block{ 41 Attributes: map[string]*Attribute{ 42 "optional": { 43 Type: cty.Number, 44 Optional: true, 45 }, 46 "required": { 47 Type: cty.String, 48 Required: true, 49 }, 50 "computed": { 51 Type: cty.List(cty.Bool), 52 Computed: true, 53 }, 54 "optional_computed": { 55 Type: cty.Map(cty.Bool), 56 Optional: true, 57 Computed: true, 58 }, 59 "optional_computed_overridden": { 60 Type: cty.Bool, 61 Optional: true, 62 Computed: true, 63 }, 64 "optional_computed_unknown": { 65 Type: cty.String, 66 Optional: true, 67 Computed: true, 68 }, 69 }, 70 }, 71 hcltest.MockBody(&hcl.BodyContent{ 72 Attributes: hcl.Attributes{ 73 "required": { 74 Name: "required", 75 Expr: hcltest.MockExprLiteral(cty.NumberIntVal(5)), 76 }, 77 "optional_computed_overridden": { 78 Name: "optional_computed_overridden", 79 Expr: hcltest.MockExprLiteral(cty.True), 80 }, 81 "optional_computed_unknown": { 82 Name: "optional_computed_overridden", 83 Expr: hcltest.MockExprLiteral(cty.UnknownVal(cty.String)), 84 }, 85 }, 86 }), 87 cty.ObjectVal(map[string]cty.Value{ 88 "optional": cty.NullVal(cty.Number), 89 "required": cty.StringVal("5"), // converted from number to string 90 "computed": cty.NullVal(cty.List(cty.Bool)), 91 "optional_computed": cty.NullVal(cty.Map(cty.Bool)), 92 "optional_computed_overridden": cty.True, 93 "optional_computed_unknown": cty.UnknownVal(cty.String), 94 }), 95 0, 96 }, 97 "dynamically-typed attribute": { 98 &Block{ 99 Attributes: map[string]*Attribute{ 100 "foo": { 101 Type: cty.DynamicPseudoType, // any type is permitted 102 Required: true, 103 }, 104 }, 105 }, 106 hcltest.MockBody(&hcl.BodyContent{ 107 Attributes: hcl.Attributes{ 108 "foo": { 109 Name: "foo", 110 Expr: hcltest.MockExprLiteral(cty.True), 111 }, 112 }, 113 }), 114 cty.ObjectVal(map[string]cty.Value{ 115 "foo": cty.True, 116 }), 117 0, 118 }, 119 "dynamically-typed attribute omitted": { 120 &Block{ 121 Attributes: map[string]*Attribute{ 122 "foo": { 123 Type: cty.DynamicPseudoType, // any type is permitted 124 Optional: true, 125 }, 126 }, 127 }, 128 hcltest.MockBody(&hcl.BodyContent{}), 129 cty.ObjectVal(map[string]cty.Value{ 130 "foo": cty.NullVal(cty.DynamicPseudoType), 131 }), 132 0, 133 }, 134 "required attribute omitted": { 135 &Block{ 136 Attributes: map[string]*Attribute{ 137 "foo": { 138 Type: cty.Bool, 139 Required: true, 140 }, 141 }, 142 }, 143 hcltest.MockBody(&hcl.BodyContent{}), 144 cty.ObjectVal(map[string]cty.Value{ 145 "foo": cty.NullVal(cty.Bool), 146 }), 147 1, // missing required attribute 148 }, 149 "wrong attribute type": { 150 &Block{ 151 Attributes: map[string]*Attribute{ 152 "optional": { 153 Type: cty.Number, 154 Optional: true, 155 }, 156 }, 157 }, 158 hcltest.MockBody(&hcl.BodyContent{ 159 Attributes: hcl.Attributes{ 160 "optional": { 161 Name: "optional", 162 Expr: hcltest.MockExprLiteral(cty.True), 163 }, 164 }, 165 }), 166 cty.ObjectVal(map[string]cty.Value{ 167 "optional": cty.UnknownVal(cty.Number), 168 }), 169 1, // incorrect type; number required 170 }, 171 "blocks": { 172 &Block{ 173 BlockTypes: map[string]*NestedBlock{ 174 "single": { 175 Nesting: NestingSingle, 176 Block: Block{}, 177 }, 178 "list": { 179 Nesting: NestingList, 180 Block: Block{}, 181 }, 182 "set": { 183 Nesting: NestingSet, 184 Block: Block{}, 185 }, 186 "map": { 187 Nesting: NestingMap, 188 Block: Block{}, 189 }, 190 }, 191 }, 192 hcltest.MockBody(&hcl.BodyContent{ 193 Blocks: hcl.Blocks{ 194 &hcl.Block{ 195 Type: "list", 196 Body: hcl.EmptyBody(), 197 }, 198 &hcl.Block{ 199 Type: "single", 200 Body: hcl.EmptyBody(), 201 }, 202 &hcl.Block{ 203 Type: "list", 204 Body: hcl.EmptyBody(), 205 }, 206 &hcl.Block{ 207 Type: "set", 208 Body: hcl.EmptyBody(), 209 }, 210 &hcl.Block{ 211 Type: "map", 212 Labels: []string{"foo"}, 213 LabelRanges: []hcl.Range{hcl.Range{}}, 214 Body: hcl.EmptyBody(), 215 }, 216 &hcl.Block{ 217 Type: "map", 218 Labels: []string{"bar"}, 219 LabelRanges: []hcl.Range{hcl.Range{}}, 220 Body: hcl.EmptyBody(), 221 }, 222 &hcl.Block{ 223 Type: "set", 224 Body: hcl.EmptyBody(), 225 }, 226 }, 227 }), 228 cty.ObjectVal(map[string]cty.Value{ 229 "single": cty.EmptyObjectVal, 230 "list": cty.ListVal([]cty.Value{ 231 cty.EmptyObjectVal, 232 cty.EmptyObjectVal, 233 }), 234 "set": cty.SetVal([]cty.Value{ 235 cty.EmptyObjectVal, 236 cty.EmptyObjectVal, 237 }), 238 "map": cty.MapVal(map[string]cty.Value{ 239 "foo": cty.EmptyObjectVal, 240 "bar": cty.EmptyObjectVal, 241 }), 242 }), 243 0, 244 }, 245 "blocks with dynamically-typed attributes": { 246 &Block{ 247 BlockTypes: map[string]*NestedBlock{ 248 "single": { 249 Nesting: NestingSingle, 250 Block: Block{ 251 Attributes: map[string]*Attribute{ 252 "a": { 253 Type: cty.DynamicPseudoType, 254 Optional: true, 255 }, 256 }, 257 }, 258 }, 259 "list": { 260 Nesting: NestingList, 261 Block: Block{ 262 Attributes: map[string]*Attribute{ 263 "a": { 264 Type: cty.DynamicPseudoType, 265 Optional: true, 266 }, 267 }, 268 }, 269 }, 270 "map": { 271 Nesting: NestingMap, 272 Block: Block{ 273 Attributes: map[string]*Attribute{ 274 "a": { 275 Type: cty.DynamicPseudoType, 276 Optional: true, 277 }, 278 }, 279 }, 280 }, 281 }, 282 }, 283 hcltest.MockBody(&hcl.BodyContent{ 284 Blocks: hcl.Blocks{ 285 &hcl.Block{ 286 Type: "list", 287 Body: hcl.EmptyBody(), 288 }, 289 &hcl.Block{ 290 Type: "single", 291 Body: hcl.EmptyBody(), 292 }, 293 &hcl.Block{ 294 Type: "list", 295 Body: hcl.EmptyBody(), 296 }, 297 &hcl.Block{ 298 Type: "map", 299 Labels: []string{"foo"}, 300 LabelRanges: []hcl.Range{hcl.Range{}}, 301 Body: hcl.EmptyBody(), 302 }, 303 &hcl.Block{ 304 Type: "map", 305 Labels: []string{"bar"}, 306 LabelRanges: []hcl.Range{hcl.Range{}}, 307 Body: hcl.EmptyBody(), 308 }, 309 }, 310 }), 311 cty.ObjectVal(map[string]cty.Value{ 312 "single": cty.ObjectVal(map[string]cty.Value{ 313 "a": cty.NullVal(cty.DynamicPseudoType), 314 }), 315 "list": cty.TupleVal([]cty.Value{ 316 cty.ObjectVal(map[string]cty.Value{ 317 "a": cty.NullVal(cty.DynamicPseudoType), 318 }), 319 cty.ObjectVal(map[string]cty.Value{ 320 "a": cty.NullVal(cty.DynamicPseudoType), 321 }), 322 }), 323 "map": cty.ObjectVal(map[string]cty.Value{ 324 "foo": cty.ObjectVal(map[string]cty.Value{ 325 "a": cty.NullVal(cty.DynamicPseudoType), 326 }), 327 "bar": cty.ObjectVal(map[string]cty.Value{ 328 "a": cty.NullVal(cty.DynamicPseudoType), 329 }), 330 }), 331 }), 332 0, 333 }, 334 "too many list items": { 335 &Block{ 336 BlockTypes: map[string]*NestedBlock{ 337 "foo": { 338 Nesting: NestingList, 339 Block: Block{}, 340 MaxItems: 1, 341 }, 342 }, 343 }, 344 hcltest.MockBody(&hcl.BodyContent{ 345 Blocks: hcl.Blocks{ 346 &hcl.Block{ 347 Type: "foo", 348 Body: hcl.EmptyBody(), 349 }, 350 &hcl.Block{ 351 Type: "foo", 352 Body: unknownBody{hcl.EmptyBody()}, 353 }, 354 }, 355 }), 356 cty.ObjectVal(map[string]cty.Value{ 357 "foo": cty.UnknownVal(cty.List(cty.EmptyObject)), 358 }), 359 0, // max items cannot be validated during decode 360 }, 361 // dynamic blocks may fulfill MinItems, but there is only one block to 362 // decode. 363 "required MinItems": { 364 &Block{ 365 BlockTypes: map[string]*NestedBlock{ 366 "foo": { 367 Nesting: NestingList, 368 Block: Block{}, 369 MinItems: 2, 370 }, 371 }, 372 }, 373 hcltest.MockBody(&hcl.BodyContent{ 374 Blocks: hcl.Blocks{ 375 &hcl.Block{ 376 Type: "foo", 377 Body: unknownBody{hcl.EmptyBody()}, 378 }, 379 }, 380 }), 381 cty.ObjectVal(map[string]cty.Value{ 382 "foo": cty.UnknownVal(cty.List(cty.EmptyObject)), 383 }), 384 0, 385 }, 386 "extraneous attribute": { 387 &Block{}, 388 hcltest.MockBody(&hcl.BodyContent{ 389 Attributes: hcl.Attributes{ 390 "extra": { 391 Name: "extra", 392 Expr: hcltest.MockExprLiteral(cty.StringVal("hello")), 393 }, 394 }, 395 }), 396 cty.EmptyObjectVal, 397 1, // extraneous attribute 398 }, 399 } 400 401 for name, test := range tests { 402 t.Run(name, func(t *testing.T) { 403 spec := test.Schema.DecoderSpec() 404 405 got, diags := hcldec.Decode(test.TestBody, spec, nil) 406 if len(diags) != test.DiagCount { 407 t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.DiagCount) 408 for _, diag := range diags { 409 t.Logf("- %s", diag.Error()) 410 } 411 } 412 413 if !got.RawEquals(test.Want) { 414 t.Logf("[INFO] implied schema is %s", spew.Sdump(hcldec.ImpliedSchema(spec))) 415 t.Errorf("wrong result\ngot: %s\nwant: %s", dump.Value(got), dump.Value(test.Want)) 416 } 417 418 // Double-check that we're producing consistent results for DecoderSpec 419 // and ImpliedType. 420 impliedType := test.Schema.ImpliedType() 421 if errs := got.Type().TestConformance(impliedType); len(errs) != 0 { 422 t.Errorf("result does not conform to the schema's implied type") 423 for _, err := range errs { 424 t.Logf("- %s", err.Error()) 425 } 426 } 427 }) 428 } 429 } 430 431 // this satisfies hcldec.UnknownBody to simulate a dynamic block with an 432 // unknown number of values. 433 type unknownBody struct { 434 hcl.Body 435 } 436 437 func (b unknownBody) Unknown() bool { 438 return true 439 } 440 441 func TestAttributeDecoderSpec(t *testing.T) { 442 tests := map[string]struct { 443 Schema *Attribute 444 TestBody hcl.Body 445 Want cty.Value 446 DiagCount int 447 }{ 448 "empty": { 449 &Attribute{}, 450 hcl.EmptyBody(), 451 cty.NilVal, 452 0, 453 }, 454 "nil": { 455 nil, 456 hcl.EmptyBody(), 457 cty.NilVal, 458 0, 459 }, 460 "optional string (null)": { 461 &Attribute{ 462 Type: cty.String, 463 Optional: true, 464 }, 465 hcltest.MockBody(&hcl.BodyContent{}), 466 cty.NullVal(cty.String), 467 0, 468 }, 469 "optional string": { 470 &Attribute{ 471 Type: cty.String, 472 Optional: true, 473 }, 474 hcltest.MockBody(&hcl.BodyContent{ 475 Attributes: hcl.Attributes{ 476 "attr": { 477 Name: "attr", 478 Expr: hcltest.MockExprLiteral(cty.StringVal("bar")), 479 }, 480 }, 481 }), 482 cty.StringVal("bar"), 483 0, 484 }, 485 "NestedType with required string": { 486 &Attribute{ 487 NestedType: &Object{ 488 Nesting: NestingSingle, 489 Attributes: map[string]*Attribute{ 490 "foo": { 491 Type: cty.String, 492 Required: true, 493 }, 494 }, 495 }, 496 Optional: true, 497 }, 498 hcltest.MockBody(&hcl.BodyContent{ 499 Attributes: hcl.Attributes{ 500 "attr": { 501 Name: "attr", 502 Expr: hcltest.MockExprLiteral(cty.ObjectVal(map[string]cty.Value{ 503 "foo": cty.StringVal("bar"), 504 })), 505 }, 506 }, 507 }), 508 cty.ObjectVal(map[string]cty.Value{ 509 "foo": cty.StringVal("bar"), 510 }), 511 0, 512 }, 513 "NestedType with optional attributes": { 514 &Attribute{ 515 NestedType: &Object{ 516 Nesting: NestingSingle, 517 Attributes: map[string]*Attribute{ 518 "foo": { 519 Type: cty.String, 520 Optional: true, 521 }, 522 "bar": { 523 Type: cty.String, 524 Optional: true, 525 }, 526 }, 527 }, 528 Optional: true, 529 }, 530 hcltest.MockBody(&hcl.BodyContent{ 531 Attributes: hcl.Attributes{ 532 "attr": { 533 Name: "attr", 534 Expr: hcltest.MockExprLiteral(cty.ObjectVal(map[string]cty.Value{ 535 "foo": cty.StringVal("bar"), 536 })), 537 }, 538 }, 539 }), 540 cty.ObjectVal(map[string]cty.Value{ 541 "foo": cty.StringVal("bar"), 542 "bar": cty.NullVal(cty.String), 543 }), 544 0, 545 }, 546 "NestedType with missing required string": { 547 &Attribute{ 548 NestedType: &Object{ 549 Nesting: NestingSingle, 550 Attributes: map[string]*Attribute{ 551 "foo": { 552 Type: cty.String, 553 Required: true, 554 }, 555 }, 556 }, 557 Optional: true, 558 }, 559 hcltest.MockBody(&hcl.BodyContent{ 560 Attributes: hcl.Attributes{ 561 "attr": { 562 Name: "attr", 563 Expr: hcltest.MockExprLiteral(cty.EmptyObjectVal), 564 }, 565 }, 566 }), 567 cty.UnknownVal(cty.Object(map[string]cty.Type{ 568 "foo": cty.String, 569 })), 570 1, 571 }, 572 // NestedModes 573 "NestedType NestingModeList valid": { 574 &Attribute{ 575 NestedType: &Object{ 576 Nesting: NestingList, 577 Attributes: map[string]*Attribute{ 578 "foo": { 579 Type: cty.String, 580 Required: true, 581 }, 582 }, 583 }, 584 Optional: true, 585 }, 586 hcltest.MockBody(&hcl.BodyContent{ 587 Attributes: hcl.Attributes{ 588 "attr": { 589 Name: "attr", 590 Expr: hcltest.MockExprLiteral(cty.ListVal([]cty.Value{ 591 cty.ObjectVal(map[string]cty.Value{ 592 "foo": cty.StringVal("bar"), 593 }), 594 cty.ObjectVal(map[string]cty.Value{ 595 "foo": cty.StringVal("baz"), 596 }), 597 })), 598 }, 599 }, 600 }), 601 cty.ListVal([]cty.Value{ 602 cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("bar")}), 603 cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("baz")}), 604 }), 605 0, 606 }, 607 "NestedType NestingModeList invalid": { 608 &Attribute{ 609 NestedType: &Object{ 610 Nesting: NestingList, 611 Attributes: map[string]*Attribute{ 612 "foo": { 613 Type: cty.String, 614 Required: true, 615 }, 616 }, 617 }, 618 Optional: true, 619 }, 620 hcltest.MockBody(&hcl.BodyContent{ 621 Attributes: hcl.Attributes{ 622 "attr": { 623 Name: "attr", 624 Expr: hcltest.MockExprLiteral(cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ 625 // "foo" should be a string, not a list 626 "foo": cty.ListVal([]cty.Value{cty.StringVal("bar"), cty.StringVal("baz")}), 627 })})), 628 }, 629 }, 630 }), 631 cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{"foo": cty.String}))), 632 1, 633 }, 634 "NestedType NestingModeSet valid": { 635 &Attribute{ 636 NestedType: &Object{ 637 Nesting: NestingSet, 638 Attributes: map[string]*Attribute{ 639 "foo": { 640 Type: cty.String, 641 Required: true, 642 }, 643 }, 644 }, 645 Optional: true, 646 }, 647 hcltest.MockBody(&hcl.BodyContent{ 648 Attributes: hcl.Attributes{ 649 "attr": { 650 Name: "attr", 651 Expr: hcltest.MockExprLiteral(cty.SetVal([]cty.Value{ 652 cty.ObjectVal(map[string]cty.Value{ 653 "foo": cty.StringVal("bar"), 654 }), 655 cty.ObjectVal(map[string]cty.Value{ 656 "foo": cty.StringVal("baz"), 657 }), 658 })), 659 }, 660 }, 661 }), 662 cty.SetVal([]cty.Value{ 663 cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("bar")}), 664 cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("baz")}), 665 }), 666 0, 667 }, 668 "NestedType NestingModeSet invalid": { 669 &Attribute{ 670 NestedType: &Object{ 671 Nesting: NestingSet, 672 Attributes: map[string]*Attribute{ 673 "foo": { 674 Type: cty.String, 675 Required: true, 676 }, 677 }, 678 }, 679 Optional: true, 680 }, 681 hcltest.MockBody(&hcl.BodyContent{ 682 Attributes: hcl.Attributes{ 683 "attr": { 684 Name: "attr", 685 Expr: hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ 686 // "foo" should be a string, not a list 687 "foo": cty.ListVal([]cty.Value{cty.StringVal("bar"), cty.StringVal("baz")}), 688 })})), 689 }, 690 }, 691 }), 692 cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{"foo": cty.String}))), 693 1, 694 }, 695 "NestedType NestingModeMap valid": { 696 &Attribute{ 697 NestedType: &Object{ 698 Nesting: NestingMap, 699 Attributes: map[string]*Attribute{ 700 "foo": { 701 Type: cty.String, 702 Required: true, 703 }, 704 }, 705 }, 706 Optional: true, 707 }, 708 hcltest.MockBody(&hcl.BodyContent{ 709 Attributes: hcl.Attributes{ 710 "attr": { 711 Name: "attr", 712 Expr: hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{ 713 "one": cty.ObjectVal(map[string]cty.Value{ 714 "foo": cty.StringVal("bar"), 715 }), 716 "two": cty.ObjectVal(map[string]cty.Value{ 717 "foo": cty.StringVal("baz"), 718 }), 719 })), 720 }, 721 }, 722 }), 723 cty.MapVal(map[string]cty.Value{ 724 "one": cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("bar")}), 725 "two": cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("baz")}), 726 }), 727 0, 728 }, 729 "NestedType NestingModeMap invalid": { 730 &Attribute{ 731 NestedType: &Object{ 732 Nesting: NestingMap, 733 Attributes: map[string]*Attribute{ 734 "foo": { 735 Type: cty.String, 736 Required: true, 737 }, 738 }, 739 }, 740 Optional: true, 741 }, 742 hcltest.MockBody(&hcl.BodyContent{ 743 Attributes: hcl.Attributes{ 744 "attr": { 745 Name: "attr", 746 Expr: hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{ 747 "one": cty.ObjectVal(map[string]cty.Value{ 748 // "foo" should be a string, not a list 749 "foo": cty.ListVal([]cty.Value{cty.StringVal("bar"), cty.StringVal("baz")}), 750 }), 751 })), 752 }, 753 }, 754 }), 755 cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{"foo": cty.String}))), 756 1, 757 }, 758 "deeply NestedType NestingModeList valid": { 759 &Attribute{ 760 NestedType: &Object{ 761 Nesting: NestingList, 762 Attributes: map[string]*Attribute{ 763 "foo": { 764 NestedType: &Object{ 765 Nesting: NestingList, 766 Attributes: map[string]*Attribute{ 767 "bar": { 768 Type: cty.String, 769 Required: true, 770 }, 771 }, 772 }, 773 Required: true, 774 }, 775 }, 776 }, 777 Optional: true, 778 }, 779 hcltest.MockBody(&hcl.BodyContent{ 780 Attributes: hcl.Attributes{ 781 "attr": { 782 Name: "attr", 783 Expr: hcltest.MockExprLiteral(cty.ListVal([]cty.Value{ 784 cty.ObjectVal(map[string]cty.Value{ 785 "foo": cty.ListVal([]cty.Value{ 786 cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("baz")}), 787 cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("boz")}), 788 }), 789 }), 790 cty.ObjectVal(map[string]cty.Value{ 791 "foo": cty.ListVal([]cty.Value{ 792 cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("biz")}), 793 cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("buz")}), 794 }), 795 }), 796 })), 797 }, 798 }, 799 }), 800 cty.ListVal([]cty.Value{ 801 cty.ObjectVal(map[string]cty.Value{"foo": cty.ListVal([]cty.Value{ 802 cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("baz")}), 803 cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("boz")}), 804 })}), 805 cty.ObjectVal(map[string]cty.Value{"foo": cty.ListVal([]cty.Value{ 806 cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("biz")}), 807 cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("buz")}), 808 })}), 809 }), 810 0, 811 }, 812 "deeply NestedType NestingList invalid": { 813 &Attribute{ 814 NestedType: &Object{ 815 Nesting: NestingList, 816 Attributes: map[string]*Attribute{ 817 "foo": { 818 NestedType: &Object{ 819 Nesting: NestingList, 820 Attributes: map[string]*Attribute{ 821 "bar": { 822 Type: cty.Number, 823 Required: true, 824 }, 825 }, 826 }, 827 Required: true, 828 }, 829 }, 830 }, 831 Optional: true, 832 }, 833 hcltest.MockBody(&hcl.BodyContent{ 834 Attributes: hcl.Attributes{ 835 "attr": { 836 Name: "attr", 837 Expr: hcltest.MockExprLiteral(cty.ListVal([]cty.Value{ 838 cty.ObjectVal(map[string]cty.Value{ 839 "foo": cty.ListVal([]cty.Value{ 840 // bar should be a Number 841 cty.ObjectVal(map[string]cty.Value{"bar": cty.True}), 842 cty.ObjectVal(map[string]cty.Value{"bar": cty.False}), 843 }), 844 }), 845 })), 846 }, 847 }, 848 }), 849 cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{ 850 "foo": cty.List(cty.Object(map[string]cty.Type{"bar": cty.Number})), 851 }))), 852 1, 853 }, 854 } 855 856 for name, test := range tests { 857 t.Run(name, func(t *testing.T) { 858 spec := test.Schema.decoderSpec("attr") 859 got, diags := hcldec.Decode(test.TestBody, spec, nil) 860 if len(diags) != test.DiagCount { 861 t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.DiagCount) 862 for _, diag := range diags { 863 t.Logf("- %s", diag.Error()) 864 } 865 } 866 867 if !got.RawEquals(test.Want) { 868 t.Logf("[INFO] implied schema is %s", spew.Sdump(hcldec.ImpliedSchema(spec))) 869 t.Errorf("wrong result\ngot: %s\nwant: %s", dump.Value(got), dump.Value(test.Want)) 870 } 871 }) 872 } 873 874 } 875 876 // TestAttributeDecodeSpec_panic is a temporary test which verifies that 877 // decoderSpec panics when an invalid Attribute schema is encountered. It will 878 // be removed when InternalValidate() is extended to validate Attribute specs 879 // (and is used). See the #FIXME in decoderSpec. 880 func TestAttributeDecoderSpec_panic(t *testing.T) { 881 attrS := &Attribute{ 882 Type: cty.Object(map[string]cty.Type{ 883 "nested_attribute": cty.String, 884 }), 885 NestedType: &Object{}, 886 Optional: true, 887 } 888 889 defer func() { recover() }() 890 attrS.decoderSpec("attr") 891 t.Errorf("expected panic") 892 } 893 894 func TestListOptionalAttrsFromObject(t *testing.T) { 895 tests := []struct { 896 input *Object 897 want []string 898 }{ 899 { 900 nil, 901 []string{}, 902 }, 903 { 904 &Object{}, 905 []string{}, 906 }, 907 { 908 &Object{ 909 Nesting: NestingSingle, 910 Attributes: map[string]*Attribute{ 911 "optional": {Type: cty.String, Optional: true}, 912 "required": {Type: cty.Number, Required: true}, 913 "computed": {Type: cty.List(cty.Bool), Computed: true}, 914 "optional_computed": {Type: cty.Map(cty.Bool), Optional: true, Computed: true}, 915 }, 916 }, 917 []string{"optional", "computed", "optional_computed"}, 918 }, 919 } 920 921 for _, test := range tests { 922 got := listOptionalAttrsFromObject(test.input) 923 924 // order is irrelevant 925 sort.Strings(got) 926 sort.Strings(test.want) 927 928 if diff := cmp.Diff(got, test.want); diff != "" { 929 t.Fatalf("wrong result: %s\n", diff) 930 } 931 } 932 }