mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-04 13:17:43 -06:00
0d80a74539
* configs/configschema: fix missing "computed" attributes from NestedObject's ImpliedType listOptionalAttrsFromObject was not including "computed" attributes in the list of optional object attributes. This is now fixed. I've also added some tests and fixed some panics and otherwise bad behavior when bad input is given. One natable change is in ImpliedType, which was panicking on an invalid nesting mode. The comment expressly states that it will return a result even when the schema is inconsistent, so I removed the panic and instead return an empty object.
930 lines
22 KiB
Go
930 lines
22 KiB
Go
package configschema
|
|
|
|
import (
|
|
"sort"
|
|
"testing"
|
|
|
|
"github.com/apparentlymart/go-dump/dump"
|
|
"github.com/davecgh/go-spew/spew"
|
|
"github.com/google/go-cmp/cmp"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/hcldec"
|
|
"github.com/hashicorp/hcl/v2/hcltest"
|
|
"github.com/zclconf/go-cty/cty"
|
|
)
|
|
|
|
func TestBlockDecoderSpec(t *testing.T) {
|
|
tests := map[string]struct {
|
|
Schema *Block
|
|
TestBody hcl.Body
|
|
Want cty.Value
|
|
DiagCount int
|
|
}{
|
|
"empty": {
|
|
&Block{},
|
|
hcl.EmptyBody(),
|
|
cty.EmptyObjectVal,
|
|
0,
|
|
},
|
|
"nil": {
|
|
nil,
|
|
hcl.EmptyBody(),
|
|
cty.EmptyObjectVal,
|
|
0,
|
|
},
|
|
"attributes": {
|
|
&Block{
|
|
Attributes: map[string]*Attribute{
|
|
"optional": {
|
|
Type: cty.Number,
|
|
Optional: true,
|
|
},
|
|
"required": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
"computed": {
|
|
Type: cty.List(cty.Bool),
|
|
Computed: true,
|
|
},
|
|
"optional_computed": {
|
|
Type: cty.Map(cty.Bool),
|
|
Optional: true,
|
|
Computed: true,
|
|
},
|
|
"optional_computed_overridden": {
|
|
Type: cty.Bool,
|
|
Optional: true,
|
|
Computed: true,
|
|
},
|
|
"optional_computed_unknown": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
Computed: true,
|
|
},
|
|
},
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcl.Attributes{
|
|
"required": {
|
|
Name: "required",
|
|
Expr: hcltest.MockExprLiteral(cty.NumberIntVal(5)),
|
|
},
|
|
"optional_computed_overridden": {
|
|
Name: "optional_computed_overridden",
|
|
Expr: hcltest.MockExprLiteral(cty.True),
|
|
},
|
|
"optional_computed_unknown": {
|
|
Name: "optional_computed_overridden",
|
|
Expr: hcltest.MockExprLiteral(cty.UnknownVal(cty.String)),
|
|
},
|
|
},
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"optional": cty.NullVal(cty.Number),
|
|
"required": cty.StringVal("5"), // converted from number to string
|
|
"computed": cty.NullVal(cty.List(cty.Bool)),
|
|
"optional_computed": cty.NullVal(cty.Map(cty.Bool)),
|
|
"optional_computed_overridden": cty.True,
|
|
"optional_computed_unknown": cty.UnknownVal(cty.String),
|
|
}),
|
|
0,
|
|
},
|
|
"dynamically-typed attribute": {
|
|
&Block{
|
|
Attributes: map[string]*Attribute{
|
|
"foo": {
|
|
Type: cty.DynamicPseudoType, // any type is permitted
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcl.Attributes{
|
|
"foo": {
|
|
Name: "foo",
|
|
Expr: hcltest.MockExprLiteral(cty.True),
|
|
},
|
|
},
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.True,
|
|
}),
|
|
0,
|
|
},
|
|
"dynamically-typed attribute omitted": {
|
|
&Block{
|
|
Attributes: map[string]*Attribute{
|
|
"foo": {
|
|
Type: cty.DynamicPseudoType, // any type is permitted
|
|
Optional: true,
|
|
},
|
|
},
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.NullVal(cty.DynamicPseudoType),
|
|
}),
|
|
0,
|
|
},
|
|
"required attribute omitted": {
|
|
&Block{
|
|
Attributes: map[string]*Attribute{
|
|
"foo": {
|
|
Type: cty.Bool,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.NullVal(cty.Bool),
|
|
}),
|
|
1, // missing required attribute
|
|
},
|
|
"wrong attribute type": {
|
|
&Block{
|
|
Attributes: map[string]*Attribute{
|
|
"optional": {
|
|
Type: cty.Number,
|
|
Optional: true,
|
|
},
|
|
},
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcl.Attributes{
|
|
"optional": {
|
|
Name: "optional",
|
|
Expr: hcltest.MockExprLiteral(cty.True),
|
|
},
|
|
},
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"optional": cty.UnknownVal(cty.Number),
|
|
}),
|
|
1, // incorrect type; number required
|
|
},
|
|
"blocks": {
|
|
&Block{
|
|
BlockTypes: map[string]*NestedBlock{
|
|
"single": {
|
|
Nesting: NestingSingle,
|
|
Block: Block{},
|
|
},
|
|
"list": {
|
|
Nesting: NestingList,
|
|
Block: Block{},
|
|
},
|
|
"set": {
|
|
Nesting: NestingSet,
|
|
Block: Block{},
|
|
},
|
|
"map": {
|
|
Nesting: NestingMap,
|
|
Block: Block{},
|
|
},
|
|
},
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Blocks: hcl.Blocks{
|
|
&hcl.Block{
|
|
Type: "list",
|
|
Body: hcl.EmptyBody(),
|
|
},
|
|
&hcl.Block{
|
|
Type: "single",
|
|
Body: hcl.EmptyBody(),
|
|
},
|
|
&hcl.Block{
|
|
Type: "list",
|
|
Body: hcl.EmptyBody(),
|
|
},
|
|
&hcl.Block{
|
|
Type: "set",
|
|
Body: hcl.EmptyBody(),
|
|
},
|
|
&hcl.Block{
|
|
Type: "map",
|
|
Labels: []string{"foo"},
|
|
LabelRanges: []hcl.Range{hcl.Range{}},
|
|
Body: hcl.EmptyBody(),
|
|
},
|
|
&hcl.Block{
|
|
Type: "map",
|
|
Labels: []string{"bar"},
|
|
LabelRanges: []hcl.Range{hcl.Range{}},
|
|
Body: hcl.EmptyBody(),
|
|
},
|
|
&hcl.Block{
|
|
Type: "set",
|
|
Body: hcl.EmptyBody(),
|
|
},
|
|
},
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"single": cty.EmptyObjectVal,
|
|
"list": cty.ListVal([]cty.Value{
|
|
cty.EmptyObjectVal,
|
|
cty.EmptyObjectVal,
|
|
}),
|
|
"set": cty.SetVal([]cty.Value{
|
|
cty.EmptyObjectVal,
|
|
cty.EmptyObjectVal,
|
|
}),
|
|
"map": cty.MapVal(map[string]cty.Value{
|
|
"foo": cty.EmptyObjectVal,
|
|
"bar": cty.EmptyObjectVal,
|
|
}),
|
|
}),
|
|
0,
|
|
},
|
|
"blocks with dynamically-typed attributes": {
|
|
&Block{
|
|
BlockTypes: map[string]*NestedBlock{
|
|
"single": {
|
|
Nesting: NestingSingle,
|
|
Block: Block{
|
|
Attributes: map[string]*Attribute{
|
|
"a": {
|
|
Type: cty.DynamicPseudoType,
|
|
Optional: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"list": {
|
|
Nesting: NestingList,
|
|
Block: Block{
|
|
Attributes: map[string]*Attribute{
|
|
"a": {
|
|
Type: cty.DynamicPseudoType,
|
|
Optional: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"map": {
|
|
Nesting: NestingMap,
|
|
Block: Block{
|
|
Attributes: map[string]*Attribute{
|
|
"a": {
|
|
Type: cty.DynamicPseudoType,
|
|
Optional: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Blocks: hcl.Blocks{
|
|
&hcl.Block{
|
|
Type: "list",
|
|
Body: hcl.EmptyBody(),
|
|
},
|
|
&hcl.Block{
|
|
Type: "single",
|
|
Body: hcl.EmptyBody(),
|
|
},
|
|
&hcl.Block{
|
|
Type: "list",
|
|
Body: hcl.EmptyBody(),
|
|
},
|
|
&hcl.Block{
|
|
Type: "map",
|
|
Labels: []string{"foo"},
|
|
LabelRanges: []hcl.Range{hcl.Range{}},
|
|
Body: hcl.EmptyBody(),
|
|
},
|
|
&hcl.Block{
|
|
Type: "map",
|
|
Labels: []string{"bar"},
|
|
LabelRanges: []hcl.Range{hcl.Range{}},
|
|
Body: hcl.EmptyBody(),
|
|
},
|
|
},
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"single": cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.NullVal(cty.DynamicPseudoType),
|
|
}),
|
|
"list": cty.TupleVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.NullVal(cty.DynamicPseudoType),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.NullVal(cty.DynamicPseudoType),
|
|
}),
|
|
}),
|
|
"map": cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.NullVal(cty.DynamicPseudoType),
|
|
}),
|
|
"bar": cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.NullVal(cty.DynamicPseudoType),
|
|
}),
|
|
}),
|
|
}),
|
|
0,
|
|
},
|
|
"too many list items": {
|
|
&Block{
|
|
BlockTypes: map[string]*NestedBlock{
|
|
"foo": {
|
|
Nesting: NestingList,
|
|
Block: Block{},
|
|
MaxItems: 1,
|
|
},
|
|
},
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Blocks: hcl.Blocks{
|
|
&hcl.Block{
|
|
Type: "foo",
|
|
Body: hcl.EmptyBody(),
|
|
},
|
|
&hcl.Block{
|
|
Type: "foo",
|
|
Body: unknownBody{hcl.EmptyBody()},
|
|
},
|
|
},
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.UnknownVal(cty.List(cty.EmptyObject)),
|
|
}),
|
|
0, // max items cannot be validated during decode
|
|
},
|
|
// dynamic blocks may fulfill MinItems, but there is only one block to
|
|
// decode.
|
|
"required MinItems": {
|
|
&Block{
|
|
BlockTypes: map[string]*NestedBlock{
|
|
"foo": {
|
|
Nesting: NestingList,
|
|
Block: Block{},
|
|
MinItems: 2,
|
|
},
|
|
},
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Blocks: hcl.Blocks{
|
|
&hcl.Block{
|
|
Type: "foo",
|
|
Body: unknownBody{hcl.EmptyBody()},
|
|
},
|
|
},
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.UnknownVal(cty.List(cty.EmptyObject)),
|
|
}),
|
|
0,
|
|
},
|
|
"extraneous attribute": {
|
|
&Block{},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcl.Attributes{
|
|
"extra": {
|
|
Name: "extra",
|
|
Expr: hcltest.MockExprLiteral(cty.StringVal("hello")),
|
|
},
|
|
},
|
|
}),
|
|
cty.EmptyObjectVal,
|
|
1, // extraneous attribute
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
spec := test.Schema.DecoderSpec()
|
|
|
|
got, diags := hcldec.Decode(test.TestBody, spec, nil)
|
|
if len(diags) != test.DiagCount {
|
|
t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.DiagCount)
|
|
for _, diag := range diags {
|
|
t.Logf("- %s", diag.Error())
|
|
}
|
|
}
|
|
|
|
if !got.RawEquals(test.Want) {
|
|
t.Logf("[INFO] implied schema is %s", spew.Sdump(hcldec.ImpliedSchema(spec)))
|
|
t.Errorf("wrong result\ngot: %s\nwant: %s", dump.Value(got), dump.Value(test.Want))
|
|
}
|
|
|
|
// Double-check that we're producing consistent results for DecoderSpec
|
|
// and ImpliedType.
|
|
impliedType := test.Schema.ImpliedType()
|
|
if errs := got.Type().TestConformance(impliedType); len(errs) != 0 {
|
|
t.Errorf("result does not conform to the schema's implied type")
|
|
for _, err := range errs {
|
|
t.Logf("- %s", err.Error())
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// this satisfies hcldec.UnknownBody to simulate a dynamic block with an
|
|
// unknown number of values.
|
|
type unknownBody struct {
|
|
hcl.Body
|
|
}
|
|
|
|
func (b unknownBody) Unknown() bool {
|
|
return true
|
|
}
|
|
|
|
func TestAttributeDecoderSpec(t *testing.T) {
|
|
tests := map[string]struct {
|
|
Schema *Attribute
|
|
TestBody hcl.Body
|
|
Want cty.Value
|
|
DiagCount int
|
|
}{
|
|
"empty": {
|
|
&Attribute{},
|
|
hcl.EmptyBody(),
|
|
cty.NilVal,
|
|
0,
|
|
},
|
|
"nil": {
|
|
nil,
|
|
hcl.EmptyBody(),
|
|
cty.NilVal,
|
|
0,
|
|
},
|
|
"optional string (null)": {
|
|
&Attribute{
|
|
Type: cty.String,
|
|
Optional: true,
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{}),
|
|
cty.NullVal(cty.String),
|
|
0,
|
|
},
|
|
"optional string": {
|
|
&Attribute{
|
|
Type: cty.String,
|
|
Optional: true,
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcl.Attributes{
|
|
"attr": {
|
|
Name: "attr",
|
|
Expr: hcltest.MockExprLiteral(cty.StringVal("bar")),
|
|
},
|
|
},
|
|
}),
|
|
cty.StringVal("bar"),
|
|
0,
|
|
},
|
|
"NestedType with required string": {
|
|
&Attribute{
|
|
NestedType: &Object{
|
|
Nesting: NestingSingle,
|
|
Attributes: map[string]*Attribute{
|
|
"foo": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
Optional: true,
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcl.Attributes{
|
|
"attr": {
|
|
Name: "attr",
|
|
Expr: hcltest.MockExprLiteral(cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("bar"),
|
|
})),
|
|
},
|
|
},
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("bar"),
|
|
}),
|
|
0,
|
|
},
|
|
"NestedType with optional attributes": {
|
|
&Attribute{
|
|
NestedType: &Object{
|
|
Nesting: NestingSingle,
|
|
Attributes: map[string]*Attribute{
|
|
"foo": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
},
|
|
"bar": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
},
|
|
},
|
|
},
|
|
Optional: true,
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcl.Attributes{
|
|
"attr": {
|
|
Name: "attr",
|
|
Expr: hcltest.MockExprLiteral(cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("bar"),
|
|
})),
|
|
},
|
|
},
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("bar"),
|
|
"bar": cty.NullVal(cty.String),
|
|
}),
|
|
0,
|
|
},
|
|
"NestedType with missing required string": {
|
|
&Attribute{
|
|
NestedType: &Object{
|
|
Nesting: NestingSingle,
|
|
Attributes: map[string]*Attribute{
|
|
"foo": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
Optional: true,
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcl.Attributes{
|
|
"attr": {
|
|
Name: "attr",
|
|
Expr: hcltest.MockExprLiteral(cty.EmptyObjectVal),
|
|
},
|
|
},
|
|
}),
|
|
cty.UnknownVal(cty.Object(map[string]cty.Type{
|
|
"foo": cty.String,
|
|
})),
|
|
1,
|
|
},
|
|
// NestedModes
|
|
"NestedType NestingModeList valid": {
|
|
&Attribute{
|
|
NestedType: &Object{
|
|
Nesting: NestingList,
|
|
Attributes: map[string]*Attribute{
|
|
"foo": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
Optional: true,
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcl.Attributes{
|
|
"attr": {
|
|
Name: "attr",
|
|
Expr: hcltest.MockExprLiteral(cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("bar"),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("baz"),
|
|
}),
|
|
})),
|
|
},
|
|
},
|
|
}),
|
|
cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("bar")}),
|
|
cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("baz")}),
|
|
}),
|
|
0,
|
|
},
|
|
"NestedType NestingModeList invalid": {
|
|
&Attribute{
|
|
NestedType: &Object{
|
|
Nesting: NestingList,
|
|
Attributes: map[string]*Attribute{
|
|
"foo": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
Optional: true,
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcl.Attributes{
|
|
"attr": {
|
|
Name: "attr",
|
|
Expr: hcltest.MockExprLiteral(cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
|
|
// "foo" should be a string, not a list
|
|
"foo": cty.ListVal([]cty.Value{cty.StringVal("bar"), cty.StringVal("baz")}),
|
|
})})),
|
|
},
|
|
},
|
|
}),
|
|
cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{"foo": cty.String}))),
|
|
1,
|
|
},
|
|
"NestedType NestingModeSet valid": {
|
|
&Attribute{
|
|
NestedType: &Object{
|
|
Nesting: NestingSet,
|
|
Attributes: map[string]*Attribute{
|
|
"foo": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
Optional: true,
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcl.Attributes{
|
|
"attr": {
|
|
Name: "attr",
|
|
Expr: hcltest.MockExprLiteral(cty.SetVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("bar"),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("baz"),
|
|
}),
|
|
})),
|
|
},
|
|
},
|
|
}),
|
|
cty.SetVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("bar")}),
|
|
cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("baz")}),
|
|
}),
|
|
0,
|
|
},
|
|
"NestedType NestingModeSet invalid": {
|
|
&Attribute{
|
|
NestedType: &Object{
|
|
Nesting: NestingSet,
|
|
Attributes: map[string]*Attribute{
|
|
"foo": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
Optional: true,
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcl.Attributes{
|
|
"attr": {
|
|
Name: "attr",
|
|
Expr: hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
|
|
// "foo" should be a string, not a list
|
|
"foo": cty.ListVal([]cty.Value{cty.StringVal("bar"), cty.StringVal("baz")}),
|
|
})})),
|
|
},
|
|
},
|
|
}),
|
|
cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{"foo": cty.String}))),
|
|
1,
|
|
},
|
|
"NestedType NestingModeMap valid": {
|
|
&Attribute{
|
|
NestedType: &Object{
|
|
Nesting: NestingMap,
|
|
Attributes: map[string]*Attribute{
|
|
"foo": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
Optional: true,
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcl.Attributes{
|
|
"attr": {
|
|
Name: "attr",
|
|
Expr: hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{
|
|
"one": cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("bar"),
|
|
}),
|
|
"two": cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("baz"),
|
|
}),
|
|
})),
|
|
},
|
|
},
|
|
}),
|
|
cty.MapVal(map[string]cty.Value{
|
|
"one": cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("bar")}),
|
|
"two": cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("baz")}),
|
|
}),
|
|
0,
|
|
},
|
|
"NestedType NestingModeMap invalid": {
|
|
&Attribute{
|
|
NestedType: &Object{
|
|
Nesting: NestingMap,
|
|
Attributes: map[string]*Attribute{
|
|
"foo": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
Optional: true,
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcl.Attributes{
|
|
"attr": {
|
|
Name: "attr",
|
|
Expr: hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{
|
|
"one": cty.ObjectVal(map[string]cty.Value{
|
|
// "foo" should be a string, not a list
|
|
"foo": cty.ListVal([]cty.Value{cty.StringVal("bar"), cty.StringVal("baz")}),
|
|
}),
|
|
})),
|
|
},
|
|
},
|
|
}),
|
|
cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{"foo": cty.String}))),
|
|
1,
|
|
},
|
|
"deeply NestedType NestingModeList valid": {
|
|
&Attribute{
|
|
NestedType: &Object{
|
|
Nesting: NestingList,
|
|
Attributes: map[string]*Attribute{
|
|
"foo": {
|
|
NestedType: &Object{
|
|
Nesting: NestingList,
|
|
Attributes: map[string]*Attribute{
|
|
"bar": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
Optional: true,
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcl.Attributes{
|
|
"attr": {
|
|
Name: "attr",
|
|
Expr: hcltest.MockExprLiteral(cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("baz")}),
|
|
cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("boz")}),
|
|
}),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("biz")}),
|
|
cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("buz")}),
|
|
}),
|
|
}),
|
|
})),
|
|
},
|
|
},
|
|
}),
|
|
cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{"foo": cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("baz")}),
|
|
cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("boz")}),
|
|
})}),
|
|
cty.ObjectVal(map[string]cty.Value{"foo": cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("biz")}),
|
|
cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("buz")}),
|
|
})}),
|
|
}),
|
|
0,
|
|
},
|
|
"deeply NestedType NestingList invalid": {
|
|
&Attribute{
|
|
NestedType: &Object{
|
|
Nesting: NestingList,
|
|
Attributes: map[string]*Attribute{
|
|
"foo": {
|
|
NestedType: &Object{
|
|
Nesting: NestingList,
|
|
Attributes: map[string]*Attribute{
|
|
"bar": {
|
|
Type: cty.Number,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
Optional: true,
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcl.Attributes{
|
|
"attr": {
|
|
Name: "attr",
|
|
Expr: hcltest.MockExprLiteral(cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.ListVal([]cty.Value{
|
|
// bar should be a Number
|
|
cty.ObjectVal(map[string]cty.Value{"bar": cty.True}),
|
|
cty.ObjectVal(map[string]cty.Value{"bar": cty.False}),
|
|
}),
|
|
}),
|
|
})),
|
|
},
|
|
},
|
|
}),
|
|
cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{
|
|
"foo": cty.List(cty.Object(map[string]cty.Type{"bar": cty.Number})),
|
|
}))),
|
|
1,
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
spec := test.Schema.decoderSpec("attr")
|
|
got, diags := hcldec.Decode(test.TestBody, spec, nil)
|
|
if len(diags) != test.DiagCount {
|
|
t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.DiagCount)
|
|
for _, diag := range diags {
|
|
t.Logf("- %s", diag.Error())
|
|
}
|
|
}
|
|
|
|
if !got.RawEquals(test.Want) {
|
|
t.Logf("[INFO] implied schema is %s", spew.Sdump(hcldec.ImpliedSchema(spec)))
|
|
t.Errorf("wrong result\ngot: %s\nwant: %s", dump.Value(got), dump.Value(test.Want))
|
|
}
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
// TestAttributeDecodeSpec_panic is a temporary test which verifies that
|
|
// decoderSpec panics when an invalid Attribute schema is encountered. It will
|
|
// be removed when InternalValidate() is extended to validate Attribute specs
|
|
// (and is used). See the #FIXME in decoderSpec.
|
|
func TestAttributeDecoderSpec_panic(t *testing.T) {
|
|
attrS := &Attribute{
|
|
Type: cty.Object(map[string]cty.Type{
|
|
"nested_attribute": cty.String,
|
|
}),
|
|
NestedType: &Object{},
|
|
Optional: true,
|
|
}
|
|
|
|
defer func() { recover() }()
|
|
attrS.decoderSpec("attr")
|
|
t.Errorf("expected panic")
|
|
}
|
|
|
|
func TestListOptionalAttrsFromObject(t *testing.T) {
|
|
tests := []struct {
|
|
input *Object
|
|
want []string
|
|
}{
|
|
{
|
|
nil,
|
|
[]string{},
|
|
},
|
|
{
|
|
&Object{},
|
|
[]string{},
|
|
},
|
|
{
|
|
&Object{
|
|
Nesting: NestingSingle,
|
|
Attributes: map[string]*Attribute{
|
|
"optional": {Type: cty.String, Optional: true},
|
|
"required": {Type: cty.Number, Required: true},
|
|
"computed": {Type: cty.List(cty.Bool), Computed: true},
|
|
"optional_computed": {Type: cty.Map(cty.Bool), Optional: true, Computed: true},
|
|
},
|
|
},
|
|
[]string{"optional", "computed", "optional_computed"},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
got := listOptionalAttrsFromObject(test.input)
|
|
|
|
// order is irrelevant
|
|
sort.Strings(got)
|
|
sort.Strings(test.want)
|
|
|
|
if diff := cmp.Diff(got, test.want); diff != "" {
|
|
t.Fatalf("wrong result: %s\n", diff)
|
|
}
|
|
}
|
|
}
|