terraform/internal/lang/funcs/defaults_test.go

649 lines
18 KiB
Go

package funcs
import (
"fmt"
"testing"
"github.com/zclconf/go-cty/cty"
)
func TestDefaults(t *testing.T) {
tests := []struct {
Input, Defaults cty.Value
Want cty.Value
WantErr string
}{
{ // When *either* input or default are unknown, an unknown is returned.
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.UnknownVal(cty.String),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("hello"),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"a": cty.UnknownVal(cty.String),
}),
},
{
// When *either* input or default are unknown, an unknown is
// returned with marks from both input and defaults.
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.UnknownVal(cty.String),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("hello").Mark("marked"),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"a": cty.UnknownVal(cty.String).Mark("marked"),
}),
},
{
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.NullVal(cty.String),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("hello"),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("hello"),
}),
},
{
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("hey"),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("hello"),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("hey"),
}),
},
{
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.NullVal(cty.String),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"a": cty.NullVal(cty.String),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"a": cty.NullVal(cty.String),
}),
},
{
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.NullVal(cty.String),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{}),
Want: cty.ObjectVal(map[string]cty.Value{
"a": cty.NullVal(cty.String),
}),
},
{
Input: cty.ObjectVal(map[string]cty.Value{}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"a": cty.NullVal(cty.String),
}),
WantErr: `.a: target type does not expect an attribute named "a"`,
},
{
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.NullVal(cty.String),
}),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("hello"),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.StringVal("hello"),
}),
}),
},
{
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.NullVal(cty.String),
cty.StringVal("hey"),
cty.NullVal(cty.String),
}),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("hello"),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.StringVal("hello"),
cty.StringVal("hey"),
cty.StringVal("hello"),
}),
}),
},
{
// Using defaults with single set elements is a pretty
// odd thing to do, but this behavior is just here because
// it generalizes from how we handle collections. It's
// tested only to ensure it doesn't change accidentally
// in future.
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.SetVal([]cty.Value{
cty.NullVal(cty.String),
cty.StringVal("hey"),
}),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("hello"),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"a": cty.SetVal([]cty.Value{
cty.StringVal("hey"),
cty.StringVal("hello"),
}),
}),
},
{
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.MapVal(map[string]cty.Value{
"x": cty.NullVal(cty.String),
"y": cty.StringVal("hey"),
"z": cty.NullVal(cty.String),
}),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("hello"),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"a": cty.MapVal(map[string]cty.Value{
"x": cty.StringVal("hello"),
"y": cty.StringVal("hey"),
"z": cty.StringVal("hello"),
}),
}),
},
{
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("hey"),
}),
cty.ObjectVal(map[string]cty.Value{
"b": cty.NullVal(cty.String),
}),
cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("hey"),
}),
}),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"a": cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("hello"),
}),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("hey"),
}),
cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("hello"),
}),
cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("hey"),
}),
}),
}),
},
{
Input: cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("hey"),
}),
cty.ObjectVal(map[string]cty.Value{
"b": cty.NullVal(cty.String),
}),
cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("hey"),
}),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("hello"),
}),
Want: cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("hey"),
}),
cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("hello"),
}),
cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("hey"),
}),
}),
},
{
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("boop"),
}),
cty.ObjectVal(map[string]cty.Value{
"b": cty.NullVal(cty.String),
}),
cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("hey"),
}),
}),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"a": cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("hello"),
}),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"a": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("boop"),
}),
cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("hello"),
}),
cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("hey"),
}),
}),
}),
},
{
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("hello"),
}),
cty.ObjectVal(map[string]cty.Value{
"b": cty.NullVal(cty.String),
}),
}),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"a": cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("hello"),
}),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"a": cty.SetVal([]cty.Value{
// After applying defaults, the one with a null value
// coalesced with the one with a non-null value,
// and so there's only one left.
cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("hello"),
}),
}),
}),
},
{
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.MapVal(map[string]cty.Value{
"boop": cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("hey"),
}),
"beep": cty.ObjectVal(map[string]cty.Value{
"b": cty.NullVal(cty.String),
}),
}),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"a": cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("hello"),
}),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"a": cty.MapVal(map[string]cty.Value{
"boop": cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("hey"),
}),
"beep": cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("hello"),
}),
}),
}),
},
{
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("hey"),
}),
cty.ObjectVal(map[string]cty.Value{
"b": cty.NullVal(cty.String),
}),
cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("hey"),
}),
}),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("hello"),
}),
WantErr: `.a: the default value for a collection of an object type must itself be an object type, not string`,
},
{
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.NullVal(cty.String),
cty.StringVal("hey"),
cty.NullVal(cty.String),
}),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
// The default value for a list must be a single value
// of the list's element type which provides defaults
// for each element separately, so the default for a
// list of string should be just a single string, not
// a list of string.
"a": cty.ListVal([]cty.Value{
cty.StringVal("hello"),
}),
}),
WantErr: `.a: invalid default value for string: string required`,
},
{
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.TupleVal([]cty.Value{
cty.NullVal(cty.String),
cty.StringVal("hey"),
cty.NullVal(cty.String),
}),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("hello"),
}),
WantErr: `.a: the default value for a tuple type must itself be a tuple type, not string`,
},
{
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.TupleVal([]cty.Value{
cty.NullVal(cty.String),
cty.StringVal("hey"),
cty.NullVal(cty.String),
}),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"a": cty.TupleVal([]cty.Value{
cty.StringVal("hello 0"),
cty.StringVal("hello 1"),
cty.StringVal("hello 2"),
}),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"a": cty.TupleVal([]cty.Value{
cty.StringVal("hello 0"),
cty.StringVal("hey"),
cty.StringVal("hello 2"),
}),
}),
},
{
// There's no reason to use this function for plain primitive
// types, because the "default" argument in a variable definition
// already has the equivalent behavior. This function is only
// to deal with the situation of a complex-typed variable where
// only parts of the data structure are optional.
Input: cty.NullVal(cty.String),
Defaults: cty.StringVal("hello"),
WantErr: `only object types and collections of object types can have defaults applied`,
},
// When applying default values to structural types, null objects or
// tuples in the input should be passed through.
{
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.NullVal(cty.Object(map[string]cty.Type{
"x": cty.String,
"y": cty.String,
})),
"b": cty.NullVal(cty.Tuple([]cty.Type{cty.String, cty.String})),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"a": cty.ObjectVal(map[string]cty.Value{
"x": cty.StringVal("hello"),
"y": cty.StringVal("there"),
}),
"b": cty.TupleVal([]cty.Value{
cty.StringVal("how are"),
cty.StringVal("you?"),
}),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"a": cty.NullVal(cty.Object(map[string]cty.Type{
"x": cty.String,
"y": cty.String,
})),
"b": cty.NullVal(cty.Tuple([]cty.Type{cty.String, cty.String})),
}),
},
// When applying default values to structural types, we permit null
// values in the defaults, and just pass through the input value.
{
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"p": cty.StringVal("xyz"),
"q": cty.StringVal("xyz"),
}),
}),
"b": cty.SetVal([]cty.Value{
cty.TupleVal([]cty.Value{
cty.NumberIntVal(0),
cty.NumberIntVal(2),
}),
cty.TupleVal([]cty.Value{
cty.NumberIntVal(1),
cty.NumberIntVal(3),
}),
}),
"c": cty.NullVal(cty.String),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"c": cty.StringVal("tada"),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"p": cty.StringVal("xyz"),
"q": cty.StringVal("xyz"),
}),
}),
"b": cty.SetVal([]cty.Value{
cty.TupleVal([]cty.Value{
cty.NumberIntVal(0),
cty.NumberIntVal(2),
}),
cty.TupleVal([]cty.Value{
cty.NumberIntVal(1),
cty.NumberIntVal(3),
}),
}),
"c": cty.StringVal("tada"),
}),
},
// When applying default values to collection types, null collections in the
// input should result in empty collections in the output.
{
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.NullVal(cty.List(cty.String)),
"b": cty.NullVal(cty.Map(cty.String)),
"c": cty.NullVal(cty.Set(cty.String)),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("hello"),
"b": cty.StringVal("hi"),
"c": cty.StringVal("greetings"),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"a": cty.ListValEmpty(cty.String),
"b": cty.MapValEmpty(cty.String),
"c": cty.SetValEmpty(cty.String),
}),
},
// When specifying fallbacks, we allow mismatched primitive attribute
// types so long as a safe conversion is possible. This means that we
// can accept number or boolean values for string attributes.
{
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.NullVal(cty.String),
"b": cty.NullVal(cty.String),
"c": cty.NullVal(cty.String),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"a": cty.NumberIntVal(5),
"b": cty.True,
"c": cty.StringVal("greetings"),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("5"),
"b": cty.StringVal("true"),
"c": cty.StringVal("greetings"),
}),
},
// Fallbacks with mismatched primitive attribute types which do not
// have safe conversions must not pass the suitable fallback check,
// even if unsafe conversion would be possible.
{
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.NullVal(cty.Bool),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("5"),
}),
WantErr: ".a: invalid default value for bool: bool required",
},
// marks: we should preserve marks from both input value and defaults as leafily as possible
{
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.NullVal(cty.String),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("hello").Mark("world"),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("hello").Mark("world"),
}),
},
{ // "unused" marks don't carry over
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.NullVal(cty.String).Mark("a"),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("hello"),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("hello"),
}),
},
{ // Marks on tuples remain attached to individual elements
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.TupleVal([]cty.Value{
cty.NullVal(cty.String),
cty.StringVal("hey").Mark("input"),
cty.NullVal(cty.String),
}),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"a": cty.TupleVal([]cty.Value{
cty.StringVal("hello 0").Mark("fallback"),
cty.StringVal("hello 1"),
cty.StringVal("hello 2"),
}),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"a": cty.TupleVal([]cty.Value{
cty.StringVal("hello 0").Mark("fallback"),
cty.StringVal("hey").Mark("input"),
cty.StringVal("hello 2"),
}),
}),
},
{ // Marks from list elements
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.NullVal(cty.String),
cty.StringVal("hey").Mark("input"),
cty.NullVal(cty.String),
}),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("hello 0").Mark("fallback"),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.StringVal("hello 0").Mark("fallback"),
cty.StringVal("hey").Mark("input"),
cty.StringVal("hello 0").Mark("fallback"),
}),
}),
},
{
// Sets don't allow individually-marked elements, so the marks
// end up aggregating on the set itself anyway in this case.
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.SetVal([]cty.Value{
cty.NullVal(cty.String),
cty.NullVal(cty.String),
cty.StringVal("hey").Mark("input"),
}),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("hello 0").Mark("fallback"),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"a": cty.SetVal([]cty.Value{
cty.StringVal("hello 0"),
cty.StringVal("hey"),
cty.StringVal("hello 0"),
}).WithMarks(cty.NewValueMarks("fallback", "input")),
}),
},
{
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.NullVal(cty.String),
}),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("hello").Mark("beep"),
}).Mark("boop"),
// This is the least-intuitive case. The mark "boop" is attached to
// the default object, not it's elements, but both marks end up
// aggregated on the list element.
Want: cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.StringVal("hello").WithMarks(cty.NewValueMarks("beep", "boop")),
}),
}),
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("defaults(%#v, %#v)", test.Input, test.Defaults), func(t *testing.T) {
got, gotErr := Defaults(test.Input, test.Defaults)
if test.WantErr != "" {
if gotErr == nil {
t.Fatalf("unexpected success\nwant error: %s", test.WantErr)
}
if got, want := gotErr.Error(), test.WantErr; got != want {
t.Fatalf("wrong error\ngot: %s\nwant: %s", got, want)
}
return
} else if gotErr != nil {
t.Fatalf("unexpected error\ngot: %s", gotErr.Error())
}
if !test.Want.RawEquals(got) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}