mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-28 17:34:24 -06:00
13e2e10577
We can only validate MinItems >= 1 (equiv to "Required") during decoding, as dynamic blocks each only decode as a single block. MaxItems cannot be validated at all, also because of dynamic blocks, which may have any number of blocks in the config.
429 lines
9.6 KiB
Go
429 lines
9.6 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,
|
|
}),
|
|
}),
|
|
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: hcl.EmptyBody(),
|
|
},
|
|
},
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.ListVal([]cty.Value{
|
|
cty.EmptyObjectVal,
|
|
}),
|
|
}),
|
|
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())
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|