opentofu/configs/configschema/decoder_spec_test.go
Martin Atkins 549544f201 configschema: Handle nested blocks containing dynamic-typed attributes
We need to make the collection itself be a tuple or object rather than
list or map in this case, since otherwise all of the elements of the
collection are constrained to be of the same type and that isn't the
intent of a provider indicating that it accepts any type.
2018-10-16 19:11:09 -07:00

402 lines
9.1 KiB
Go

package configschema
import (
"testing"
"github.com/apparentlymart/go-dump/dump"
"github.com/davecgh/go-spew/spew"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcldec"
"github.com/hashicorp/hcl2/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: hcl.EmptyBody(),
},
},
}),
cty.ObjectVal(map[string]cty.Value{
"foo": cty.ListVal([]cty.Value{
cty.EmptyObjectVal,
cty.EmptyObjectVal,
}),
}),
1, // too many "foo" blocks
},
"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())
}
}
})
}
}