opentofu/plans/objchange/plan_valid_test.go
James Bardin cd7fb9bd5a catch invalidly planned attributes earlier
Catch attributes which are planed but not computed separately to provide
a clearer error to provider developers.

The error conditions were previously caught, however it was unclear from
the error text as to _why_ the change was an error. The statements about
value inequality would be incorrect when planning no changes for a value
which should not have been set in the first place.
2021-02-24 12:13:12 -05:00

1155 lines
28 KiB
Go

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 map, normal update": {
&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.ObjectVal(map[string]cty.Value{
"b": cty.MapVal(map[string]cty.Value{
"boop": cty.ObjectVal(map[string]cty.Value{
"c": cty.StringVal("hello"),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"b": cty.MapVal(map[string]cty.Value{
"boop": cty.ObjectVal(map[string]cty.Value{
"c": cty.StringVal("howdy"),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"b": cty.MapVal(map[string]cty.Value{
"boop": cty.ObjectVal(map[string]cty.Value{
"c": cty.StringVal("howdy"),
}),
}),
}),
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,
},
// Attributes with NestedTypes
"NestedType attr, no computed, all match": {
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"a": {
NestedType: &configschema.Object{
Nesting: configschema.NestingList,
Attributes: map[string]*configschema.Attribute{
"b": {
Type: cty.String,
Optional: true,
},
},
},
Optional: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("b value"),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("b value"),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("b value"),
}),
}),
}),
nil,
},
"NestedType attr, no computed, plan matches, no prior": {
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"a": {
NestedType: &configschema.Object{
Nesting: configschema.NestingList,
Attributes: map[string]*configschema.Attribute{
"b": {
Type: cty.String,
Optional: true,
},
},
},
Optional: true,
},
},
},
cty.NullVal(cty.Object(map[string]cty.Type{
"a": cty.List(cty.Object(map[string]cty.Type{
"b": cty.String,
})),
})),
cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("c value"),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("c value"),
}),
}),
}),
nil,
},
"NestedType, no computed, invalid change in plan": {
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"a": {
NestedType: &configschema.Object{
Nesting: configschema.NestingList,
Attributes: map[string]*configschema.Attribute{
"b": {
Type: cty.String,
Optional: true,
},
},
},
Optional: true,
},
},
},
cty.NullVal(cty.Object(map[string]cty.Type{
"a": cty.List(cty.Object(map[string]cty.Type{
"b": cty.String,
})),
})),
cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("c value"),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("new c value"),
}),
}),
}),
[]string{
`.a[0].b: planned value cty.StringVal("new c value") does not match config value cty.StringVal("c value")`,
},
},
"NestedType attr, no computed, invalid change in plan sensitive": {
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"a": {
NestedType: &configschema.Object{
Nesting: configschema.NestingList,
Attributes: map[string]*configschema.Attribute{
"b": {
Type: cty.String,
Optional: true,
Sensitive: true,
},
},
},
Optional: true,
},
},
},
cty.NullVal(cty.Object(map[string]cty.Type{
"a": cty.List(cty.Object(map[string]cty.Type{
"b": cty.String,
})),
})),
cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("b value"),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("new b value"),
}),
}),
}),
[]string{
`.a[0].b: sensitive planned value does not match config value`,
},
},
"NestedType attr, no computed, diff suppression in plan": {
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"a": {
NestedType: &configschema.Object{
Nesting: configschema.NestingList,
Attributes: map[string]*configschema.Attribute{
"b": {
Type: cty.String,
Optional: true,
},
},
},
Optional: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("b value"),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("new b value"),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"b": cty.StringVal("b value"), // plan uses value from prior object
}),
}),
}),
nil,
},
"NestedType attr, no computed, all null": {
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"a": {
NestedType: &configschema.Object{
Nesting: configschema.NestingList,
Attributes: map[string]*configschema.Attribute{
"b": {
Type: cty.String,
Optional: true,
},
},
},
Optional: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"a": cty.NullVal(cty.DynamicPseudoType),
}),
cty.ObjectVal(map[string]cty.Value{
"a": cty.NullVal(cty.DynamicPseudoType),
}),
cty.ObjectVal(map[string]cty.Value{
"a": cty.NullVal(cty.DynamicPseudoType),
}),
nil,
},
"NestedType attr, no computed, all zero value": {
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"a": {
NestedType: &configschema.Object{
Nesting: configschema.NestingList,
Attributes: map[string]*configschema.Attribute{
"b": {
Type: cty.String,
Optional: true,
},
},
},
Optional: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"a": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{
"b": cty.String,
}))),
}),
cty.ObjectVal(map[string]cty.Value{
"a": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{
"b": cty.String,
}))),
}),
cty.ObjectVal(map[string]cty.Value{
"a": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{
"b": cty.String,
}))),
}),
nil,
},
"NestedType NestingSet attribute to null": {
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"bloop": {
NestedType: &configschema.Object{
Nesting: configschema.NestingSet,
Attributes: map[string]*configschema.Attribute{
"blop": {
Type: cty.String,
Required: true,
},
},
},
Optional: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"bloop": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"blop": cty.StringVal("ok"),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"bloop": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{
"blop": cty.String,
}))),
}),
cty.ObjectVal(map[string]cty.Value{
"bloop": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{
"blop": cty.String,
}))),
}),
nil,
},
"NestedType deep nested optional set attribute to null": {
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"bleep": {
NestedType: &configschema.Object{
Nesting: configschema.NestingList,
Attributes: map[string]*configschema.Attribute{
"bloop": {
NestedType: &configschema.Object{
Nesting: configschema.NestingSet,
Attributes: map[string]*configschema.Attribute{
"blome": {
Type: cty.String,
Optional: true,
},
},
},
Optional: true,
},
},
},
Optional: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"bleep": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"bloop": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"blome": cty.StringVal("ok"),
}),
}),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"bleep": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"bloop": cty.NullVal(cty.Set(
cty.Object(map[string]cty.Type{
"blome": cty.String,
}),
)),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"bleep": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"bloop": cty.NullVal(cty.List(
cty.Object(map[string]cty.Type{
"blome": cty.String,
}),
)),
}),
}),
}),
nil,
},
"NestedType deep nested set": {
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"bleep": {
NestedType: &configschema.Object{
Nesting: configschema.NestingList,
Attributes: map[string]*configschema.Attribute{
"bloop": {
NestedType: &configschema.Object{
Nesting: configschema.NestingSet,
Attributes: map[string]*configschema.Attribute{
"blome": {
Type: cty.String,
Optional: true,
},
},
},
Optional: true,
},
},
},
Optional: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"bleep": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"bloop": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"blome": cty.StringVal("ok"),
}),
}),
}),
}),
}),
// Note: bloop is null in the config
cty.ObjectVal(map[string]cty.Value{
"bleep": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"bloop": cty.NullVal(cty.Set(
cty.Object(map[string]cty.Type{
"blome": cty.String,
}),
)),
}),
}),
}),
// provider sends back the prior value, not matching the config
cty.ObjectVal(map[string]cty.Value{
"bleep": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"bloop": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"blome": cty.StringVal("ok"),
}),
}),
}),
}),
}),
nil, // we cannot validate individual set elements, and trust the provider's response
},
"NestedType nested computed list attribute": {
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"bloop": {
NestedType: &configschema.Object{
Nesting: configschema.NestingList,
Attributes: map[string]*configschema.Attribute{
"blop": {
Type: cty.String,
Optional: true,
},
},
},
Computed: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"bloop": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"blop": cty.StringVal("ok"),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"bloop": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{
"blop": cty.String,
}))),
}),
cty.ObjectVal(map[string]cty.Value{
"bloop": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"blop": cty.StringVal("ok"),
}),
}),
}),
nil,
},
"NestedType nested list attribute to null": {
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"bloop": {
NestedType: &configschema.Object{
Nesting: configschema.NestingList,
Attributes: map[string]*configschema.Attribute{
"blop": {
Type: cty.String,
Optional: true,
},
},
},
Optional: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"bloop": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"blop": cty.StringVal("ok"),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"bloop": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{
"blop": cty.String,
}))),
}),
// provider returned the old value
cty.ObjectVal(map[string]cty.Value{
"bloop": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"blop": cty.StringVal("ok"),
}),
}),
}),
[]string{`.bloop: planned value cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{"blop":cty.StringVal("ok")})}) for a non-computed attribute`},
},
"NestedType nested set attribute to null": {
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"bloop": {
NestedType: &configschema.Object{
Nesting: configschema.NestingSet,
Attributes: map[string]*configschema.Attribute{
"blop": {
Type: cty.String,
Optional: true,
},
},
},
Optional: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"bloop": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"blop": cty.StringVal("ok"),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"bloop": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{
"blop": cty.String,
}))),
}),
// provider returned the old value
cty.ObjectVal(map[string]cty.Value{
"bloop": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"blop": cty.StringVal("ok"),
}),
}),
}),
[]string{`.bloop: planned value cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{"blop":cty.StringVal("ok")})}) for a non-computed attribute`},
},
}
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)
}
}
})
}
}