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  }