opentofu/configs/configschema/coerce_value_test.go
James Bardin 67dbd6d345 don't check MinItems with unknowns in blocks
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.
2019-07-27 11:50:28 -07:00

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)
}
})
}
}