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  }