terraform/configs/configschema/coerce_value_test.go

565 lines
12 KiB
Go

package configschema
import (
"testing"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/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)
}
})
}
}