mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-28 18:01:01 -06:00
731d4226d3
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.
565 lines
12 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|