terraform/plans/objchange/plan_valid_test.go

577 lines
14 KiB
Go
Raw Normal View History

package objchange
import (
"testing"
"github.com/apparentlymart/go-dump/dump"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/tfdiags"
)
func TestAssertPlanValid(t *testing.T) {
tests := map[string]struct {
Schema *configschema.Block
Prior cty.Value
Config cty.Value
Planned cty.Value
WantErrs []string
}{
"all empty": {
&configschema.Block{},
cty.EmptyObjectVal,
cty.EmptyObjectVal,
cty.EmptyObjectVal,
nil,
},
"no computed, all match": {
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"a": {
Type: cty.String,
Optional: true,
},
},
BlockTypes: map[string]*configschema.NestedBlock{
"b": {
Nesting: configschema.NestingList,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"c": {
Type: cty.String,
Optional: true,
},
},
},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("a value"),
"b": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"c": cty.StringVal("c value"),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("a value"),
"b": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"c": cty.StringVal("c value"),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("a value"),
"b": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"c": cty.StringVal("c value"),
}),
}),
}),
nil,
},
"no computed, plan matches, no prior": {
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"a": {
Type: cty.String,
Optional: true,
},
},
BlockTypes: map[string]*configschema.NestedBlock{
"b": {
Nesting: configschema.NestingList,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"c": {
Type: cty.String,
Optional: true,
},
},
},
},
},
},
cty.NullVal(cty.Object(map[string]cty.Type{
"a": cty.String,
"b": cty.List(cty.Object(map[string]cty.Type{
"c": cty.String,
})),
})),
cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("a value"),
"b": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"c": cty.StringVal("c value"),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("a value"),
"b": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"c": cty.StringVal("c value"),
}),
}),
}),
nil,
},
"no computed, invalid change in plan": {
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"a": {
Type: cty.String,
Optional: true,
},
},
BlockTypes: map[string]*configschema.NestedBlock{
"b": {
Nesting: configschema.NestingList,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"c": {
Type: cty.String,
Optional: true,
},
},
},
},
},
},
cty.NullVal(cty.Object(map[string]cty.Type{
"a": cty.String,
"b": cty.List(cty.Object(map[string]cty.Type{
"c": cty.String,
})),
})),
cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("a value"),
"b": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"c": cty.StringVal("c value"),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("a value"),
"b": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"c": cty.StringVal("new c value"),
}),
}),
}),
[]string{
`.b[0].c: planned value cty.StringVal("new c value") does not match config value cty.StringVal("c value")`,
},
},
"no computed, invalid change in plan sensitive": {
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"a": {
Type: cty.String,
Optional: true,
},
},
BlockTypes: map[string]*configschema.NestedBlock{
"b": {
Nesting: configschema.NestingList,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"c": {
Type: cty.String,
Optional: true,
Sensitive: true,
},
},
},
},
},
},
cty.NullVal(cty.Object(map[string]cty.Type{
"a": cty.String,
"b": cty.List(cty.Object(map[string]cty.Type{
"c": cty.String,
})),
})),
cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("a value"),
"b": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"c": cty.StringVal("c value"),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("a value"),
"b": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"c": cty.StringVal("new c value"),
}),
}),
}),
[]string{
`.b[0].c: sensitive planned value does not match config value`,
},
},
"no computed, diff suppression in plan": {
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"a": {
Type: cty.String,
Optional: true,
},
},
BlockTypes: map[string]*configschema.NestedBlock{
"b": {
Nesting: configschema.NestingList,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"c": {
Type: cty.String,
Optional: true,
},
},
},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("a value"),
"b": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"c": cty.StringVal("c value"),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("a value"),
"b": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"c": cty.StringVal("new c value"),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("a value"),
"b": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"c": cty.StringVal("c value"), // plan uses value from prior object
}),
}),
}),
nil,
},
"no computed, all null": {
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"a": {
Type: cty.String,
Optional: true,
},
},
BlockTypes: map[string]*configschema.NestedBlock{
"b": {
Nesting: configschema.NestingList,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"c": {
Type: cty.String,
Optional: true,
},
},
},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"a": cty.NullVal(cty.String),
"b": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"c": cty.NullVal(cty.String),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"a": cty.NullVal(cty.String),
"b": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"c": cty.NullVal(cty.String),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"a": cty.NullVal(cty.String),
"b": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"c": cty.NullVal(cty.String),
}),
}),
}),
nil,
},
// Nested block collections are never null
"nested list, null in plan": {
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"b": {
Nesting: configschema.NestingList,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"c": {
Type: cty.String,
Optional: true,
},
},
},
},
},
},
cty.NullVal(cty.Object(map[string]cty.Type{
"b": cty.List(cty.Object(map[string]cty.Type{
"c": cty.String,
})),
})),
cty.ObjectVal(map[string]cty.Value{
"b": cty.ListValEmpty(cty.Object(map[string]cty.Type{
"c": cty.String,
})),
}),
cty.ObjectVal(map[string]cty.Value{
"b": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{
"c": cty.String,
}))),
}),
[]string{
`.b: attribute representing a list of nested blocks must be empty to indicate no blocks, not null`,
},
},
"nested set, null in plan": {
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"b": {
Nesting: configschema.NestingSet,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"c": {
Type: cty.String,
Optional: true,
},
},
},
},
},
},
cty.NullVal(cty.Object(map[string]cty.Type{
"b": cty.Set(cty.Object(map[string]cty.Type{
"c": cty.String,
})),
})),
cty.ObjectVal(map[string]cty.Value{
"b": cty.SetValEmpty(cty.Object(map[string]cty.Type{
"c": cty.String,
})),
}),
cty.ObjectVal(map[string]cty.Value{
"b": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{
"c": cty.String,
}))),
}),
[]string{
`.b: attribute representing a set of nested blocks must be empty to indicate no blocks, not null`,
},
},
"nested map, null in plan": {
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"b": {
Nesting: configschema.NestingMap,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"c": {
Type: cty.String,
Optional: true,
},
},
},
},
},
},
cty.NullVal(cty.Object(map[string]cty.Type{
"b": cty.Map(cty.Object(map[string]cty.Type{
"c": cty.String,
})),
})),
cty.ObjectVal(map[string]cty.Value{
"b": cty.MapValEmpty(cty.Object(map[string]cty.Type{
"c": cty.String,
})),
}),
cty.ObjectVal(map[string]cty.Value{
"b": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{
"c": cty.String,
}))),
}),
[]string{
`.b: attribute representing a map of nested blocks must be empty to indicate no blocks, not null`,
},
},
// We don't actually do any validation for nested set blocks, and so
// the remaining cases here are just intending to ensure we don't
// inadvertently start generating errors incorrectly in future.
"nested set, no computed, no changes": {
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"b": {
Nesting: configschema.NestingSet,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"c": {
Type: cty.String,
Optional: true,
},
},
},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"b": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"c": cty.StringVal("c value"),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"b": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"c": cty.StringVal("c value"),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"b": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"c": cty.StringVal("c value"),
}),
}),
}),
nil,
},
"nested set, no computed, invalid change in plan": {
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"b": {
Nesting: configschema.NestingSet,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"c": {
Type: cty.String,
Optional: true,
},
},
},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"b": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"c": cty.StringVal("c value"),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"b": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"c": cty.StringVal("c value"),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"b": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"c": cty.StringVal("new c value"), // matches neither prior nor config
}),
}),
}),
nil,
},
"nested set, no computed, diff suppressed": {
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"b": {
Nesting: configschema.NestingSet,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"c": {
Type: cty.String,
Optional: true,
},
},
},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"b": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"c": cty.StringVal("c value"),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"b": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"c": cty.StringVal("new c value"),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"b": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"c": cty.StringVal("c value"), // plan uses value from prior object
}),
}),
}),
nil,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
errs := AssertPlanValid(test.Schema, test.Prior, test.Config, test.Planned)
wantErrs := make(map[string]struct{})
gotErrs := make(map[string]struct{})
for _, err := range errs {
gotErrs[tfdiags.FormatError(err)] = struct{}{}
}
for _, msg := range test.WantErrs {
wantErrs[msg] = struct{}{}
}
t.Logf(
"\nprior: %sconfig: %splanned: %s",
dump.Value(test.Planned),
dump.Value(test.Config),
dump.Value(test.Planned),
)
for msg := range wantErrs {
if _, ok := gotErrs[msg]; !ok {
t.Errorf("missing expected error: %s", msg)
}
}
for msg := range gotErrs {
if _, ok := wantErrs[msg]; !ok {
t.Errorf("unexpected extra error: %s", msg)
}
}
})
}
}