// Copyright (c) The OpenTofu Authors // SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2023 HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package differ import ( "encoding/json" "fmt" "testing" "github.com/zclconf/go-cty/cty" ctyjson "github.com/zclconf/go-cty/cty/json" "github.com/opentofu/opentofu/internal/command/jsonformat/computed/renderers" "github.com/opentofu/opentofu/internal/command/jsonformat/structured" "github.com/opentofu/opentofu/internal/command/jsonformat/structured/attribute_path" "github.com/opentofu/opentofu/internal/command/jsonprovider" "github.com/opentofu/opentofu/internal/plans" ) type SetDiff struct { Before SetDiffEntry After SetDiffEntry } type SetDiffEntry struct { SingleDiff renderers.ValidateDiffFunction ObjectDiff map[string]renderers.ValidateDiffFunction Replace bool Action plans.Action } func (entry SetDiffEntry) Validate(obj func(attributes map[string]renderers.ValidateDiffFunction, action plans.Action, replace bool) renderers.ValidateDiffFunction) renderers.ValidateDiffFunction { if entry.SingleDiff != nil { return entry.SingleDiff } return obj(entry.ObjectDiff, entry.Action, entry.Replace) } func TestValue_SimpleBlocks(t *testing.T) { // Most of the other test functions wrap the test cases in various // collections or blocks. This function just very simply lets you specify // individual test cases within blocks for some simple tests. tcs := map[string]struct { input structured.Change block *jsonprovider.Block validate renderers.ValidateDiffFunction }{ "delete_with_null_sensitive_value": { input: structured.Change{ Before: map[string]interface{}{ "normal_attribute": "some value", }, After: nil, BeforeSensitive: map[string]interface{}{ "sensitive_attribute": true, }, AfterSensitive: false, }, block: &jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "normal_attribute": { AttributeType: unmarshalType(t, cty.String), }, "sensitive_attribute": { AttributeType: unmarshalType(t, cty.String), }, }, }, validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "normal_attribute": renderers.ValidatePrimitive("some value", nil, plans.Delete, false), }, nil, nil, nil, nil, plans.Delete, false), }, "create_with_null_sensitive_value": { input: structured.Change{ Before: nil, After: map[string]interface{}{ "normal_attribute": "some value", }, BeforeSensitive: map[string]interface{}{ "sensitive_attribute": true, }, AfterSensitive: false, }, block: &jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "normal_attribute": { AttributeType: unmarshalType(t, cty.String), }, "sensitive_attribute": { AttributeType: unmarshalType(t, cty.String), }, }, }, validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "normal_attribute": renderers.ValidatePrimitive(nil, "some value", plans.Create, false), }, nil, nil, nil, nil, plans.Create, false), }, "delete_with_write_only_value": { input: structured.Change{ Before: map[string]any{}, After: nil, BeforeSensitive: false, AfterSensitive: false, }, block: &jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "write_only_attribute": { AttributeType: unmarshalType(t, cty.String), WriteOnly: true, }, }, BlockTypes: map[string]*jsonprovider.BlockType{ "nested_with_write_only": { NestingMode: "single", Block: &jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "inner_write_only": { AttributeType: unmarshalType(t, cty.String), WriteOnly: true, }, }, }, }, }, }, validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "write_only_attribute": renderers.ValidateWriteOnly(plans.Delete, false), }, nil, nil, nil, nil, plans.Delete, false), }, "create_with_write_only_value": { input: structured.Change{ Before: nil, After: map[string]any{}, BeforeSensitive: false, AfterSensitive: false, }, block: &jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "write_only_attribute": { AttributeType: unmarshalType(t, cty.String), WriteOnly: true, }, }, BlockTypes: map[string]*jsonprovider.BlockType{ "nested_with_write_only": { NestingMode: "single", Block: &jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "inner_write_only": { AttributeType: unmarshalType(t, cty.String), WriteOnly: true, }, }, }, }, }, }, validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "write_only_attribute": renderers.ValidateWriteOnly(plans.Create, false), }, nil, nil, nil, nil, plans.Create, false), }, } for name, tc := range tcs { // Set some default values if tc.input.ReplacePaths == nil { tc.input.ReplacePaths = &attribute_path.PathMatcher{} } if tc.input.RelevantAttributes == nil { tc.input.RelevantAttributes = attribute_path.AlwaysMatcher() } t.Run(name, func(t *testing.T) { tc.validate(t, ComputeDiffForBlock(tc.input, tc.block)) }) } } func TestValue_ObjectAttributes(t *testing.T) { // This function holds a range of test cases creating, deleting and editing // objects. It is built in such a way that it can automatically test these // operations on objects both directly and nested, as well as within all // types of collections. tcs := map[string]struct { input structured.Change attributes map[string]cty.Type validateSingleDiff renderers.ValidateDiffFunction validateObject renderers.ValidateDiffFunction validateNestedObject renderers.ValidateDiffFunction validateDiffs map[string]renderers.ValidateDiffFunction validateList renderers.ValidateDiffFunction validateReplace bool validateAction plans.Action // Sets break changes out differently to the other collections, so they // have their own entry. validateSetDiffs *SetDiff }{ "create": { input: structured.Change{ Before: nil, After: map[string]interface{}{ "attribute_one": "new", }, }, attributes: map[string]cty.Type{ "attribute_one": cty.String, }, validateDiffs: map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive(nil, "new", plans.Create, false), }, validateAction: plans.Create, validateReplace: false, }, "delete": { input: structured.Change{ Before: map[string]interface{}{ "attribute_one": "old", }, After: nil, }, attributes: map[string]cty.Type{ "attribute_one": cty.String, }, validateDiffs: map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), }, validateAction: plans.Delete, validateReplace: false, }, "create_sensitive": { input: structured.Change{ Before: nil, After: map[string]interface{}{ "attribute_one": "new", }, AfterSensitive: true, }, attributes: map[string]cty.Type{ "attribute_one": cty.String, }, validateSingleDiff: renderers.ValidateSensitive(renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive(nil, "new", plans.Create, false), }, plans.Create, false), false, true, plans.Create, false), validateNestedObject: renderers.ValidateSensitive(renderers.ValidateNestedObject(map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive(nil, "new", plans.Create, false), }, plans.Create, false), false, true, plans.Create, false), }, "delete_sensitive": { input: structured.Change{ Before: map[string]interface{}{ "attribute_one": "old", }, BeforeSensitive: true, After: nil, }, attributes: map[string]cty.Type{ "attribute_one": cty.String, }, validateSingleDiff: renderers.ValidateSensitive(renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), }, plans.Delete, false), true, false, plans.Delete, false), validateNestedObject: renderers.ValidateSensitive(renderers.ValidateNestedObject(map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), }, plans.Delete, false), true, false, plans.Delete, false), }, "create_unknown": { input: structured.Change{ Before: nil, After: nil, Unknown: true, }, attributes: map[string]cty.Type{ "attribute_one": cty.String, }, validateSingleDiff: renderers.ValidateUnknown(nil, plans.Create, false), }, "update_unknown": { input: structured.Change{ Before: map[string]interface{}{ "attribute_one": "old", }, After: nil, Unknown: true, }, attributes: map[string]cty.Type{ "attribute_one": cty.String, }, validateObject: renderers.ValidateUnknown(renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), }, plans.Delete, false), plans.Update, false), validateNestedObject: renderers.ValidateUnknown(renderers.ValidateNestedObject(map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidateUnknown(renderers.ValidatePrimitive("old", nil, plans.Delete, false), plans.Update, false), }, plans.Update, false), plans.Update, false), validateSetDiffs: &SetDiff{ Before: SetDiffEntry{ ObjectDiff: map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), }, Action: plans.Delete, Replace: false, }, After: SetDiffEntry{ SingleDiff: renderers.ValidateUnknown(nil, plans.Create, false), }, }, }, "create_attribute": { input: structured.Change{ Before: map[string]interface{}{}, After: map[string]interface{}{ "attribute_one": "new", }, }, attributes: map[string]cty.Type{ "attribute_one": cty.String, }, validateDiffs: map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive(nil, "new", plans.Create, false), }, validateAction: plans.Update, validateReplace: false, validateSetDiffs: &SetDiff{ Before: SetDiffEntry{ ObjectDiff: nil, Action: plans.Delete, Replace: false, }, After: SetDiffEntry{ ObjectDiff: map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive(nil, "new", plans.Create, false), }, Action: plans.Create, Replace: false, }, }, }, "create_attribute_from_explicit_null": { input: structured.Change{ Before: map[string]interface{}{ "attribute_one": nil, }, After: map[string]interface{}{ "attribute_one": "new", }, }, attributes: map[string]cty.Type{ "attribute_one": cty.String, }, validateDiffs: map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive(nil, "new", plans.Create, false), }, validateAction: plans.Update, validateReplace: false, validateSetDiffs: &SetDiff{ Before: SetDiffEntry{ ObjectDiff: nil, Action: plans.Delete, Replace: false, }, After: SetDiffEntry{ ObjectDiff: map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive(nil, "new", plans.Create, false), }, Action: plans.Create, Replace: false, }, }, }, "delete_attribute": { input: structured.Change{ Before: map[string]interface{}{ "attribute_one": "old", }, After: map[string]interface{}{}, }, attributes: map[string]cty.Type{ "attribute_one": cty.String, }, validateDiffs: map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), }, validateAction: plans.Update, validateReplace: false, validateSetDiffs: &SetDiff{ Before: SetDiffEntry{ ObjectDiff: map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), }, Action: plans.Delete, Replace: false, }, After: SetDiffEntry{ ObjectDiff: nil, Action: plans.Create, Replace: false, }, }, }, "delete_attribute_to_explicit_null": { input: structured.Change{ Before: map[string]interface{}{ "attribute_one": "old", }, After: map[string]interface{}{ "attribute_one": nil, }, }, attributes: map[string]cty.Type{ "attribute_one": cty.String, }, validateDiffs: map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), }, validateAction: plans.Update, validateReplace: false, validateSetDiffs: &SetDiff{ Before: SetDiffEntry{ ObjectDiff: map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), }, Action: plans.Delete, Replace: false, }, After: SetDiffEntry{ ObjectDiff: nil, Action: plans.Create, Replace: false, }, }, }, "update_attribute": { input: structured.Change{ Before: map[string]interface{}{ "attribute_one": "old", }, After: map[string]interface{}{ "attribute_one": "new", }, }, attributes: map[string]cty.Type{ "attribute_one": cty.String, }, validateDiffs: map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive("old", "new", plans.Update, false), }, validateAction: plans.Update, validateReplace: false, validateSetDiffs: &SetDiff{ Before: SetDiffEntry{ ObjectDiff: map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), }, Action: plans.Delete, Replace: false, }, After: SetDiffEntry{ ObjectDiff: map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive(nil, "new", plans.Create, false), }, Action: plans.Create, Replace: false, }, }, }, "create_sensitive_attribute": { input: structured.Change{ Before: map[string]interface{}{}, After: map[string]interface{}{ "attribute_one": "new", }, AfterSensitive: map[string]interface{}{ "attribute_one": true, }, }, attributes: map[string]cty.Type{ "attribute_one": cty.String, }, validateDiffs: map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidateSensitive(renderers.ValidatePrimitive(nil, "new", plans.Create, false), false, true, plans.Create, false), }, validateAction: plans.Update, validateReplace: false, validateSetDiffs: &SetDiff{ Before: SetDiffEntry{ ObjectDiff: nil, Action: plans.Delete, Replace: false, }, After: SetDiffEntry{ ObjectDiff: map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidateSensitive(renderers.ValidatePrimitive(nil, "new", plans.Create, false), false, true, plans.Create, false), }, Action: plans.Create, Replace: false, }, }, }, "delete_sensitive_attribute": { input: structured.Change{ Before: map[string]interface{}{ "attribute_one": "old", }, BeforeSensitive: map[string]interface{}{ "attribute_one": true, }, After: map[string]interface{}{}, }, attributes: map[string]cty.Type{ "attribute_one": cty.String, }, validateDiffs: map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidateSensitive(renderers.ValidatePrimitive("old", nil, plans.Delete, false), true, false, plans.Delete, false), }, validateAction: plans.Update, validateReplace: false, validateSetDiffs: &SetDiff{ Before: SetDiffEntry{ ObjectDiff: map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidateSensitive(renderers.ValidatePrimitive("old", nil, plans.Delete, false), true, false, plans.Delete, false), }, Action: plans.Delete, Replace: false, }, After: SetDiffEntry{ ObjectDiff: nil, Action: plans.Create, Replace: false, }, }, }, "update_sensitive_attribute": { input: structured.Change{ Before: map[string]interface{}{ "attribute_one": "old", }, BeforeSensitive: map[string]interface{}{ "attribute_one": true, }, After: map[string]interface{}{ "attribute_one": "new", }, AfterSensitive: map[string]interface{}{ "attribute_one": true, }, }, attributes: map[string]cty.Type{ "attribute_one": cty.String, }, validateDiffs: map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidateSensitive(renderers.ValidatePrimitive("old", "new", plans.Update, false), true, true, plans.Update, false), }, validateAction: plans.Update, validateReplace: false, validateSetDiffs: &SetDiff{ Before: SetDiffEntry{ ObjectDiff: map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidateSensitive(renderers.ValidatePrimitive("old", nil, plans.Delete, false), true, false, plans.Delete, false), }, Action: plans.Delete, Replace: false, }, After: SetDiffEntry{ ObjectDiff: map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidateSensitive(renderers.ValidatePrimitive(nil, "new", plans.Create, false), false, true, plans.Create, false), }, Action: plans.Create, Replace: false, }, }, }, "create_computed_attribute": { input: structured.Change{ Before: map[string]interface{}{}, After: map[string]interface{}{}, Unknown: map[string]interface{}{ "attribute_one": true, }, }, attributes: map[string]cty.Type{ "attribute_one": cty.String, }, validateDiffs: map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidateUnknown(nil, plans.Create, false), }, validateAction: plans.Update, validateReplace: false, }, "update_computed_attribute": { input: structured.Change{ Before: map[string]interface{}{ "attribute_one": "old", }, After: map[string]interface{}{}, Unknown: map[string]interface{}{ "attribute_one": true, }, }, attributes: map[string]cty.Type{ "attribute_one": cty.String, }, validateDiffs: map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidateUnknown( renderers.ValidatePrimitive("old", nil, plans.Delete, false), plans.Update, false), }, validateAction: plans.Update, validateReplace: false, validateSetDiffs: &SetDiff{ Before: SetDiffEntry{ ObjectDiff: map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), }, Action: plans.Delete, Replace: false, }, After: SetDiffEntry{ ObjectDiff: map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidateUnknown(nil, plans.Create, false), }, Action: plans.Create, Replace: false, }, }, }, "ignores_unset_fields": { input: structured.Change{ Before: map[string]interface{}{}, After: map[string]interface{}{}, }, attributes: map[string]cty.Type{ "attribute_one": cty.String, }, validateDiffs: map[string]renderers.ValidateDiffFunction{}, validateAction: plans.NoOp, validateReplace: false, }, "update_replace_self": { input: structured.Change{ Before: map[string]interface{}{ "attribute_one": "old", }, After: map[string]interface{}{ "attribute_one": "new", }, ReplacePaths: &attribute_path.PathMatcher{ Paths: [][]interface{}{ {}, }, }, }, attributes: map[string]cty.Type{ "attribute_one": cty.String, }, validateDiffs: map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive("old", "new", plans.Update, false), }, validateAction: plans.Update, validateReplace: true, validateSetDiffs: &SetDiff{ Before: SetDiffEntry{ ObjectDiff: map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), }, Action: plans.Delete, Replace: true, }, After: SetDiffEntry{ ObjectDiff: map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive(nil, "new", plans.Create, false), }, Action: plans.Create, Replace: true, }, }, }, "update_replace_attribute": { input: structured.Change{ Before: map[string]interface{}{ "attribute_one": "old", }, After: map[string]interface{}{ "attribute_one": "new", }, ReplacePaths: &attribute_path.PathMatcher{ Paths: [][]interface{}{ {"attribute_one"}, }, }, }, attributes: map[string]cty.Type{ "attribute_one": cty.String, }, validateDiffs: map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive("old", "new", plans.Update, true), }, validateAction: plans.Update, validateReplace: false, validateSetDiffs: &SetDiff{ Before: SetDiffEntry{ ObjectDiff: map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, true), }, Action: plans.Delete, Replace: false, }, After: SetDiffEntry{ ObjectDiff: map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive(nil, "new", plans.Create, true), }, Action: plans.Create, Replace: false, }, }, }, "update_includes_relevant_attributes": { input: structured.Change{ Before: map[string]interface{}{ "attribute_one": "old_one", "attribute_two": "old_two", }, After: map[string]interface{}{ "attribute_one": "new_one", "attribute_two": "new_two", }, RelevantAttributes: &attribute_path.PathMatcher{ Paths: [][]interface{}{ {"attribute_one"}, }, }, }, attributes: map[string]cty.Type{ "attribute_one": cty.String, "attribute_two": cty.String, }, validateDiffs: map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive("old_one", "new_one", plans.Update, false), "attribute_two": renderers.ValidatePrimitive("old_two", "old_two", plans.NoOp, false), }, validateList: renderers.ValidateList([]renderers.ValidateDiffFunction{ renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ // Lists are a bit special, and in this case is actually // going to ignore the relevant attributes. This is // deliberate. See the comments in list.go for an // explanation. "attribute_one": renderers.ValidatePrimitive("old_one", "new_one", plans.Update, false), "attribute_two": renderers.ValidatePrimitive("old_two", "new_two", plans.Update, false), }, plans.Update, false), }, plans.Update, false), validateAction: plans.Update, validateReplace: false, validateSetDiffs: &SetDiff{ Before: SetDiffEntry{ ObjectDiff: map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive("old_one", nil, plans.Delete, false), "attribute_two": renderers.ValidatePrimitive("old_two", nil, plans.Delete, false), }, Action: plans.Delete, Replace: false, }, After: SetDiffEntry{ ObjectDiff: map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive(nil, "new_one", plans.Create, false), "attribute_two": renderers.ValidatePrimitive(nil, "new_two", plans.Create, false), }, Action: plans.Create, Replace: false, }, }, }, } for name, tmp := range tcs { tc := tmp // Let's set some default values on the input. if tc.input.RelevantAttributes == nil { tc.input.RelevantAttributes = attribute_path.AlwaysMatcher() } if tc.input.ReplacePaths == nil { tc.input.ReplacePaths = &attribute_path.PathMatcher{} } collectionDefaultAction := plans.Update if name == "ignores_unset_fields" { // Special case for this test, as it is the only one that doesn't // have the collection types return an update. collectionDefaultAction = plans.NoOp } t.Run(name, func(t *testing.T) { t.Run("object", func(t *testing.T) { attribute := &jsonprovider.Attribute{ AttributeType: unmarshalType(t, cty.Object(tc.attributes)), } if tc.validateObject != nil { tc.validateObject(t, ComputeDiffForAttribute(tc.input, attribute, tc.input.CalculateAction())) return } if tc.validateSingleDiff != nil { tc.validateSingleDiff(t, ComputeDiffForAttribute(tc.input, attribute, tc.input.CalculateAction())) return } validate := renderers.ValidateObject(tc.validateDiffs, tc.validateAction, tc.validateReplace) validate(t, ComputeDiffForAttribute(tc.input, attribute, tc.input.CalculateAction())) }) t.Run("map", func(t *testing.T) { attribute := &jsonprovider.Attribute{ AttributeType: unmarshalType(t, cty.Map(cty.Object(tc.attributes))), } input := wrapChangeInMap(tc.input) if tc.validateObject != nil { validate := renderers.ValidateMap(map[string]renderers.ValidateDiffFunction{ "element": tc.validateObject, }, collectionDefaultAction, false) validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction())) return } if tc.validateSingleDiff != nil { validate := renderers.ValidateMap(map[string]renderers.ValidateDiffFunction{ "element": tc.validateSingleDiff, }, collectionDefaultAction, false) validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction())) return } validate := renderers.ValidateMap(map[string]renderers.ValidateDiffFunction{ "element": renderers.ValidateObject(tc.validateDiffs, tc.validateAction, tc.validateReplace), }, collectionDefaultAction, false) validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction())) }) t.Run("list", func(t *testing.T) { attribute := &jsonprovider.Attribute{ AttributeType: unmarshalType(t, cty.List(cty.Object(tc.attributes))), } input := wrapChangeInSlice(tc.input) if tc.validateList != nil { tc.validateList(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction())) return } if tc.validateObject != nil { validate := renderers.ValidateList([]renderers.ValidateDiffFunction{ tc.validateObject, }, collectionDefaultAction, false) validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction())) return } if tc.validateSingleDiff != nil { validate := renderers.ValidateList([]renderers.ValidateDiffFunction{ tc.validateSingleDiff, }, collectionDefaultAction, false) validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction())) return } validate := renderers.ValidateList([]renderers.ValidateDiffFunction{ renderers.ValidateObject(tc.validateDiffs, tc.validateAction, tc.validateReplace), }, collectionDefaultAction, false) validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction())) }) t.Run("set", func(t *testing.T) { attribute := &jsonprovider.Attribute{ AttributeType: unmarshalType(t, cty.Set(cty.Object(tc.attributes))), } input := wrapChangeInSlice(tc.input) if tc.validateSetDiffs != nil { validate := renderers.ValidateSet(func() []renderers.ValidateDiffFunction { var ret []renderers.ValidateDiffFunction ret = append(ret, tc.validateSetDiffs.Before.Validate(renderers.ValidateObject)) ret = append(ret, tc.validateSetDiffs.After.Validate(renderers.ValidateObject)) return ret }(), collectionDefaultAction, false) validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction())) return } if tc.validateObject != nil { validate := renderers.ValidateSet([]renderers.ValidateDiffFunction{ tc.validateObject, }, collectionDefaultAction, false) validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction())) return } if tc.validateSingleDiff != nil { validate := renderers.ValidateSet([]renderers.ValidateDiffFunction{ tc.validateSingleDiff, }, collectionDefaultAction, false) validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction())) return } validate := renderers.ValidateSet([]renderers.ValidateDiffFunction{ renderers.ValidateObject(tc.validateDiffs, tc.validateAction, tc.validateReplace), }, collectionDefaultAction, false) validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction())) }) }) t.Run(fmt.Sprintf("nested_%s", name), func(t *testing.T) { t.Run("object", func(t *testing.T) { attribute := &jsonprovider.Attribute{ AttributeNestedType: &jsonprovider.NestedType{ Attributes: func() map[string]*jsonprovider.Attribute { attributes := make(map[string]*jsonprovider.Attribute) for key, attribute := range tc.attributes { attributes[key] = &jsonprovider.Attribute{ AttributeType: unmarshalType(t, attribute), } } return attributes }(), NestingMode: "single", }, } if tc.validateNestedObject != nil { tc.validateNestedObject(t, ComputeDiffForAttribute(tc.input, attribute, tc.input.CalculateAction())) return } if tc.validateSingleDiff != nil { tc.validateSingleDiff(t, ComputeDiffForAttribute(tc.input, attribute, tc.input.CalculateAction())) return } validate := renderers.ValidateNestedObject(tc.validateDiffs, tc.validateAction, tc.validateReplace) validate(t, ComputeDiffForAttribute(tc.input, attribute, tc.input.CalculateAction())) }) t.Run("map", func(t *testing.T) { attribute := &jsonprovider.Attribute{ AttributeNestedType: &jsonprovider.NestedType{ Attributes: func() map[string]*jsonprovider.Attribute { attributes := make(map[string]*jsonprovider.Attribute) for key, attribute := range tc.attributes { attributes[key] = &jsonprovider.Attribute{ AttributeType: unmarshalType(t, attribute), } } return attributes }(), NestingMode: "map", }, } input := wrapChangeInMap(tc.input) if tc.validateNestedObject != nil { validate := renderers.ValidateMap(map[string]renderers.ValidateDiffFunction{ "element": tc.validateNestedObject, }, collectionDefaultAction, false) validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction())) return } if tc.validateSingleDiff != nil { validate := renderers.ValidateMap(map[string]renderers.ValidateDiffFunction{ "element": tc.validateSingleDiff, }, collectionDefaultAction, false) validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction())) return } validate := renderers.ValidateMap(map[string]renderers.ValidateDiffFunction{ "element": renderers.ValidateNestedObject(tc.validateDiffs, tc.validateAction, tc.validateReplace), }, collectionDefaultAction, false) validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction())) }) t.Run("list", func(t *testing.T) { attribute := &jsonprovider.Attribute{ AttributeNestedType: &jsonprovider.NestedType{ Attributes: func() map[string]*jsonprovider.Attribute { attributes := make(map[string]*jsonprovider.Attribute) for key, attribute := range tc.attributes { attributes[key] = &jsonprovider.Attribute{ AttributeType: unmarshalType(t, attribute), } } return attributes }(), NestingMode: "list", }, } input := wrapChangeInSlice(tc.input) if tc.validateNestedObject != nil { validate := renderers.ValidateNestedList([]renderers.ValidateDiffFunction{ tc.validateNestedObject, }, collectionDefaultAction, false) validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction())) return } if tc.validateSingleDiff != nil { validate := renderers.ValidateNestedList([]renderers.ValidateDiffFunction{ tc.validateSingleDiff, }, collectionDefaultAction, false) validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction())) return } validate := renderers.ValidateNestedList([]renderers.ValidateDiffFunction{ renderers.ValidateNestedObject(tc.validateDiffs, tc.validateAction, tc.validateReplace), }, collectionDefaultAction, false) validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction())) }) t.Run("set", func(t *testing.T) { attribute := &jsonprovider.Attribute{ AttributeNestedType: &jsonprovider.NestedType{ Attributes: func() map[string]*jsonprovider.Attribute { attributes := make(map[string]*jsonprovider.Attribute) for key, attribute := range tc.attributes { attributes[key] = &jsonprovider.Attribute{ AttributeType: unmarshalType(t, attribute), } } return attributes }(), NestingMode: "set", }, } input := wrapChangeInSlice(tc.input) if tc.validateSetDiffs != nil { validate := renderers.ValidateSet(func() []renderers.ValidateDiffFunction { var ret []renderers.ValidateDiffFunction ret = append(ret, tc.validateSetDiffs.Before.Validate(renderers.ValidateNestedObject)) ret = append(ret, tc.validateSetDiffs.After.Validate(renderers.ValidateNestedObject)) return ret }(), collectionDefaultAction, false) validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction())) return } if tc.validateNestedObject != nil { validate := renderers.ValidateSet([]renderers.ValidateDiffFunction{ tc.validateNestedObject, }, collectionDefaultAction, false) validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction())) return } if tc.validateSingleDiff != nil { validate := renderers.ValidateSet([]renderers.ValidateDiffFunction{ tc.validateSingleDiff, }, collectionDefaultAction, false) validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction())) return } validate := renderers.ValidateSet([]renderers.ValidateDiffFunction{ renderers.ValidateNestedObject(tc.validateDiffs, tc.validateAction, tc.validateReplace), }, collectionDefaultAction, false) validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction())) }) }) } } func TestValue_BlockAttributesAndNestedBlocks(t *testing.T) { // This function tests manipulating simple attributes and blocks within // blocks. It automatically tests these operations within the contexts of // different block types. tcs := map[string]struct { before interface{} after interface{} block *jsonprovider.Block validate renderers.ValidateDiffFunction validateSet []renderers.ValidateDiffFunction }{ "create_attribute": { before: map[string]interface{}{}, after: map[string]interface{}{ "attribute_one": "new", }, block: &jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "attribute_one": { AttributeType: unmarshalType(t, cty.String), }, }, }, validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive(nil, "new", plans.Create, false), }, nil, nil, nil, nil, plans.Update, false), validateSet: []renderers.ValidateDiffFunction{ renderers.ValidateBlock(nil, nil, nil, nil, nil, plans.Delete, false), renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive(nil, "new", plans.Create, false), }, nil, nil, nil, nil, plans.Create, false), }, }, "update_attribute": { before: map[string]interface{}{ "attribute_one": "old", }, after: map[string]interface{}{ "attribute_one": "new", }, block: &jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "attribute_one": { AttributeType: unmarshalType(t, cty.String), }, }, }, validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive("old", "new", plans.Update, false), }, nil, nil, nil, nil, plans.Update, false), validateSet: []renderers.ValidateDiffFunction{ renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), }, nil, nil, nil, nil, plans.Delete, false), renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive(nil, "new", plans.Create, false), }, nil, nil, nil, nil, plans.Create, false), }, }, "delete_attribute": { before: map[string]interface{}{ "attribute_one": "old", }, after: map[string]interface{}{}, block: &jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "attribute_one": { AttributeType: unmarshalType(t, cty.String), }, }, }, validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), }, nil, nil, nil, nil, plans.Update, false), validateSet: []renderers.ValidateDiffFunction{ renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), }, nil, nil, nil, nil, plans.Delete, false), renderers.ValidateBlock(nil, nil, nil, nil, nil, plans.Create, false), }, }, "create_block": { before: map[string]interface{}{}, after: map[string]interface{}{ "block_one": map[string]interface{}{ "attribute_one": "new", }, }, block: &jsonprovider.Block{ BlockTypes: map[string]*jsonprovider.BlockType{ "block_one": { Block: &jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "attribute_one": { AttributeType: unmarshalType(t, cty.String), }, }, }, NestingMode: "single", }, }, }, validate: renderers.ValidateBlock(nil, map[string]renderers.ValidateDiffFunction{ "block_one": renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive(nil, "new", plans.Create, false), }, nil, nil, nil, nil, plans.Create, false), }, nil, nil, nil, plans.Update, false), validateSet: []renderers.ValidateDiffFunction{ renderers.ValidateBlock(nil, nil, nil, nil, nil, plans.Delete, false), renderers.ValidateBlock(nil, map[string]renderers.ValidateDiffFunction{ "block_one": renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive(nil, "new", plans.Create, false), }, nil, nil, nil, nil, plans.Create, false), }, nil, nil, nil, plans.Create, false), }, }, "update_block": { before: map[string]interface{}{ "block_one": map[string]interface{}{ "attribute_one": "old", }, }, after: map[string]interface{}{ "block_one": map[string]interface{}{ "attribute_one": "new", }, }, block: &jsonprovider.Block{ BlockTypes: map[string]*jsonprovider.BlockType{ "block_one": { Block: &jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "attribute_one": { AttributeType: unmarshalType(t, cty.String), }, }, }, NestingMode: "single", }, }, }, validate: renderers.ValidateBlock(nil, map[string]renderers.ValidateDiffFunction{ "block_one": renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive("old", "new", plans.Update, false), }, nil, nil, nil, nil, plans.Update, false), }, nil, nil, nil, plans.Update, false), validateSet: []renderers.ValidateDiffFunction{ renderers.ValidateBlock(nil, map[string]renderers.ValidateDiffFunction{ "block_one": renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), }, nil, nil, nil, nil, plans.Delete, false), }, nil, nil, nil, plans.Delete, false), renderers.ValidateBlock(nil, map[string]renderers.ValidateDiffFunction{ "block_one": renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive(nil, "new", plans.Create, false), }, nil, nil, nil, nil, plans.Create, false), }, nil, nil, nil, plans.Create, false), }, }, "delete_block": { before: map[string]interface{}{ "block_one": map[string]interface{}{ "attribute_one": "old", }, }, after: map[string]interface{}{}, block: &jsonprovider.Block{ BlockTypes: map[string]*jsonprovider.BlockType{ "block_one": { Block: &jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "attribute_one": { AttributeType: unmarshalType(t, cty.String), }, }, }, NestingMode: "single", }, }, }, validate: renderers.ValidateBlock(nil, map[string]renderers.ValidateDiffFunction{ "block_one": renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), }, nil, nil, nil, nil, plans.Delete, false), }, nil, nil, nil, plans.Update, false), validateSet: []renderers.ValidateDiffFunction{ renderers.ValidateBlock(nil, map[string]renderers.ValidateDiffFunction{ "block_one": renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), }, nil, nil, nil, nil, plans.Delete, false), }, nil, nil, nil, plans.Delete, false), renderers.ValidateBlock(nil, nil, nil, nil, nil, plans.Create, false), }, }, } for name, tmp := range tcs { tc := tmp t.Run(name, func(t *testing.T) { t.Run("single", func(t *testing.T) { input := structured.Change{ Before: map[string]interface{}{ "block_type": tc.before, }, After: map[string]interface{}{ "block_type": tc.after, }, ReplacePaths: &attribute_path.PathMatcher{}, RelevantAttributes: attribute_path.AlwaysMatcher(), } block := &jsonprovider.Block{ BlockTypes: map[string]*jsonprovider.BlockType{ "block_type": { Block: tc.block, NestingMode: "single", }, }, } validate := renderers.ValidateBlock(nil, map[string]renderers.ValidateDiffFunction{ "block_type": tc.validate, }, nil, nil, nil, plans.Update, false) validate(t, ComputeDiffForBlock(input, block)) }) t.Run("map", func(t *testing.T) { input := structured.Change{ Before: map[string]interface{}{ "block_type": map[string]interface{}{ "one": tc.before, }, }, After: map[string]interface{}{ "block_type": map[string]interface{}{ "one": tc.after, }, }, ReplacePaths: &attribute_path.PathMatcher{}, RelevantAttributes: attribute_path.AlwaysMatcher(), } block := &jsonprovider.Block{ BlockTypes: map[string]*jsonprovider.BlockType{ "block_type": { Block: tc.block, NestingMode: "map", }, }, } validate := renderers.ValidateBlock(nil, nil, nil, map[string]map[string]renderers.ValidateDiffFunction{ "block_type": { "one": tc.validate, }, }, nil, plans.Update, false) validate(t, ComputeDiffForBlock(input, block)) }) t.Run("list", func(t *testing.T) { input := structured.Change{ Before: map[string]interface{}{ "block_type": []interface{}{ tc.before, }, }, After: map[string]interface{}{ "block_type": []interface{}{ tc.after, }, }, ReplacePaths: &attribute_path.PathMatcher{}, RelevantAttributes: attribute_path.AlwaysMatcher(), } block := &jsonprovider.Block{ BlockTypes: map[string]*jsonprovider.BlockType{ "block_type": { Block: tc.block, NestingMode: "list", }, }, } validate := renderers.ValidateBlock(nil, nil, map[string][]renderers.ValidateDiffFunction{ "block_type": { tc.validate, }, }, nil, nil, plans.Update, false) validate(t, ComputeDiffForBlock(input, block)) }) t.Run("set", func(t *testing.T) { input := structured.Change{ Before: map[string]interface{}{ "block_type": []interface{}{ tc.before, }, }, After: map[string]interface{}{ "block_type": []interface{}{ tc.after, }, }, ReplacePaths: &attribute_path.PathMatcher{}, RelevantAttributes: attribute_path.AlwaysMatcher(), } block := &jsonprovider.Block{ BlockTypes: map[string]*jsonprovider.BlockType{ "block_type": { Block: tc.block, NestingMode: "set", }, }, } validate := renderers.ValidateBlock(nil, nil, nil, nil, map[string][]renderers.ValidateDiffFunction{ "block_type": func() []renderers.ValidateDiffFunction { if tc.validateSet != nil { return tc.validateSet } return []renderers.ValidateDiffFunction{tc.validate} }(), }, plans.Update, false) validate(t, ComputeDiffForBlock(input, block)) }) }) } } func TestValue_Outputs(t *testing.T) { tcs := map[string]struct { input structured.Change validateDiff renderers.ValidateDiffFunction }{ "primitive_create": { input: structured.Change{ Before: nil, After: "new", }, validateDiff: renderers.ValidatePrimitive(nil, "new", plans.Create, false), }, "object_create": { input: structured.Change{ Before: nil, After: map[string]interface{}{ "element_one": "new_one", "element_two": "new_two", }, }, validateDiff: renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ "element_one": renderers.ValidatePrimitive(nil, "new_one", plans.Create, false), "element_two": renderers.ValidatePrimitive(nil, "new_two", plans.Create, false), }, plans.Create, false), }, "list_create": { input: structured.Change{ Before: nil, After: []interface{}{ "new_one", "new_two", }, }, validateDiff: renderers.ValidateList([]renderers.ValidateDiffFunction{ renderers.ValidatePrimitive(nil, "new_one", plans.Create, false), renderers.ValidatePrimitive(nil, "new_two", plans.Create, false), }, plans.Create, false), }, "primitive_update": { input: structured.Change{ Before: "old", After: "new", }, validateDiff: renderers.ValidatePrimitive("old", "new", plans.Update, false), }, "object_update": { input: structured.Change{ Before: map[string]interface{}{ "element_one": "old_one", "element_two": "old_two", }, After: map[string]interface{}{ "element_one": "new_one", "element_two": "new_two", }, }, validateDiff: renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ "element_one": renderers.ValidatePrimitive("old_one", "new_one", plans.Update, false), "element_two": renderers.ValidatePrimitive("old_two", "new_two", plans.Update, false), }, plans.Update, false), }, "list_update": { input: structured.Change{ Before: []interface{}{ "old_one", "old_two", }, After: []interface{}{ "new_one", "new_two", }, }, validateDiff: renderers.ValidateList([]renderers.ValidateDiffFunction{ renderers.ValidatePrimitive("old_one", nil, plans.Delete, false), renderers.ValidatePrimitive("old_two", nil, plans.Delete, false), renderers.ValidatePrimitive(nil, "new_one", plans.Create, false), renderers.ValidatePrimitive(nil, "new_two", plans.Create, false), }, plans.Update, false), }, "primitive_delete": { input: structured.Change{ Before: "old", After: nil, }, validateDiff: renderers.ValidatePrimitive("old", nil, plans.Delete, false), }, "object_delete": { input: structured.Change{ Before: map[string]interface{}{ "element_one": "old_one", "element_two": "old_two", }, After: nil, }, validateDiff: renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ "element_one": renderers.ValidatePrimitive("old_one", nil, plans.Delete, false), "element_two": renderers.ValidatePrimitive("old_two", nil, plans.Delete, false), }, plans.Delete, false), }, "list_delete": { input: structured.Change{ Before: []interface{}{ "old_one", "old_two", }, After: nil, }, validateDiff: renderers.ValidateList([]renderers.ValidateDiffFunction{ renderers.ValidatePrimitive("old_one", nil, plans.Delete, false), renderers.ValidatePrimitive("old_two", nil, plans.Delete, false), }, plans.Delete, false), }, "primitive_to_list": { input: structured.Change{ Before: "old", After: []interface{}{ "new_one", "new_two", }, }, validateDiff: renderers.ValidateTypeChange( renderers.ValidatePrimitive("old", nil, plans.Delete, false), renderers.ValidateList([]renderers.ValidateDiffFunction{ renderers.ValidatePrimitive(nil, "new_one", plans.Create, false), renderers.ValidatePrimitive(nil, "new_two", plans.Create, false), }, plans.Create, false), plans.Update, false), }, "primitive_to_object": { input: structured.Change{ Before: "old", After: map[string]interface{}{ "element_one": "new_one", "element_two": "new_two", }, }, validateDiff: renderers.ValidateTypeChange( renderers.ValidatePrimitive("old", nil, plans.Delete, false), renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ "element_one": renderers.ValidatePrimitive(nil, "new_one", plans.Create, false), "element_two": renderers.ValidatePrimitive(nil, "new_two", plans.Create, false), }, plans.Create, false), plans.Update, false), }, "list_to_primitive": { input: structured.Change{ Before: []interface{}{ "old_one", "old_two", }, After: "new", }, validateDiff: renderers.ValidateTypeChange( renderers.ValidateList([]renderers.ValidateDiffFunction{ renderers.ValidatePrimitive("old_one", nil, plans.Delete, false), renderers.ValidatePrimitive("old_two", nil, plans.Delete, false), }, plans.Delete, false), renderers.ValidatePrimitive(nil, "new", plans.Create, false), plans.Update, false), }, "list_to_object": { input: structured.Change{ Before: []interface{}{ "old_one", "old_two", }, After: map[string]interface{}{ "element_one": "new_one", "element_two": "new_two", }, }, validateDiff: renderers.ValidateTypeChange( renderers.ValidateList([]renderers.ValidateDiffFunction{ renderers.ValidatePrimitive("old_one", nil, plans.Delete, false), renderers.ValidatePrimitive("old_two", nil, plans.Delete, false), }, plans.Delete, false), renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ "element_one": renderers.ValidatePrimitive(nil, "new_one", plans.Create, false), "element_two": renderers.ValidatePrimitive(nil, "new_two", plans.Create, false), }, plans.Create, false), plans.Update, false), }, "object_to_primitive": { input: structured.Change{ Before: map[string]interface{}{ "element_one": "old_one", "element_two": "old_two", }, After: "new", }, validateDiff: renderers.ValidateTypeChange( renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ "element_one": renderers.ValidatePrimitive("old_one", nil, plans.Delete, false), "element_two": renderers.ValidatePrimitive("old_two", nil, plans.Delete, false), }, plans.Delete, false), renderers.ValidatePrimitive(nil, "new", plans.Create, false), plans.Update, false), }, "object_to_list": { input: structured.Change{ Before: map[string]interface{}{ "element_one": "old_one", "element_two": "old_two", }, After: []interface{}{ "new_one", "new_two", }, }, validateDiff: renderers.ValidateTypeChange( renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ "element_one": renderers.ValidatePrimitive("old_one", nil, plans.Delete, false), "element_two": renderers.ValidatePrimitive("old_two", nil, plans.Delete, false), }, plans.Delete, false), renderers.ValidateList([]renderers.ValidateDiffFunction{ renderers.ValidatePrimitive(nil, "new_one", plans.Create, false), renderers.ValidatePrimitive(nil, "new_two", plans.Create, false), }, plans.Create, false), plans.Update, false), }, } for name, tc := range tcs { // Let's set some default values on the input. if tc.input.RelevantAttributes == nil { tc.input.RelevantAttributes = attribute_path.AlwaysMatcher() } if tc.input.ReplacePaths == nil { tc.input.ReplacePaths = &attribute_path.PathMatcher{} } t.Run(name, func(t *testing.T) { tc.validateDiff(t, ComputeDiffForOutput(tc.input)) }) } } func TestValue_MultilineStringsInList(t *testing.T) { // This function tests that multiline strings in lists are handled correctly // by the diff renderer. tcs := map[string]struct { input structured.Change validateDiff renderers.ValidateDiffFunction }{ "multiline_string_list_update": { input: structured.Change{ Before: []interface{}{ "line1\nline2", "line1", }, After: []interface{}{ "line1\nline2+", "line1\nline2", }, }, validateDiff: renderers.ValidateList([]renderers.ValidateDiffFunction{ renderers.ValidatePrimitive("line1\nline2", "line1\nline2+", plans.Update, false), renderers.ValidatePrimitive("line1", "line1\nline2", plans.Update, false), }, plans.Update, false), }, "multiline_string_list_update_2": { input: structured.Change{ Before: []interface{}{ "unchanged line", "deleted line", "line1\nline2", "line1", }, After: []interface{}{ "unchanged line", "line1\nline2+", "line1\nline2", }, }, validateDiff: renderers.ValidateList([]renderers.ValidateDiffFunction{ renderers.ValidatePrimitive("unchanged line", "unchanged line", plans.NoOp, false), renderers.ValidatePrimitive("deleted line", nil, plans.Delete, false), renderers.ValidatePrimitive("line1\nline2", "line1\nline2+", plans.Update, false), renderers.ValidatePrimitive("line1", "line1\nline2", plans.Update, false), }, plans.Update, false), }, } for name, tc := range tcs { // Let's set some default values on the input. if tc.input.RelevantAttributes == nil { tc.input.RelevantAttributes = attribute_path.AlwaysMatcher() } if tc.input.ReplacePaths == nil { tc.input.ReplacePaths = &attribute_path.PathMatcher{} } t.Run(name, func(t *testing.T) { tc.validateDiff(t, ComputeDiffForOutput(tc.input)) }) } } func TestValue_PrimitiveAttributes(t *testing.T) { // This function tests manipulating primitives: creating, deleting and // updating. It also automatically tests these operations within the // contexts of collections. tcs := map[string]struct { input structured.Change attribute cty.Type validateDiff renderers.ValidateDiffFunction validateSliceDiffs []renderers.ValidateDiffFunction // Lists are special in some cases. }{ "primitive_create": { input: structured.Change{ After: "new", }, attribute: cty.String, validateDiff: renderers.ValidatePrimitive(nil, "new", plans.Create, false), }, "primitive_delete": { input: structured.Change{ Before: "old", }, attribute: cty.String, validateDiff: renderers.ValidatePrimitive("old", nil, plans.Delete, false), }, "primitive_update": { input: structured.Change{ Before: "old", After: "new", }, attribute: cty.String, validateDiff: renderers.ValidatePrimitive("old", "new", plans.Update, false), validateSliceDiffs: []renderers.ValidateDiffFunction{ renderers.ValidatePrimitive("old", nil, plans.Delete, false), renderers.ValidatePrimitive(nil, "new", plans.Create, false), }, }, "primitive_set_explicit_null": { input: structured.Change{ Before: "old", After: nil, AfterExplicit: true, }, attribute: cty.String, validateDiff: renderers.ValidatePrimitive("old", nil, plans.Update, false), validateSliceDiffs: []renderers.ValidateDiffFunction{ renderers.ValidatePrimitive("old", nil, plans.Delete, false), renderers.ValidatePrimitive(nil, nil, plans.Create, false), }, }, "primitive_unset_explicit_null": { input: structured.Change{ BeforeExplicit: true, Before: nil, After: "new", }, attribute: cty.String, validateDiff: renderers.ValidatePrimitive(nil, "new", plans.Update, false), validateSliceDiffs: []renderers.ValidateDiffFunction{ renderers.ValidatePrimitive(nil, nil, plans.Delete, false), renderers.ValidatePrimitive(nil, "new", plans.Create, false), }, }, "primitive_create_sensitive": { input: structured.Change{ Before: nil, After: "new", AfterSensitive: true, }, attribute: cty.String, validateDiff: renderers.ValidateSensitive(renderers.ValidatePrimitive(nil, "new", plans.Create, false), false, true, plans.Create, false), }, "primitive_delete_sensitive": { input: structured.Change{ Before: "old", BeforeSensitive: true, After: nil, }, attribute: cty.String, validateDiff: renderers.ValidateSensitive(renderers.ValidatePrimitive("old", nil, plans.Delete, false), true, false, plans.Delete, false), }, "primitive_update_sensitive": { input: structured.Change{ Before: "old", BeforeSensitive: true, After: "new", AfterSensitive: true, }, attribute: cty.String, validateDiff: renderers.ValidateSensitive(renderers.ValidatePrimitive("old", "new", plans.Update, false), true, true, plans.Update, false), validateSliceDiffs: []renderers.ValidateDiffFunction{ renderers.ValidateSensitive(renderers.ValidatePrimitive("old", nil, plans.Delete, false), true, false, plans.Delete, false), renderers.ValidateSensitive(renderers.ValidatePrimitive(nil, "new", plans.Create, false), false, true, plans.Create, false), }, }, "primitive_create_computed": { input: structured.Change{ Before: nil, After: nil, Unknown: true, }, attribute: cty.String, validateDiff: renderers.ValidateUnknown(nil, plans.Create, false), }, "primitive_update_computed": { input: structured.Change{ Before: "old", After: nil, Unknown: true, }, attribute: cty.String, validateDiff: renderers.ValidateUnknown(renderers.ValidatePrimitive("old", nil, plans.Delete, false), plans.Update, false), validateSliceDiffs: []renderers.ValidateDiffFunction{ renderers.ValidatePrimitive("old", nil, plans.Delete, false), renderers.ValidateUnknown(nil, plans.Create, false), }, }, "primitive_update_replace": { input: structured.Change{ Before: "old", After: "new", ReplacePaths: &attribute_path.PathMatcher{ Paths: [][]interface{}{ {}, // An empty path suggests replace should be true. }, }, }, attribute: cty.String, validateDiff: renderers.ValidatePrimitive("old", "new", plans.Update, true), validateSliceDiffs: []renderers.ValidateDiffFunction{ renderers.ValidatePrimitive("old", nil, plans.Delete, true), renderers.ValidatePrimitive(nil, "new", plans.Create, true), }, }, "noop": { input: structured.Change{ Before: "old", After: "old", }, attribute: cty.String, validateDiff: renderers.ValidatePrimitive("old", "old", plans.NoOp, false), }, "dynamic": { input: structured.Change{ Before: "old", After: "new", }, attribute: cty.DynamicPseudoType, validateDiff: renderers.ValidatePrimitive("old", "new", plans.Update, false), validateSliceDiffs: []renderers.ValidateDiffFunction{ renderers.ValidatePrimitive("old", nil, plans.Delete, false), renderers.ValidatePrimitive(nil, "new", plans.Create, false), }, }, "dynamic_type_change": { input: structured.Change{ Before: "old", After: json.Number("4"), }, attribute: cty.DynamicPseudoType, validateDiff: renderers.ValidateTypeChange( renderers.ValidatePrimitive("old", nil, plans.Delete, false), renderers.ValidatePrimitive(nil, json.Number("4"), plans.Create, false), plans.Update, false), validateSliceDiffs: []renderers.ValidateDiffFunction{ renderers.ValidatePrimitive("old", nil, plans.Delete, false), renderers.ValidatePrimitive(nil, json.Number("4"), plans.Create, false), }, }, } for name, tmp := range tcs { tc := tmp // Let's set some default values on the input. if tc.input.RelevantAttributes == nil { tc.input.RelevantAttributes = attribute_path.AlwaysMatcher() } if tc.input.ReplacePaths == nil { tc.input.ReplacePaths = &attribute_path.PathMatcher{} } defaultCollectionsAction := plans.Update if name == "noop" { defaultCollectionsAction = plans.NoOp } t.Run(name, func(t *testing.T) { t.Run("direct", func(t *testing.T) { attr := &jsonprovider.Attribute{AttributeType: unmarshalType(t, tc.attribute)} diff := ComputeDiffForAttribute(tc.input, attr, tc.input.CalculateAction()) tc.validateDiff(t, diff) }) t.Run("map", func(t *testing.T) { input := wrapChangeInMap(tc.input) attribute := &jsonprovider.Attribute{ AttributeType: unmarshalType(t, cty.Map(tc.attribute)), } validate := renderers.ValidateMap(map[string]renderers.ValidateDiffFunction{ "element": tc.validateDiff, }, defaultCollectionsAction, false) validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction())) }) t.Run("list", func(t *testing.T) { input := wrapChangeInSlice(tc.input) attribute := &jsonprovider.Attribute{ AttributeType: unmarshalType(t, cty.List(tc.attribute)), } if tc.validateSliceDiffs != nil { validate := renderers.ValidateList(tc.validateSliceDiffs, defaultCollectionsAction, false) validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction())) return } validate := renderers.ValidateList([]renderers.ValidateDiffFunction{ tc.validateDiff, }, defaultCollectionsAction, false) validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction())) }) t.Run("set", func(t *testing.T) { input := wrapChangeInSlice(tc.input) attribute := &jsonprovider.Attribute{ AttributeType: unmarshalType(t, cty.Set(tc.attribute)), } if tc.validateSliceDiffs != nil { validate := renderers.ValidateSet(tc.validateSliceDiffs, defaultCollectionsAction, false) validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction())) return } validate := renderers.ValidateSet([]renderers.ValidateDiffFunction{ tc.validateDiff, }, defaultCollectionsAction, false) validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction())) }) }) } } func TestValue_CollectionAttributes(t *testing.T) { // This function tests creating and deleting collections. Note, it does not // generally cover editing collections except in special cases as editing // collections is handled automatically by other functions. tcs := map[string]struct { input structured.Change attribute *jsonprovider.Attribute validateDiff renderers.ValidateDiffFunction }{ "map_create_empty": { input: structured.Change{ Before: nil, After: map[string]interface{}{}, }, attribute: &jsonprovider.Attribute{ AttributeType: unmarshalType(t, cty.Map(cty.String)), }, validateDiff: renderers.ValidateMap(nil, plans.Create, false), }, "map_create_populated": { input: structured.Change{ Before: nil, After: map[string]interface{}{ "element_one": "one", "element_two": "two", }, }, attribute: &jsonprovider.Attribute{ AttributeType: unmarshalType(t, cty.Map(cty.String)), }, validateDiff: renderers.ValidateMap(map[string]renderers.ValidateDiffFunction{ "element_one": renderers.ValidatePrimitive(nil, "one", plans.Create, false), "element_two": renderers.ValidatePrimitive(nil, "two", plans.Create, false), }, plans.Create, false), }, "map_delete_empty": { input: structured.Change{ Before: map[string]interface{}{}, After: nil, }, attribute: &jsonprovider.Attribute{ AttributeType: unmarshalType(t, cty.Map(cty.String)), }, validateDiff: renderers.ValidateMap(nil, plans.Delete, false), }, "map_delete_populated": { input: structured.Change{ Before: map[string]interface{}{ "element_one": "one", "element_two": "two", }, After: nil, }, attribute: &jsonprovider.Attribute{ AttributeType: unmarshalType(t, cty.Map(cty.String)), }, validateDiff: renderers.ValidateMap(map[string]renderers.ValidateDiffFunction{ "element_one": renderers.ValidatePrimitive("one", nil, plans.Delete, false), "element_two": renderers.ValidatePrimitive("two", nil, plans.Delete, false), }, plans.Delete, false), }, "map_create_sensitive": { input: structured.Change{ Before: nil, After: map[string]interface{}{}, AfterSensitive: true, }, attribute: &jsonprovider.Attribute{ AttributeType: unmarshalType(t, cty.Map(cty.String)), }, validateDiff: renderers.ValidateSensitive(renderers.ValidateMap(nil, plans.Create, false), false, true, plans.Create, false), }, "map_update_sensitive": { input: structured.Change{ Before: map[string]interface{}{ "element": "one", }, BeforeSensitive: true, After: map[string]interface{}{}, AfterSensitive: true, }, attribute: &jsonprovider.Attribute{ AttributeType: unmarshalType(t, cty.Map(cty.String)), }, validateDiff: renderers.ValidateSensitive(renderers.ValidateMap(map[string]renderers.ValidateDiffFunction{ "element": renderers.ValidatePrimitive("one", nil, plans.Delete, false), }, plans.Update, false), true, true, plans.Update, false), }, "map_delete_sensitive": { input: structured.Change{ Before: map[string]interface{}{}, BeforeSensitive: true, After: nil, }, attribute: &jsonprovider.Attribute{ AttributeType: unmarshalType(t, cty.Map(cty.String)), }, validateDiff: renderers.ValidateSensitive(renderers.ValidateMap(nil, plans.Delete, false), true, false, plans.Delete, false), }, "map_create_unknown": { input: structured.Change{ Before: nil, After: map[string]interface{}{}, Unknown: true, }, attribute: &jsonprovider.Attribute{ AttributeType: unmarshalType(t, cty.Map(cty.String)), }, validateDiff: renderers.ValidateUnknown(nil, plans.Create, false), }, "map_update_unknown": { input: structured.Change{ Before: map[string]interface{}{}, After: map[string]interface{}{ "element": "one", }, Unknown: true, }, attribute: &jsonprovider.Attribute{ AttributeType: unmarshalType(t, cty.Map(cty.String)), }, validateDiff: renderers.ValidateUnknown(renderers.ValidateMap(nil, plans.Delete, false), plans.Update, false), }, "list_create_empty": { input: structured.Change{ Before: nil, After: []interface{}{}, }, attribute: &jsonprovider.Attribute{ AttributeType: unmarshalType(t, cty.List(cty.String)), }, validateDiff: renderers.ValidateList(nil, plans.Create, false), }, "list_create_populated": { input: structured.Change{ Before: nil, After: []interface{}{"one", "two"}, }, attribute: &jsonprovider.Attribute{ AttributeType: unmarshalType(t, cty.List(cty.String)), }, validateDiff: renderers.ValidateList([]renderers.ValidateDiffFunction{ renderers.ValidatePrimitive(nil, "one", plans.Create, false), renderers.ValidatePrimitive(nil, "two", plans.Create, false), }, plans.Create, false), }, "list_delete_empty": { input: structured.Change{ Before: []interface{}{}, After: nil, }, attribute: &jsonprovider.Attribute{ AttributeType: unmarshalType(t, cty.List(cty.String)), }, validateDiff: renderers.ValidateList(nil, plans.Delete, false), }, "list_delete_populated": { input: structured.Change{ Before: []interface{}{"one", "two"}, After: nil, }, attribute: &jsonprovider.Attribute{ AttributeType: unmarshalType(t, cty.List(cty.String)), }, validateDiff: renderers.ValidateList([]renderers.ValidateDiffFunction{ renderers.ValidatePrimitive("one", nil, plans.Delete, false), renderers.ValidatePrimitive("two", nil, plans.Delete, false), }, plans.Delete, false), }, "list_create_sensitive": { input: structured.Change{ Before: nil, After: []interface{}{}, AfterSensitive: true, }, attribute: &jsonprovider.Attribute{ AttributeType: unmarshalType(t, cty.List(cty.String)), }, validateDiff: renderers.ValidateSensitive(renderers.ValidateList(nil, plans.Create, false), false, true, plans.Create, false), }, "list_update_sensitive": { input: structured.Change{ Before: []interface{}{"one"}, BeforeSensitive: true, After: []interface{}{}, AfterSensitive: true, }, attribute: &jsonprovider.Attribute{ AttributeType: unmarshalType(t, cty.List(cty.String)), }, validateDiff: renderers.ValidateSensitive(renderers.ValidateList([]renderers.ValidateDiffFunction{ renderers.ValidatePrimitive("one", nil, plans.Delete, false), }, plans.Update, false), true, true, plans.Update, false), }, "list_delete_sensitive": { input: structured.Change{ Before: []interface{}{}, BeforeSensitive: true, After: nil, }, attribute: &jsonprovider.Attribute{ AttributeType: unmarshalType(t, cty.List(cty.String)), }, validateDiff: renderers.ValidateSensitive(renderers.ValidateList(nil, plans.Delete, false), true, false, plans.Delete, false), }, "list_create_unknown": { input: structured.Change{ Before: nil, After: []interface{}{}, Unknown: true, }, attribute: &jsonprovider.Attribute{ AttributeType: unmarshalType(t, cty.List(cty.String)), }, validateDiff: renderers.ValidateUnknown(nil, plans.Create, false), }, "list_update_unknown": { input: structured.Change{ Before: []interface{}{}, After: []interface{}{"one"}, Unknown: true, }, attribute: &jsonprovider.Attribute{ AttributeType: unmarshalType(t, cty.List(cty.String)), }, validateDiff: renderers.ValidateUnknown(renderers.ValidateList(nil, plans.Delete, false), plans.Update, false), }, "set_create_empty": { input: structured.Change{ Before: nil, After: []interface{}{}, }, attribute: &jsonprovider.Attribute{ AttributeType: unmarshalType(t, cty.Set(cty.String)), }, validateDiff: renderers.ValidateSet(nil, plans.Create, false), }, "set_create_populated": { input: structured.Change{ Before: nil, After: []interface{}{"one", "two"}, }, attribute: &jsonprovider.Attribute{ AttributeType: unmarshalType(t, cty.Set(cty.String)), }, validateDiff: renderers.ValidateSet([]renderers.ValidateDiffFunction{ renderers.ValidatePrimitive(nil, "one", plans.Create, false), renderers.ValidatePrimitive(nil, "two", plans.Create, false), }, plans.Create, false), }, "set_delete_empty": { input: structured.Change{ Before: []interface{}{}, After: nil, }, attribute: &jsonprovider.Attribute{ AttributeType: unmarshalType(t, cty.Set(cty.String)), }, validateDiff: renderers.ValidateSet(nil, plans.Delete, false), }, "set_delete_populated": { input: structured.Change{ Before: []interface{}{"one", "two"}, After: nil, }, attribute: &jsonprovider.Attribute{ AttributeType: unmarshalType(t, cty.Set(cty.String)), }, validateDiff: renderers.ValidateSet([]renderers.ValidateDiffFunction{ renderers.ValidatePrimitive("one", nil, plans.Delete, false), renderers.ValidatePrimitive("two", nil, plans.Delete, false), }, plans.Delete, false), }, "set_create_sensitive": { input: structured.Change{ Before: nil, After: []interface{}{}, AfterSensitive: true, }, attribute: &jsonprovider.Attribute{ AttributeType: unmarshalType(t, cty.Set(cty.String)), }, validateDiff: renderers.ValidateSensitive(renderers.ValidateSet(nil, plans.Create, false), false, true, plans.Create, false), }, "set_update_sensitive": { input: structured.Change{ Before: []interface{}{"one"}, BeforeSensitive: true, After: []interface{}{}, AfterSensitive: true, }, attribute: &jsonprovider.Attribute{ AttributeType: unmarshalType(t, cty.Set(cty.String)), }, validateDiff: renderers.ValidateSensitive(renderers.ValidateSet([]renderers.ValidateDiffFunction{ renderers.ValidatePrimitive("one", nil, plans.Delete, false), }, plans.Update, false), true, true, plans.Update, false), }, "set_delete_sensitive": { input: structured.Change{ Before: []interface{}{}, BeforeSensitive: true, After: nil, }, attribute: &jsonprovider.Attribute{ AttributeType: unmarshalType(t, cty.Set(cty.String)), }, validateDiff: renderers.ValidateSensitive(renderers.ValidateSet(nil, plans.Delete, false), true, false, plans.Delete, false), }, "set_create_unknown": { input: structured.Change{ Before: nil, After: []interface{}{}, Unknown: true, }, attribute: &jsonprovider.Attribute{ AttributeType: unmarshalType(t, cty.Set(cty.String)), }, validateDiff: renderers.ValidateUnknown(nil, plans.Create, false), }, "set_update_unknown": { input: structured.Change{ Before: []interface{}{}, After: []interface{}{"one"}, Unknown: true, }, attribute: &jsonprovider.Attribute{ AttributeType: unmarshalType(t, cty.Set(cty.String)), }, validateDiff: renderers.ValidateUnknown(renderers.ValidateSet(nil, plans.Delete, false), plans.Update, false), }, "tuple_primitive": { input: structured.Change{ Before: []interface{}{ "one", json.Number("2"), "three", }, After: []interface{}{ "one", json.Number("4"), "three", }, }, attribute: &jsonprovider.Attribute{ AttributeType: unmarshalType(t, cty.Tuple([]cty.Type{cty.String, cty.Number, cty.String})), }, validateDiff: renderers.ValidateList([]renderers.ValidateDiffFunction{ renderers.ValidatePrimitive("one", "one", plans.NoOp, false), renderers.ValidatePrimitive(json.Number("2"), json.Number("4"), plans.Update, false), renderers.ValidatePrimitive("three", "three", plans.NoOp, false), }, plans.Update, false), }, } for name, tc := range tcs { // Let's set some default values on the input. if tc.input.RelevantAttributes == nil { tc.input.RelevantAttributes = attribute_path.AlwaysMatcher() } if tc.input.ReplacePaths == nil { tc.input.ReplacePaths = &attribute_path.PathMatcher{} } t.Run(name, func(t *testing.T) { tc.validateDiff(t, ComputeDiffForAttribute(tc.input, tc.attribute, tc.input.CalculateAction())) }) } } func TestRelevantAttributes(t *testing.T) { tcs := map[string]struct { input structured.Change block *jsonprovider.Block validate renderers.ValidateDiffFunction }{ "simple_attributes": { input: structured.Change{ Before: map[string]interface{}{ "id": "old_id", "ignore": "doesn't matter", }, After: map[string]interface{}{ "id": "new_id", "ignore": "doesn't matter but modified", }, RelevantAttributes: &attribute_path.PathMatcher{ Paths: [][]interface{}{ { "id", }, }, }, }, block: &jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "id": { AttributeType: unmarshalType(t, cty.String), }, "ignore": { AttributeType: unmarshalType(t, cty.String), }, }, }, validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "id": renderers.ValidatePrimitive("old_id", "new_id", plans.Update, false), "ignore": renderers.ValidatePrimitive("doesn't matter", "doesn't matter", plans.NoOp, false), }, nil, nil, nil, nil, plans.Update, false), }, "nested_attributes": { input: structured.Change{ Before: map[string]interface{}{ "list_block": []interface{}{ map[string]interface{}{ "id": "old_one", }, map[string]interface{}{ "id": "ignored", }, }, }, After: map[string]interface{}{ "list_block": []interface{}{ map[string]interface{}{ "id": "new_one", }, map[string]interface{}{ "id": "ignored_but_changed", }, }, }, RelevantAttributes: &attribute_path.PathMatcher{ Paths: [][]interface{}{ { "list_block", float64(0), "id", }, }, }, }, block: &jsonprovider.Block{ BlockTypes: map[string]*jsonprovider.BlockType{ "list_block": { Block: &jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "id": { AttributeType: unmarshalType(t, cty.String), }, }, }, NestingMode: "list", }, }, }, validate: renderers.ValidateBlock(nil, nil, map[string][]renderers.ValidateDiffFunction{ "list_block": { renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "id": renderers.ValidatePrimitive("old_one", "new_one", plans.Update, false), }, nil, nil, nil, nil, plans.Update, false), renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "id": renderers.ValidatePrimitive("ignored", "ignored", plans.NoOp, false), }, nil, nil, nil, nil, plans.NoOp, false), }, }, nil, nil, plans.Update, false), }, "nested_attributes_in_object": { input: structured.Change{ Before: map[string]interface{}{ "object": map[string]interface{}{ "id": "old_id", }, }, After: map[string]interface{}{ "object": map[string]interface{}{ "id": "new_id", }, }, RelevantAttributes: &attribute_path.PathMatcher{ Propagate: true, Paths: [][]interface{}{ { "object", // Even though we just specify object, it should now include every below object as well. }, }, }, }, block: &jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "object": { AttributeType: unmarshalType(t, cty.Object(map[string]cty.Type{ "id": cty.String, })), }, }, }, validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "object": renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ "id": renderers.ValidatePrimitive("old_id", "new_id", plans.Update, false), }, plans.Update, false), }, nil, nil, nil, nil, plans.Update, false), }, "elements_in_list": { input: structured.Change{ Before: map[string]interface{}{ "list": []interface{}{ json.Number("0"), json.Number("1"), json.Number("2"), json.Number("3"), json.Number("4"), }, }, After: map[string]interface{}{ "list": []interface{}{ json.Number("0"), json.Number("5"), json.Number("6"), json.Number("7"), json.Number("4"), }, }, RelevantAttributes: &attribute_path.PathMatcher{ Paths: [][]interface{}{ // The list is actually just going to ignore this. { "list", 0.0, }, { "list", 2.0, }, { "list", 4.0, }, }, }, }, block: &jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "list": { AttributeType: unmarshalType(t, cty.List(cty.Number)), }, }, }, validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ // The list validator below just ignores our relevant // attributes. This is deliberate. "list": renderers.ValidateList([]renderers.ValidateDiffFunction{ renderers.ValidatePrimitive(json.Number("0"), json.Number("0"), plans.NoOp, false), renderers.ValidatePrimitive(json.Number("1"), nil, plans.Delete, false), renderers.ValidatePrimitive(json.Number("2"), nil, plans.Delete, false), renderers.ValidatePrimitive(json.Number("3"), nil, plans.Delete, false), renderers.ValidatePrimitive(nil, json.Number("5"), plans.Create, false), renderers.ValidatePrimitive(nil, json.Number("6"), plans.Create, false), renderers.ValidatePrimitive(nil, json.Number("7"), plans.Create, false), renderers.ValidatePrimitive(json.Number("4"), json.Number("4"), plans.NoOp, false), }, plans.Update, false), }, nil, nil, nil, nil, plans.Update, false), }, "elements_in_map": { input: structured.Change{ Before: map[string]interface{}{ "map": map[string]interface{}{ "key_one": "value_one", "key_two": "value_two", "key_three": "value_three", }, }, After: map[string]interface{}{ "map": map[string]interface{}{ "key_one": "value_three", "key_two": "value_seven", "key_four": "value_four", }, }, RelevantAttributes: &attribute_path.PathMatcher{ Paths: [][]interface{}{ { "map", "key_one", }, { "map", "key_three", }, { "map", "key_four", }, }, }, }, block: &jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "map": { AttributeType: unmarshalType(t, cty.Map(cty.String)), }, }, }, validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "map": renderers.ValidateMap(map[string]renderers.ValidateDiffFunction{ "key_one": renderers.ValidatePrimitive("value_one", "value_three", plans.Update, false), "key_two": renderers.ValidatePrimitive("value_two", "value_two", plans.NoOp, false), "key_three": renderers.ValidatePrimitive("value_three", nil, plans.Delete, false), "key_four": renderers.ValidatePrimitive(nil, "value_four", plans.Create, false), }, plans.Update, false), }, nil, nil, nil, nil, plans.Update, false), }, "elements_in_set": { input: structured.Change{ Before: map[string]interface{}{ "set": []interface{}{ json.Number("0"), json.Number("1"), json.Number("2"), json.Number("3"), json.Number("4"), }, }, After: map[string]interface{}{ "set": []interface{}{ json.Number("0"), json.Number("2"), json.Number("4"), json.Number("5"), json.Number("6"), }, }, RelevantAttributes: &attribute_path.PathMatcher{ Propagate: true, Paths: [][]interface{}{ { "set", }, }, }, }, block: &jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "set": { AttributeType: unmarshalType(t, cty.Set(cty.Number)), }, }, }, validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "set": renderers.ValidateSet([]renderers.ValidateDiffFunction{ renderers.ValidatePrimitive(json.Number("0"), json.Number("0"), plans.NoOp, false), renderers.ValidatePrimitive(json.Number("1"), nil, plans.Delete, false), renderers.ValidatePrimitive(json.Number("2"), json.Number("2"), plans.NoOp, false), renderers.ValidatePrimitive(json.Number("3"), nil, plans.Delete, false), renderers.ValidatePrimitive(json.Number("4"), json.Number("4"), plans.NoOp, false), renderers.ValidatePrimitive(nil, json.Number("5"), plans.Create, false), renderers.ValidatePrimitive(nil, json.Number("6"), plans.Create, false), }, plans.Update, false), }, nil, nil, nil, nil, plans.Update, false), }, "dynamic_types": { input: structured.Change{ Before: map[string]interface{}{ "dynamic_nested_type": map[string]interface{}{ "nested_id": "nomatch", "nested_object": map[string]interface{}{ "nested_nested_id": "matched", }, }, "dynamic_nested_type_match": map[string]interface{}{ "nested_id": "allmatch", "nested_object": map[string]interface{}{ "nested_nested_id": "allmatch", }, }, }, After: map[string]interface{}{ "dynamic_nested_type": map[string]interface{}{ "nested_id": "nomatch_changed", "nested_object": map[string]interface{}{ "nested_nested_id": "matched", }, }, "dynamic_nested_type_match": map[string]interface{}{ "nested_id": "allmatch", "nested_object": map[string]interface{}{ "nested_nested_id": "allmatch", }, }, }, RelevantAttributes: &attribute_path.PathMatcher{ Propagate: true, Paths: [][]interface{}{ { "dynamic_nested_type", "nested_object", "nested_nested_id", }, { "dynamic_nested_type_match", }, }, }, }, block: &jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "dynamic_nested_type": { AttributeType: unmarshalType(t, cty.DynamicPseudoType), }, "dynamic_nested_type_match": { AttributeType: unmarshalType(t, cty.DynamicPseudoType), }, }, }, validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "dynamic_nested_type": renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ "nested_id": renderers.ValidatePrimitive("nomatch", "nomatch", plans.NoOp, false), "nested_object": renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ "nested_nested_id": renderers.ValidatePrimitive("matched", "matched", plans.NoOp, false), }, plans.NoOp, false), }, plans.NoOp, false), "dynamic_nested_type_match": renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ "nested_id": renderers.ValidatePrimitive("allmatch", "allmatch", plans.NoOp, false), "nested_object": renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ "nested_nested_id": renderers.ValidatePrimitive("allmatch", "allmatch", plans.NoOp, false), }, plans.NoOp, false), }, plans.NoOp, false), }, nil, nil, nil, nil, plans.NoOp, false), }, } for name, tc := range tcs { if tc.input.ReplacePaths == nil { tc.input.ReplacePaths = &attribute_path.PathMatcher{} } t.Run(name, func(t *testing.T) { tc.validate(t, ComputeDiffForBlock(tc.input, tc.block)) }) } } func TestDynamicPseudoType(t *testing.T) { tcs := map[string]struct { input structured.Change validate renderers.ValidateDiffFunction }{ "after_sensitive_in_dynamic_type": { input: structured.Change{ Before: nil, After: map[string]interface{}{ "key": "value", }, Unknown: false, BeforeSensitive: false, AfterSensitive: map[string]interface{}{ "key": true, }, ReplacePaths: attribute_path.Empty(false), RelevantAttributes: attribute_path.AlwaysMatcher(), }, validate: renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ "key": renderers.ValidateSensitive(renderers.ValidatePrimitive(nil, "value", plans.Create, false), false, true, plans.Create, false), }, plans.Create, false), }, "before_sensitive_in_dynamic_type": { input: structured.Change{ Before: map[string]interface{}{ "key": "value", }, After: nil, Unknown: false, BeforeSensitive: map[string]interface{}{ "key": true, }, AfterSensitive: false, ReplacePaths: attribute_path.Empty(false), RelevantAttributes: attribute_path.AlwaysMatcher(), }, validate: renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ "key": renderers.ValidateSensitive(renderers.ValidatePrimitive("value", nil, plans.Delete, false), true, false, plans.Delete, false), }, plans.Delete, false), }, "sensitive_in_dynamic_type": { input: structured.Change{ Before: map[string]interface{}{ "key": "before", }, After: map[string]interface{}{ "key": "after", }, Unknown: false, BeforeSensitive: map[string]interface{}{ "key": true, }, AfterSensitive: map[string]interface{}{ "key": true, }, ReplacePaths: attribute_path.Empty(false), RelevantAttributes: attribute_path.AlwaysMatcher(), }, validate: renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ "key": renderers.ValidateSensitive(renderers.ValidatePrimitive("before", "after", plans.Update, false), true, true, plans.Update, false), }, plans.Update, false), }, "create_unknown_in_dynamic_type": { input: structured.Change{ Before: nil, After: map[string]interface{}{}, Unknown: map[string]interface{}{ "key": true, }, BeforeSensitive: false, AfterSensitive: false, ReplacePaths: attribute_path.Empty(false), RelevantAttributes: attribute_path.AlwaysMatcher(), }, validate: renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ "key": renderers.ValidateUnknown(nil, plans.Create, false), }, plans.Create, false), }, "update_unknown_in_dynamic_type": { input: structured.Change{ Before: map[string]interface{}{ "key": "before", }, After: map[string]interface{}{}, Unknown: map[string]interface{}{ "key": true, }, BeforeSensitive: false, AfterSensitive: false, ReplacePaths: attribute_path.Empty(false), RelevantAttributes: attribute_path.AlwaysMatcher(), }, validate: renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ "key": renderers.ValidateUnknown(renderers.ValidatePrimitive("before", nil, plans.Delete, false), plans.Update, false), }, plans.Update, false), }, } for key, tc := range tcs { t.Run(key, func(t *testing.T) { tc.validate(t, ComputeDiffForType(tc.input, cty.DynamicPseudoType)) }) } } func TestSpecificCases(t *testing.T) { // This is a special test that can contain any combination of individual // cases and will execute against them. For testing/fixing specific issues // you can generally put the test case in here. tcs := map[string]struct { input structured.Change block *jsonprovider.Block validate renderers.ValidateDiffFunction }{ "issues/33016/unknown": { input: structured.Change{ Before: nil, After: map[string]interface{}{ "triggers": map[string]interface{}{}, }, Unknown: map[string]interface{}{ "id": true, "triggers": map[string]interface{}{ "rotation": true, }, }, BeforeSensitive: false, AfterSensitive: map[string]interface{}{ "triggers": map[string]interface{}{}, }, ReplacePaths: attribute_path.Empty(false), RelevantAttributes: attribute_path.AlwaysMatcher(), }, block: &jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "id": { AttributeType: unmarshalType(t, cty.String), }, "triggers": { AttributeType: unmarshalType(t, cty.Map(cty.String)), }, }, }, validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "id": renderers.ValidateUnknown(nil, plans.Create, false), "triggers": renderers.ValidateMap(map[string]renderers.ValidateDiffFunction{ "rotation": renderers.ValidateUnknown(nil, plans.Create, false), }, plans.Create, false), }, nil, nil, nil, nil, plans.Create, false), }, "issues/33016/null": { input: structured.Change{ Before: nil, After: map[string]interface{}{ "triggers": map[string]interface{}{ "rotation": nil, }, }, Unknown: map[string]interface{}{ "id": true, "triggers": map[string]interface{}{}, }, BeforeSensitive: false, AfterSensitive: map[string]interface{}{ "triggers": map[string]interface{}{}, }, ReplacePaths: attribute_path.Empty(false), RelevantAttributes: attribute_path.AlwaysMatcher(), }, block: &jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "id": { AttributeType: unmarshalType(t, cty.String), }, "triggers": { AttributeType: unmarshalType(t, cty.Map(cty.String)), }, }, }, validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "id": renderers.ValidateUnknown(nil, plans.Create, false), "triggers": renderers.ValidateMap(map[string]renderers.ValidateDiffFunction{ "rotation": renderers.ValidatePrimitive(nil, nil, plans.Create, false), }, plans.Create, false), }, nil, nil, nil, nil, plans.Create, false), }, // The following tests are from issue 33472. Basically OpenTofu allows // callers to treat numbers as strings in references and expects us // to coerce the strings into numbers. For example the following are // equivalent. // - test_resource.resource.list[0].attribute // - test_resource.resource.list["0"].attribute // // We need our attribute_path package (used within the ReplacePaths and // RelevantAttributes fields) to handle coercing strings into numbers // when it's expected. "issues/33472/expected": { input: structured.Change{ Before: map[string]interface{}{ "list": []interface{}{ map[string]interface{}{ "number": json.Number("-1"), }, }, }, After: map[string]interface{}{ "list": []interface{}{ map[string]interface{}{ "number": json.Number("2"), }, }, }, Unknown: false, BeforeSensitive: false, AfterSensitive: false, ReplacePaths: attribute_path.Empty(false), RelevantAttributes: &attribute_path.PathMatcher{ Propagate: true, Paths: [][]interface{}{ { "list", 0.0, // This is normal and expected so easy case. "number", }, }, }, }, block: &jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "list": { AttributeType: unmarshalType(t, cty.List(cty.Object(map[string]cty.Type{ "number": cty.Number, }))), }, }, }, validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "list": renderers.ValidateList([]renderers.ValidateDiffFunction{ renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ "number": renderers.ValidatePrimitive(json.Number("-1"), json.Number("2"), plans.Update, false), }, plans.Update, false), }, plans.Update, false), }, nil, nil, nil, nil, plans.Update, false), }, "issues/33472/coerce": { input: structured.Change{ Before: map[string]interface{}{ "list": []interface{}{ map[string]interface{}{ "number": json.Number("-1"), }, }, }, After: map[string]interface{}{ "list": []interface{}{ map[string]interface{}{ "number": json.Number("2"), }, }, }, Unknown: false, BeforeSensitive: false, AfterSensitive: false, ReplacePaths: attribute_path.Empty(false), RelevantAttributes: &attribute_path.PathMatcher{ Propagate: true, Paths: [][]interface{}{ { "list", "0", // Difficult but allowed, we need to handle this. "number", }, }, }, }, block: &jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "list": { AttributeType: unmarshalType(t, cty.List(cty.Object(map[string]cty.Type{ "number": cty.Number, }))), }, }, }, validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "list": renderers.ValidateList([]renderers.ValidateDiffFunction{ renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ "number": renderers.ValidatePrimitive(json.Number("-1"), json.Number("2"), plans.Update, false), }, plans.Update, false), }, plans.Update, false), }, nil, nil, nil, nil, plans.Update, false), }, // Following tests are from issue 1805. https://github.com/opentofu/opentofu/issues/1805. // The issue is about handling unknown dynamic nested blocks. In these cases unknown nested blocks are // not shown at all but they should be listed as unknown in the diff. "issues/1805/create_with_unknown_dynamic_nested_block": { input: structured.Change{ Before: nil, After: map[string]interface{}{ "attribute_one": "test", }, Unknown: map[string]interface{}{ "nested_unknown_block": true, }, ReplacePaths: &attribute_path.PathMatcher{}, RelevantAttributes: attribute_path.AlwaysMatcher(), }, block: &jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "attribute_one": { AttributeType: unmarshalType(t, cty.String), }, }, BlockTypes: map[string]*jsonprovider.BlockType{ "nested_unknown_block": { Block: &jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "attribute_two": { AttributeType: unmarshalType(t, cty.String), }, }, }, NestingMode: "single", }, }, }, validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive(nil, "test", plans.Create, false), }, map[string]renderers.ValidateDiffFunction{ "nested_unknown_block": renderers.ValidateUnknown(nil, plans.Create, false), }, nil, nil, nil, plans.Create, false), }, "issues/1805/update_with_unknown_dynamic_nested_block": { input: structured.Change{ Before: map[string]interface{}{ "attribute_one": "before_value_attr_1", "attribute_two": "before_value_attr_2", }, After: map[string]interface{}{ "attribute_one": "after_value_attr_1", }, Unknown: map[string]interface{}{ "nested_unknown_block": true, }, ReplacePaths: &attribute_path.PathMatcher{}, RelevantAttributes: attribute_path.AlwaysMatcher(), }, block: &jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "attribute_one": { AttributeType: unmarshalType(t, cty.String), }, "attribute_two": { AttributeType: unmarshalType(t, cty.String), }, }, BlockTypes: map[string]*jsonprovider.BlockType{ "nested_unknown_block": { Block: &jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "attribute_two": { AttributeType: unmarshalType(t, cty.String), }, }, }, NestingMode: "single", }, }, }, validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "attribute_one": renderers.ValidatePrimitive("before_value_attr_1", "after_value_attr_1", plans.Update, false), "attribute_two": renderers.ValidatePrimitive("before_value_attr_2", nil, plans.Delete, false), }, map[string]renderers.ValidateDiffFunction{ "nested_unknown_block": renderers.ValidateUnknown(nil, plans.Create, false), }, nil, nil, nil, plans.Update, false), }, } for name, tc := range tcs { t.Run(name, func(t *testing.T) { tc.validate(t, ComputeDiffForBlock(tc.input, tc.block)) }) } } func TestWriteOnly_ComputeDiffForBlock(t *testing.T) { // NOTE: The write-only change will *always* have the after change null. // We set specific values in this test for the after change to ensure that, // based on the attribute's schema, we use the correct renderers for these attributes. cases := map[string]struct { block jsonprovider.Block change structured.Change validator renderers.ValidateDiffFunction }{ // ---> Attributes with basic types // attributes with basic types "write_only_primitive_attribute": { block: jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "write_only_attribute": { AttributeType: unmarshalType(t, cty.String), Optional: true, WriteOnly: true, }, }, }, change: structured.Change{ After: map[string]any{ "write_only_attribute": "dummy value", // NOTE: since this is WO attr, IRL it's nil }, }, validator: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "write_only_attribute": renderers.ValidateWriteOnly(plans.Create, false), }, nil, nil, nil, nil, plans.Create, false), }, "write_only_list": { block: jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "write_only_list": { AttributeType: unmarshalType(t, cty.List(cty.Object(map[string]cty.Type{"val": cty.String}))), Optional: true, WriteOnly: true, }, }, }, change: structured.Change{ After: map[string]any{ "write_only_list": []string{"val", "b"}, // NOTE: since this is WO attr, IRL it's nil }, }, validator: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "write_only_list": renderers.ValidateWriteOnly(plans.Create, false), }, nil, nil, nil, nil, plans.Create, false), }, "write_only_set": { block: jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "write_only_set": { AttributeType: unmarshalType(t, cty.Set(cty.Object(map[string]cty.Type{"val": cty.String}))), Optional: true, WriteOnly: true, }, }, }, change: structured.Change{ After: map[string]any{ "write_only_set": []string{"val", "b"}, // NOTE: since this is WO attr, IRL it's nil }, }, validator: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "write_only_set": renderers.ValidateWriteOnly(plans.Create, false), }, nil, nil, nil, nil, plans.Create, false), }, "write_only_map": { block: jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "write_only_map": { AttributeType: unmarshalType(t, cty.Map(cty.String)), Optional: true, WriteOnly: true, }, }, }, change: structured.Change{ After: map[string]any{ "write_only_map": map[string]string{"bar": "barv", "baz": "bazv"}, // NOTE: since this is WO attr, IRL it's nil }, }, validator: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "write_only_map": renderers.ValidateWriteOnly(plans.Create, false), }, nil, nil, nil, nil, plans.Create, false), }, "write_only_object": { block: jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "write_only_obj": { AttributeType: unmarshalType(t, cty.Object(map[string]cty.Type{"val": cty.String})), Optional: true, WriteOnly: true, }, }, }, change: structured.Change{ After: map[string]any{ "write_only_obj": map[string]string{"bar": "barv"}, // NOTE: since this is WO attr, IRL it's nil }, }, validator: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ "write_only_obj": renderers.ValidateWriteOnly(plans.Create, false), }, nil, nil, nil, nil, plans.Create, false), }, // ---> Attributes with nested types "attribute_single_nested_contains_write_only_attribute": { block: jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "write_only_obj": { AttributeNestedType: &jsonprovider.NestedType{ Attributes: map[string]*jsonprovider.Attribute{ "val": { AttributeType: unmarshalType(t, cty.Object(map[string]cty.Type{"val": cty.String})), Optional: true, WriteOnly: true, }, }, NestingMode: "single", }, }, }, }, change: structured.Change{ After: map[string]any{ "write_only_obj": map[string]any{"val": "foo"}, // NOTE: since this is WO attr, IRL it's nil }, }, validator: renderers.ValidateBlock( map[string]renderers.ValidateDiffFunction{ "write_only_obj": renderers.ValidateNestedObject( map[string]renderers.ValidateDiffFunction{ "val": renderers.ValidateWriteOnly(plans.Create, false), }, plans.Create, false), }, nil, nil, nil, nil, plans.Create, false), }, "attribute_map_nested_contains_write_only_attribute": { block: jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "write_only_map": { AttributeNestedType: &jsonprovider.NestedType{ Attributes: map[string]*jsonprovider.Attribute{ "val": { AttributeType: unmarshalType(t, cty.Object(map[string]cty.Type{"val": cty.String})), Optional: true, WriteOnly: true, }, }, NestingMode: "map", }, }, }, }, change: structured.Change{ After: map[string]any{ "write_only_map": map[string]any{"val": "val value"}, // NOTE: since this is WO attr, IRL it's nil }, }, validator: renderers.ValidateBlock( map[string]renderers.ValidateDiffFunction{ "write_only_map": renderers.ValidateMap( map[string]renderers.ValidateDiffFunction{ "val": renderers.ValidateNestedObject( map[string]renderers.ValidateDiffFunction{ "val": renderers.ValidateWriteOnly(plans.Create, false), }, plans.Create, false), }, plans.Create, false), }, nil, nil, nil, nil, plans.Create, false), }, "attribute_list_nested_contains_write_only_attribute": { block: jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "write_only_list": { AttributeNestedType: &jsonprovider.NestedType{ Attributes: map[string]*jsonprovider.Attribute{ "val": { AttributeType: unmarshalType(t, cty.Object(map[string]cty.Type{"val": cty.String})), Optional: true, WriteOnly: true, }, }, NestingMode: "list", }, }, }, }, change: structured.Change{ After: map[string]any{ "write_only_list": []any{map[string]any{"val": "foo value"}}, // NOTE: since this is WO attr, IRL it's nil }, }, validator: renderers.ValidateBlock( map[string]renderers.ValidateDiffFunction{ "write_only_list": renderers.ValidateNestedList( []renderers.ValidateDiffFunction{ renderers.ValidateNestedObject( map[string]renderers.ValidateDiffFunction{ "val": renderers.ValidateWriteOnly(plans.Create, false), }, plans.Create, false), }, plans.Create, false), }, nil, nil, nil, nil, plans.Create, false), }, "attribute_set_nested_contains_write_only_attribute": { block: jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "write_only_set": { AttributeNestedType: &jsonprovider.NestedType{ Attributes: map[string]*jsonprovider.Attribute{ "val": { AttributeType: unmarshalType(t, cty.Object(map[string]cty.Type{"val": cty.String})), Optional: true, WriteOnly: true, }, }, NestingMode: "set", }, }, }, }, change: structured.Change{ After: map[string]any{ "write_only_set": []any{map[string]any{"val": "foo value"}}, // NOTE: since this is WO attr, IRL it's nil }, }, validator: renderers.ValidateBlock( map[string]renderers.ValidateDiffFunction{ "write_only_set": renderers.ValidateSet( []renderers.ValidateDiffFunction{ renderers.ValidateNestedObject( map[string]renderers.ValidateDiffFunction{ "val": renderers.ValidateWriteOnly(plans.Create, false), }, plans.Create, false), }, plans.Create, false), }, nil, nil, nil, nil, plans.Create, false), }, // ---> Nested blocks "single_nested_block": { block: jsonprovider.Block{ BlockTypes: map[string]*jsonprovider.BlockType{ "nested_block": { NestingMode: "single", Block: &jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "write_only_attr": { AttributeType: unmarshalType(t, cty.String), WriteOnly: true, }, }, }, }, }, }, change: structured.Change{ After: map[string]any{ "nested_block": map[string]any{"write_only_attr": "foo"}, // NOTE: since this is WO attr, IRL it's nil }, }, validator: renderers.ValidateBlock( nil, map[string]renderers.ValidateDiffFunction{ "nested_block": renderers.ValidateBlock( map[string]renderers.ValidateDiffFunction{ "write_only_attr": renderers.ValidateWriteOnly(plans.Create, false), }, nil, nil, nil, nil, plans.Create, false), }, nil, nil, nil, plans.Create, false), }, "map_nested_block": { block: jsonprovider.Block{ BlockTypes: map[string]*jsonprovider.BlockType{ "nested_block": { NestingMode: "map", Block: &jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "write_only_attr": { AttributeType: unmarshalType(t, cty.String), WriteOnly: true, }, }, }, }, }, }, change: structured.Change{ After: map[string]any{ "nested_block": map[string]any{"write_only_attr": "foo"}, // NOTE: since this is WO attr, IRL it's nil }, }, validator: renderers.ValidateBlock( nil, nil, nil, map[string]map[string]renderers.ValidateDiffFunction{ "nested_block": { "write_only_attr": renderers.ValidateBlock( map[string]renderers.ValidateDiffFunction{ "write_only_attr": renderers.ValidateWriteOnly(plans.Create, false), }, nil, nil, nil, nil, plans.Create, false), }, }, nil, plans.Create, false), }, "list_nested_block": { block: jsonprovider.Block{ BlockTypes: map[string]*jsonprovider.BlockType{ "nested_block": { NestingMode: "list", Block: &jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "write_only_attr": { AttributeType: unmarshalType(t, cty.String), WriteOnly: true, }, }, }, }, }, }, change: structured.Change{ After: map[string]any{ "nested_block": []any{"value"}, // NOTE: since this is WO attr, IRL it's nil }, }, validator: renderers.ValidateBlock( nil, nil, map[string][]renderers.ValidateDiffFunction{ "nested_block": { renderers.ValidateBlock( map[string]renderers.ValidateDiffFunction{ "write_only_attr": renderers.ValidateWriteOnly(plans.Create, false), }, nil, nil, nil, nil, plans.Create, false), }, }, nil, nil, plans.Create, false), }, "set_nested_block": { block: jsonprovider.Block{ BlockTypes: map[string]*jsonprovider.BlockType{ "nested_block": { NestingMode: "set", Block: &jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "write_only_attr": { AttributeType: unmarshalType(t, cty.String), WriteOnly: true, }, }, }, }, }, }, change: structured.Change{ After: map[string]any{ "nested_block": []any{"value"}, // NOTE: since this is WO attr, IRL it's nil }, }, validator: renderers.ValidateBlock( nil, nil, nil, nil, map[string][]renderers.ValidateDiffFunction{ "nested_block": { renderers.ValidateBlock( map[string]renderers.ValidateDiffFunction{ "write_only_attr": renderers.ValidateWriteOnly(plans.Create, false), }, nil, nil, nil, nil, plans.Create, false), }, }, plans.Create, false), }, "write-only are shown during no-op too": { block: jsonprovider.Block{ Attributes: map[string]*jsonprovider.Attribute{ "write_only_attr": { WriteOnly: true, AttributeType: unmarshalType(t, cty.String), }, "regular_attr": { AttributeType: unmarshalType(t, cty.String), }, }, }, change: structured.Change{ After: map[string]any{ "write_only_attr": nil, "regular_attr": "test", }, Before: map[string]any{ "write_only_attr": nil, "regular_attr": "test", }, }, validator: renderers.ValidateBlock( map[string]renderers.ValidateDiffFunction{ "write_only_attr": renderers.ValidateWriteOnly(plans.NoOp, false), "regular_attr": renderers.ValidatePrimitive("test", "test", plans.NoOp, false), }, nil, nil, nil, nil, plans.NoOp, false), }, } for name, tt := range cases { t.Run(name, func(t *testing.T) { if tt.change.ReplacePaths == nil { tt.change.ReplacePaths = &attribute_path.PathMatcher{} } if tt.change.RelevantAttributes == nil { tt.change.RelevantAttributes = attribute_path.AlwaysMatcher() } diff := ComputeDiffForBlock(tt.change, &tt.block) tt.validator(t, diff) }) } } // unmarshalType converts a cty.Type into a json.RawMessage understood by the // schema. It also lets the testing framework handle any errors to keep the API // clean. func unmarshalType(t *testing.T, ctyType cty.Type) json.RawMessage { msg, err := ctyjson.MarshalType(ctyType) if err != nil { t.Fatalf("invalid type: %s", ctyType.FriendlyName()) } return msg } // wrapChangeInSlice does the same as wrapChangeInMap, except it wraps it into a // slice internally. func wrapChangeInSlice(input structured.Change) structured.Change { return wrapChange(input, float64(0), func(value interface{}, unknown interface{}, explicit bool) interface{} { switch value.(type) { case nil: if set, ok := unknown.(bool); (set && ok) || explicit { return []interface{}{nil} } return []interface{}{} default: return []interface{}{value} } }) } // wrapChangeInMap access a single structured.Change and returns a new // structured.Change that represents a map with a single element. That single // element is the input value. func wrapChangeInMap(input structured.Change) structured.Change { return wrapChange(input, "element", func(value interface{}, unknown interface{}, explicit bool) interface{} { switch value.(type) { case nil: if set, ok := unknown.(bool); (set && ok) || explicit { return map[string]interface{}{ "element": nil, } } return map[string]interface{}{} default: return map[string]interface{}{ "element": value, } } }) } func wrapChange(input structured.Change, step interface{}, wrap func(interface{}, interface{}, bool) interface{}) structured.Change { replacePaths := &attribute_path.PathMatcher{} for _, path := range input.ReplacePaths.(*attribute_path.PathMatcher).Paths { var updated []interface{} updated = append(updated, step) updated = append(updated, path...) replacePaths.Paths = append(replacePaths.Paths, updated) } // relevantAttributes usually default to AlwaysMatcher, which means we can // just ignore it. But if we have had some paths specified we need to wrap // those as well. relevantAttributes := input.RelevantAttributes if concrete, ok := relevantAttributes.(*attribute_path.PathMatcher); ok { newRelevantAttributes := &attribute_path.PathMatcher{} for _, path := range concrete.Paths { var updated []interface{} updated = append(updated, step) updated = append(updated, path...) newRelevantAttributes.Paths = append(newRelevantAttributes.Paths, updated) } relevantAttributes = newRelevantAttributes } return structured.Change{ Before: wrap(input.Before, nil, input.BeforeExplicit), After: wrap(input.After, input.Unknown, input.AfterExplicit), Unknown: wrap(input.Unknown, nil, false), BeforeSensitive: wrap(input.BeforeSensitive, nil, false), AfterSensitive: wrap(input.AfterSensitive, nil, false), ReplacePaths: replacePaths, RelevantAttributes: relevantAttributes, } }