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