mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
If a block was defined via "dynamic", there will be only one block value until the expansion is known. Since we can't detect dynamic blocks at this point, don't verify MinItems while there are unknown values in the config. The decoder spec can also only check for existence of a block, so limit the check to 0 or 1.
591 lines
13 KiB
Go
591 lines
13 KiB
Go
package configschema
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
"github.com/hashicorp/terraform/tfdiags"
|
|
)
|
|
|
|
func TestCoerceValue(t *testing.T) {
|
|
tests := map[string]struct {
|
|
Schema *Block
|
|
Input cty.Value
|
|
WantValue cty.Value
|
|
WantErr string
|
|
}{
|
|
"empty schema and value": {
|
|
&Block{},
|
|
cty.EmptyObjectVal,
|
|
cty.EmptyObjectVal,
|
|
``,
|
|
},
|
|
"attribute present": {
|
|
&Block{
|
|
Attributes: map[string]*Attribute{
|
|
"foo": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.True,
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("true"),
|
|
}),
|
|
``,
|
|
},
|
|
"single block present": {
|
|
&Block{
|
|
BlockTypes: map[string]*NestedBlock{
|
|
"foo": {
|
|
Block: Block{},
|
|
Nesting: NestingSingle,
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.EmptyObjectVal,
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.EmptyObjectVal,
|
|
}),
|
|
``,
|
|
},
|
|
"single block wrong type": {
|
|
&Block{
|
|
BlockTypes: map[string]*NestedBlock{
|
|
"foo": {
|
|
Block: Block{},
|
|
Nesting: NestingSingle,
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.True,
|
|
}),
|
|
cty.DynamicVal,
|
|
`.foo: an object is required`,
|
|
},
|
|
"list block with one item": {
|
|
&Block{
|
|
BlockTypes: map[string]*NestedBlock{
|
|
"foo": {
|
|
Block: Block{},
|
|
Nesting: NestingList,
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.ListVal([]cty.Value{cty.EmptyObjectVal}),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.ListVal([]cty.Value{cty.EmptyObjectVal}),
|
|
}),
|
|
``,
|
|
},
|
|
"set block with one item": {
|
|
&Block{
|
|
BlockTypes: map[string]*NestedBlock{
|
|
"foo": {
|
|
Block: Block{},
|
|
Nesting: NestingSet,
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.ListVal([]cty.Value{cty.EmptyObjectVal}), // can implicitly convert to set
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.SetVal([]cty.Value{cty.EmptyObjectVal}),
|
|
}),
|
|
``,
|
|
},
|
|
"map block with one item": {
|
|
&Block{
|
|
BlockTypes: map[string]*NestedBlock{
|
|
"foo": {
|
|
Block: Block{},
|
|
Nesting: NestingMap,
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.MapVal(map[string]cty.Value{"foo": cty.EmptyObjectVal}),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.MapVal(map[string]cty.Value{"foo": cty.EmptyObjectVal}),
|
|
}),
|
|
``,
|
|
},
|
|
"list block with one item having an attribute": {
|
|
&Block{
|
|
BlockTypes: map[string]*NestedBlock{
|
|
"foo": {
|
|
Block: Block{
|
|
Attributes: map[string]*Attribute{
|
|
"bar": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
Nesting: NestingList,
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
|
|
"bar": cty.StringVal("hello"),
|
|
})}),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
|
|
"bar": cty.StringVal("hello"),
|
|
})}),
|
|
}),
|
|
``,
|
|
},
|
|
"list block with one item having a missing attribute": {
|
|
&Block{
|
|
BlockTypes: map[string]*NestedBlock{
|
|
"foo": {
|
|
Block: Block{
|
|
Attributes: map[string]*Attribute{
|
|
"bar": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
Nesting: NestingList,
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.ListVal([]cty.Value{cty.EmptyObjectVal}),
|
|
}),
|
|
cty.DynamicVal,
|
|
`.foo[0]: attribute "bar" is required`,
|
|
},
|
|
"list block with one item having an extraneous attribute": {
|
|
&Block{
|
|
BlockTypes: map[string]*NestedBlock{
|
|
"foo": {
|
|
Block: Block{},
|
|
Nesting: NestingList,
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
|
|
"bar": cty.StringVal("hello"),
|
|
})}),
|
|
}),
|
|
cty.DynamicVal,
|
|
`.foo[0]: unexpected attribute "bar"`,
|
|
},
|
|
"missing optional attribute": {
|
|
&Block{
|
|
Attributes: map[string]*Attribute{
|
|
"foo": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
},
|
|
},
|
|
},
|
|
cty.EmptyObjectVal,
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.NullVal(cty.String),
|
|
}),
|
|
``,
|
|
},
|
|
"missing optional single block": {
|
|
&Block{
|
|
BlockTypes: map[string]*NestedBlock{
|
|
"foo": {
|
|
Block: Block{},
|
|
Nesting: NestingSingle,
|
|
},
|
|
},
|
|
},
|
|
cty.EmptyObjectVal,
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.NullVal(cty.EmptyObject),
|
|
}),
|
|
``,
|
|
},
|
|
"missing optional list block": {
|
|
&Block{
|
|
BlockTypes: map[string]*NestedBlock{
|
|
"foo": {
|
|
Block: Block{},
|
|
Nesting: NestingList,
|
|
},
|
|
},
|
|
},
|
|
cty.EmptyObjectVal,
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.ListValEmpty(cty.EmptyObject),
|
|
}),
|
|
``,
|
|
},
|
|
"missing optional set block": {
|
|
&Block{
|
|
BlockTypes: map[string]*NestedBlock{
|
|
"foo": {
|
|
Block: Block{},
|
|
Nesting: NestingSet,
|
|
},
|
|
},
|
|
},
|
|
cty.EmptyObjectVal,
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.SetValEmpty(cty.EmptyObject),
|
|
}),
|
|
``,
|
|
},
|
|
"missing optional map block": {
|
|
&Block{
|
|
BlockTypes: map[string]*NestedBlock{
|
|
"foo": {
|
|
Block: Block{},
|
|
Nesting: NestingMap,
|
|
},
|
|
},
|
|
},
|
|
cty.EmptyObjectVal,
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.MapValEmpty(cty.EmptyObject),
|
|
}),
|
|
``,
|
|
},
|
|
"missing required attribute": {
|
|
&Block{
|
|
Attributes: map[string]*Attribute{
|
|
"foo": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
cty.EmptyObjectVal,
|
|
cty.DynamicVal,
|
|
`attribute "foo" is required`,
|
|
},
|
|
"missing required single block": {
|
|
&Block{
|
|
BlockTypes: map[string]*NestedBlock{
|
|
"foo": {
|
|
Block: Block{},
|
|
Nesting: NestingSingle,
|
|
MinItems: 1,
|
|
MaxItems: 1,
|
|
},
|
|
},
|
|
},
|
|
cty.EmptyObjectVal,
|
|
cty.DynamicVal,
|
|
`attribute "foo" is required`,
|
|
},
|
|
"missing required list block": {
|
|
&Block{
|
|
BlockTypes: map[string]*NestedBlock{
|
|
"foo": {
|
|
Block: Block{},
|
|
Nesting: NestingList,
|
|
MinItems: 1,
|
|
},
|
|
},
|
|
},
|
|
cty.EmptyObjectVal,
|
|
cty.DynamicVal,
|
|
`attribute "foo" is required`,
|
|
},
|
|
"missing required set block": {
|
|
&Block{
|
|
BlockTypes: map[string]*NestedBlock{
|
|
"foo": {
|
|
Block: Block{},
|
|
Nesting: NestingList,
|
|
MinItems: 1,
|
|
},
|
|
},
|
|
},
|
|
cty.EmptyObjectVal,
|
|
cty.DynamicVal,
|
|
`attribute "foo" is required`,
|
|
},
|
|
"unknown nested list": {
|
|
&Block{
|
|
Attributes: map[string]*Attribute{
|
|
"attr": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
},
|
|
BlockTypes: map[string]*NestedBlock{
|
|
"foo": {
|
|
Block: Block{},
|
|
Nesting: NestingList,
|
|
MinItems: 2,
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("test"),
|
|
"foo": cty.UnknownVal(cty.EmptyObject),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("test"),
|
|
"foo": cty.UnknownVal(cty.List(cty.EmptyObject)),
|
|
}),
|
|
"",
|
|
},
|
|
"unknowns in nested list": {
|
|
&Block{
|
|
BlockTypes: map[string]*NestedBlock{
|
|
"foo": {
|
|
Block: Block{
|
|
Attributes: map[string]*Attribute{
|
|
"attr": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
Nesting: NestingList,
|
|
MinItems: 2,
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.UnknownVal(cty.String),
|
|
}),
|
|
}),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.UnknownVal(cty.String),
|
|
}),
|
|
}),
|
|
}),
|
|
"",
|
|
},
|
|
"unknown nested set": {
|
|
&Block{
|
|
Attributes: map[string]*Attribute{
|
|
"attr": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
},
|
|
BlockTypes: map[string]*NestedBlock{
|
|
"foo": {
|
|
Block: Block{},
|
|
Nesting: NestingSet,
|
|
MinItems: 1,
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("test"),
|
|
"foo": cty.UnknownVal(cty.EmptyObject),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("test"),
|
|
"foo": cty.UnknownVal(cty.Set(cty.EmptyObject)),
|
|
}),
|
|
"",
|
|
},
|
|
"unknown nested map": {
|
|
&Block{
|
|
Attributes: map[string]*Attribute{
|
|
"attr": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
},
|
|
BlockTypes: map[string]*NestedBlock{
|
|
"foo": {
|
|
Block: Block{},
|
|
Nesting: NestingMap,
|
|
MinItems: 1,
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("test"),
|
|
"foo": cty.UnknownVal(cty.Map(cty.String)),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("test"),
|
|
"foo": cty.UnknownVal(cty.Map(cty.EmptyObject)),
|
|
}),
|
|
"",
|
|
},
|
|
"extraneous attribute": {
|
|
&Block{},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("bar"),
|
|
}),
|
|
cty.DynamicVal,
|
|
`unexpected attribute "foo"`,
|
|
},
|
|
"wrong attribute type": {
|
|
&Block{
|
|
Attributes: map[string]*Attribute{
|
|
"foo": {
|
|
Type: cty.Number,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.False,
|
|
}),
|
|
cty.DynamicVal,
|
|
`.foo: number required`,
|
|
},
|
|
"unset computed value": {
|
|
&Block{
|
|
Attributes: map[string]*Attribute{
|
|
"foo": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
Computed: true,
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.NullVal(cty.String),
|
|
}),
|
|
``,
|
|
},
|
|
"dynamic value attributes": {
|
|
&Block{
|
|
BlockTypes: map[string]*NestedBlock{
|
|
"foo": {
|
|
Nesting: NestingMap,
|
|
Block: Block{
|
|
Attributes: map[string]*Attribute{
|
|
"bar": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
Computed: true,
|
|
},
|
|
"baz": {
|
|
Type: cty.DynamicPseudoType,
|
|
Optional: true,
|
|
Computed: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.ObjectVal(map[string]cty.Value{
|
|
"bar": cty.StringVal("beep"),
|
|
}),
|
|
"b": cty.ObjectVal(map[string]cty.Value{
|
|
"bar": cty.StringVal("boop"),
|
|
"baz": cty.NumberIntVal(8),
|
|
}),
|
|
}),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.ObjectVal(map[string]cty.Value{
|
|
"bar": cty.StringVal("beep"),
|
|
"baz": cty.NullVal(cty.DynamicPseudoType),
|
|
}),
|
|
"b": cty.ObjectVal(map[string]cty.Value{
|
|
"bar": cty.StringVal("boop"),
|
|
"baz": cty.NumberIntVal(8),
|
|
}),
|
|
}),
|
|
}),
|
|
``,
|
|
},
|
|
"dynamic attributes in map": {
|
|
// Convert a block represented as a map to an object if a
|
|
// DynamicPseudoType causes the element types to mismatch.
|
|
&Block{
|
|
BlockTypes: map[string]*NestedBlock{
|
|
"foo": {
|
|
Nesting: NestingMap,
|
|
Block: Block{
|
|
Attributes: map[string]*Attribute{
|
|
"bar": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
Computed: true,
|
|
},
|
|
"baz": {
|
|
Type: cty.DynamicPseudoType,
|
|
Optional: true,
|
|
Computed: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.MapVal(map[string]cty.Value{
|
|
"a": cty.ObjectVal(map[string]cty.Value{
|
|
"bar": cty.StringVal("beep"),
|
|
}),
|
|
"b": cty.ObjectVal(map[string]cty.Value{
|
|
"bar": cty.StringVal("boop"),
|
|
}),
|
|
}),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.ObjectVal(map[string]cty.Value{
|
|
"bar": cty.StringVal("beep"),
|
|
"baz": cty.NullVal(cty.DynamicPseudoType),
|
|
}),
|
|
"b": cty.ObjectVal(map[string]cty.Value{
|
|
"bar": cty.StringVal("boop"),
|
|
"baz": cty.NullVal(cty.DynamicPseudoType),
|
|
}),
|
|
}),
|
|
}),
|
|
``,
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
gotValue, gotErrObj := test.Schema.CoerceValue(test.Input)
|
|
|
|
if gotErrObj == nil {
|
|
if test.WantErr != "" {
|
|
t.Fatalf("coersion succeeded; want error: %q", test.WantErr)
|
|
}
|
|
} else {
|
|
gotErr := tfdiags.FormatError(gotErrObj)
|
|
if gotErr != test.WantErr {
|
|
t.Fatalf("wrong error\ngot: %s\nwant: %s", gotErr, test.WantErr)
|
|
}
|
|
return
|
|
}
|
|
|
|
if !gotValue.RawEquals(test.WantValue) {
|
|
t.Errorf("wrong result\ninput: %#v\ngot: %#v\nwant: %#v", test.Input, gotValue, test.WantValue)
|
|
}
|
|
})
|
|
}
|
|
}
|