package views import ( "testing" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/lang/globalref" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/terminal" "github.com/hashicorp/terraform/internal/terraform" "github.com/zclconf/go-cty/cty" ) // Ensure that the correct view type and in-automation settings propagate to the // Operation view. func TestPlanHuman_operation(t *testing.T) { streams, done := terminal.StreamsForTesting(t) defer done(t) v := NewPlan(arguments.ViewHuman, NewView(streams).SetRunningInAutomation(true)).Operation() if hv, ok := v.(*OperationHuman); !ok { t.Fatalf("unexpected return type %t", v) } else if hv.inAutomation != true { t.Fatalf("unexpected inAutomation value on Operation view") } } // Verify that Hooks includes a UI hook func TestPlanHuman_hooks(t *testing.T) { streams, done := terminal.StreamsForTesting(t) defer done(t) v := NewPlan(arguments.ViewHuman, NewView(streams).SetRunningInAutomation((true))) hooks := v.Hooks() var uiHook *UiHook for _, hook := range hooks { if ch, ok := hook.(*UiHook); ok { uiHook = ch } } if uiHook == nil { t.Fatalf("expected Hooks to include a UiHook: %#v", hooks) } } // Helper functions to build a trivial test plan, to exercise the plan // renderer. func testPlan(t *testing.T) *plans.Plan { t.Helper() plannedVal := cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("bar"), }) priorValRaw, err := plans.NewDynamicValue(cty.NullVal(plannedVal.Type()), plannedVal.Type()) if err != nil { t.Fatal(err) } plannedValRaw, err := plans.NewDynamicValue(plannedVal, plannedVal.Type()) if err != nil { t.Fatal(err) } changes := plans.NewChanges() addr := addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "foo", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) changes.SyncWrapper().AppendResourceInstanceChange(&plans.ResourceInstanceChangeSrc{ Addr: addr, PrevRunAddr: addr, ProviderAddr: addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ChangeSrc: plans.ChangeSrc{ Action: plans.Create, Before: priorValRaw, After: plannedValRaw, }, }) return &plans.Plan{ Changes: changes, } } func testSchemas() *terraform.Schemas { provider := testProvider() return &terraform.Schemas{ Providers: map[addrs.Provider]*terraform.ProviderSchema{ addrs.NewDefaultProvider("test"): provider.ProviderSchema(), }, } } func testProvider() *terraform.MockProvider { p := new(terraform.MockProvider) p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse { return providers.ReadResourceResponse{NewState: req.PriorState} } p.GetProviderSchemaResponse = testProviderSchema() return p } func testProviderSchema() *providers.GetProviderSchemaResponse { return &providers.GetProviderSchemaResponse{ Provider: providers.Schema{ Block: &configschema.Block{}, }, ResourceTypes: map[string]providers.Schema{ "test_resource": { Block: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Computed: true}, "foo": {Type: cty.String, Optional: true}, }, }, }, }, } } func TestFilterRefreshChange(t *testing.T) { tests := map[string]struct { paths []cty.Path before, after, expected cty.Value }{ "attr was null": { // nested attr was null paths: []cty.Path{ cty.GetAttrPath("attr").GetAttr("attr_null_before").GetAttr("b"), }, before: cty.ObjectVal(map[string]cty.Value{ "attr": cty.ObjectVal(map[string]cty.Value{ "attr_null_before": cty.ObjectVal(map[string]cty.Value{ "a": cty.StringVal("old"), "b": cty.NullVal(cty.String), }), }), }), after: cty.ObjectVal(map[string]cty.Value{ "attr": cty.ObjectVal(map[string]cty.Value{ "attr_null_before": cty.ObjectVal(map[string]cty.Value{ "a": cty.StringVal("new"), "b": cty.StringVal("new"), }), }), }), expected: cty.ObjectVal(map[string]cty.Value{ "attr": cty.ObjectVal(map[string]cty.Value{ "attr_null_before": cty.ObjectVal(map[string]cty.Value{ // we old picked the change in b "a": cty.StringVal("old"), "b": cty.StringVal("new"), }), }), }), }, "object was null": { // nested object attrs were null paths: []cty.Path{ cty.GetAttrPath("attr").GetAttr("obj_null_before").GetAttr("b"), }, before: cty.ObjectVal(map[string]cty.Value{ "attr": cty.ObjectVal(map[string]cty.Value{ "obj_null_before": cty.NullVal(cty.Object(map[string]cty.Type{ "a": cty.String, "b": cty.String, })), "other": cty.ObjectVal(map[string]cty.Value{ "o": cty.StringVal("old"), }), }), }), after: cty.ObjectVal(map[string]cty.Value{ "attr": cty.ObjectVal(map[string]cty.Value{ "obj_null_before": cty.ObjectVal(map[string]cty.Value{ "a": cty.StringVal("new"), "b": cty.StringVal("new"), }), "other": cty.ObjectVal(map[string]cty.Value{ "o": cty.StringVal("new"), }), }), }), expected: cty.ObjectVal(map[string]cty.Value{ "attr": cty.ObjectVal(map[string]cty.Value{ "obj_null_before": cty.ObjectVal(map[string]cty.Value{ // optimally "a" would be null, but we need to take the // entire object since it was null before. "a": cty.StringVal("new"), "b": cty.StringVal("new"), }), "other": cty.ObjectVal(map[string]cty.Value{ "o": cty.StringVal("old"), }), }), }), }, "object becomes null": { // nested object attr becoming null paths: []cty.Path{ cty.GetAttrPath("attr").GetAttr("obj_null_after").GetAttr("a"), }, before: cty.ObjectVal(map[string]cty.Value{ "attr": cty.ObjectVal(map[string]cty.Value{ "obj_null_after": cty.ObjectVal(map[string]cty.Value{ "a": cty.StringVal("old"), "b": cty.StringVal("old"), }), "other": cty.ObjectVal(map[string]cty.Value{ "o": cty.StringVal("old"), }), }), }), after: cty.ObjectVal(map[string]cty.Value{ "attr": cty.ObjectVal(map[string]cty.Value{ "obj_null_after": cty.NullVal(cty.Object(map[string]cty.Type{ "a": cty.String, "b": cty.String, })), "other": cty.ObjectVal(map[string]cty.Value{ "o": cty.StringVal("new"), }), }), }), expected: cty.ObjectVal(map[string]cty.Value{ "attr": cty.ObjectVal(map[string]cty.Value{ "obj_null_after": cty.ObjectVal(map[string]cty.Value{ "a": cty.NullVal(cty.String), "b": cty.StringVal("old"), }), "other": cty.ObjectVal(map[string]cty.Value{ "o": cty.StringVal("old"), }), }), }), }, "dynamic adding values": { // dynamic gaining values paths: []cty.Path{ cty.GetAttrPath("attr").GetAttr("after").GetAttr("a"), }, before: cty.ObjectVal(map[string]cty.Value{ "attr": cty.DynamicVal, }), after: cty.ObjectVal(map[string]cty.Value{ "attr": cty.ObjectVal(map[string]cty.Value{ // the entire attr object is taken here because there is // nothing to compare within the before value "after": cty.ObjectVal(map[string]cty.Value{ "a": cty.StringVal("new"), "b": cty.StringVal("new"), }), "other": cty.ObjectVal(map[string]cty.Value{ "o": cty.StringVal("new"), }), }), }), expected: cty.ObjectVal(map[string]cty.Value{ "attr": cty.ObjectVal(map[string]cty.Value{ "after": cty.ObjectVal(map[string]cty.Value{ "a": cty.StringVal("new"), "b": cty.StringVal("new"), }), // "other" is picked up here too this time, because we need // to take the entire dynamic "attr" value "other": cty.ObjectVal(map[string]cty.Value{ "o": cty.StringVal("new"), }), }), }), }, "whole object becomes null": { // whole object becomes null paths: []cty.Path{ cty.GetAttrPath("attr").GetAttr("after").GetAttr("a"), }, before: cty.ObjectVal(map[string]cty.Value{ "attr": cty.ObjectVal(map[string]cty.Value{ "after": cty.ObjectVal(map[string]cty.Value{ "a": cty.StringVal("old"), "b": cty.StringVal("old"), }), }), }), after: cty.NullVal(cty.Object(map[string]cty.Type{ "attr": cty.DynamicPseudoType, })), // since we have a dynamic type we have to take the entire object // because the paths may not apply between versions. expected: cty.NullVal(cty.Object(map[string]cty.Type{ "attr": cty.DynamicPseudoType, })), }, "whole object was null": { // whole object was null paths: []cty.Path{ cty.GetAttrPath("attr").GetAttr("after").GetAttr("a"), }, before: cty.NullVal(cty.Object(map[string]cty.Type{ "attr": cty.DynamicPseudoType, })), after: cty.ObjectVal(map[string]cty.Value{ "attr": cty.ObjectVal(map[string]cty.Value{ "after": cty.ObjectVal(map[string]cty.Value{ "a": cty.StringVal("new"), "b": cty.StringVal("new"), }), }), }), expected: cty.ObjectVal(map[string]cty.Value{ "attr": cty.ObjectVal(map[string]cty.Value{ "after": cty.ObjectVal(map[string]cty.Value{ "a": cty.StringVal("new"), "b": cty.StringVal("new"), }), }), }), }, "restructured dynamic": { // dynamic value changing structure significantly paths: []cty.Path{ cty.GetAttrPath("attr").GetAttr("list").IndexInt(1).GetAttr("a"), }, before: cty.ObjectVal(map[string]cty.Value{ "attr": cty.ObjectVal(map[string]cty.Value{ "list": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "a": cty.StringVal("old"), }), }), }), }), after: cty.ObjectVal(map[string]cty.Value{ "attr": cty.ObjectVal(map[string]cty.Value{ "after": cty.ObjectVal(map[string]cty.Value{ "a": cty.StringVal("new"), "b": cty.StringVal("new"), }), }), }), // the path does not apply at all to the new object, so we must // take all the changes expected: cty.ObjectVal(map[string]cty.Value{ "attr": cty.ObjectVal(map[string]cty.Value{ "after": cty.ObjectVal(map[string]cty.Value{ "a": cty.StringVal("new"), "b": cty.StringVal("new"), }), }), }), }, } for k, tc := range tests { t.Run(k, func(t *testing.T) { addr, diags := addrs.ParseAbsResourceInstanceStr("test_resource.a") if diags != nil { t.Fatal(diags.ErrWithWarnings()) } change := &plans.ResourceInstanceChange{ Addr: addr, Change: plans.Change{ Before: tc.before, After: tc.after, Action: plans.Update, }, } var contributing []globalref.ResourceAttr for _, p := range tc.paths { contributing = append(contributing, globalref.ResourceAttr{ Resource: addr, Attr: p, }) } res := filterRefreshChange(change, contributing) if !res.After.RawEquals(tc.expected) { t.Errorf("\nexpected: %#v\ngot: %#v\n", tc.expected, res.After) } }) } }