opentofu/internal/lang/funcs/defaults_test.go
Martin Atkins cdd9464f9a Move lang/ to internal/lang/
This is part of a general effort to move all of Terraform's non-library
package surface under internal in order to reinforce that these are for
internal use within Terraform only.

If you were previously importing packages under this prefix into an
external codebase, you could pin to an earlier release tag as an interim
solution until you've make a plan to achieve the same functionality some
other way.
2021-05-17 14:09:07 -07:00

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