github.com/kevinklinger/open_terraform@v1.3.6/noninternal/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  }