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