opentofu/configs/configschema/coerce_value_test.go
James Bardin 731d4226d3 do not validate Min/Max Items in CoerceValue
Due to both the nature of dynamic blocks, and the need for resources to
sometimes communicate incomplete values, we cannot validate MinItems and
MaxItems in CoerceValue.
2019-08-20 10:13:15 -04:00

565 lines
12 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.ObjectVal(map[string]cty.Value{
"foo": cty.NullVal(cty.EmptyObject),
}),
``,
},
"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)
}
})
}
}