| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package stackruntime | |
| import ( | |
| "context" | |
| "path/filepath" | |
| "testing" | |
| "time" | |
| "github.com/zclconf/go-cty/cty" | |
| "github.com/hashicorp/terraform/internal/addrs" | |
| "github.com/hashicorp/terraform/internal/depsfile" | |
| "github.com/hashicorp/terraform/internal/getproviders/providerreqs" | |
| "github.com/hashicorp/terraform/internal/plans" | |
| "github.com/hashicorp/terraform/internal/providers" | |
| "github.com/hashicorp/terraform/internal/stacks/stackaddrs" | |
| "github.com/hashicorp/terraform/internal/stacks/stackplan" | |
| stacks_testing_provider "github.com/hashicorp/terraform/internal/stacks/stackruntime/testing" | |
| "github.com/hashicorp/terraform/internal/stacks/stackstate" | |
| "github.com/hashicorp/terraform/internal/states" | |
| "github.com/hashicorp/terraform/version" | |
| ) | |
| func TestRefreshPlan(t *testing.T) { | |
| fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| tcs := map[string]struct { | |
| path string | |
| state *stackstate.State | |
| store *stacks_testing_provider.ResourceStore | |
| cycle TestCycle | |
| }{ | |
| "simple-valid": { | |
| path: filepath.Join("with-single-input", "valid"), | |
| state: stackstate.NewStateBuilder(). | |
| AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self"))). | |
| AddResourceInstance(stackstate.NewResourceInstanceBuilder(). | |
| SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.data")). | |
| SetProviderAddr(mustDefaultRootProvider("testing")). | |
| SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ | |
| Status: states.ObjectReady, | |
| AttrsJSON: mustMarshalJSONAttrs(map[string]any{ | |
| "id": "old", | |
| "value": "old", | |
| }), | |
| })). | |
| Build(), | |
| store: stacks_testing_provider.NewResourceStoreBuilder(). | |
| AddResource("old", cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("old"), | |
| "value": cty.StringVal("new"), | |
| })). | |
| Build(), | |
| cycle: TestCycle{ | |
| planInputs: map[string]cty.Value{ | |
| "id": cty.StringVal("old"), | |
| "input": cty.StringVal("old"), | |
| }, | |
| wantPlannedChanges: []stackplan.PlannedChange{ | |
| &stackplan.PlannedChangeApplyable{ | |
| Applyable: true, | |
| }, | |
| &stackplan.PlannedChangeComponentInstance{ | |
| Addr: mustAbsComponentInstance("component.self"), | |
| PlanApplyable: true, | |
| PlanComplete: true, | |
| Action: plans.Read, | |
| Mode: plans.RefreshOnlyMode, | |
| PlannedInputValues: map[string]plans.DynamicValue{ | |
| "id": mustPlanDynamicValueDynamicType(cty.StringVal("old")), | |
| "input": mustPlanDynamicValueDynamicType(cty.StringVal("old")), | |
| }, | |
| PlannedInputValueMarks: map[string][]cty.PathValueMarks{ | |
| "id": nil, | |
| "input": nil, | |
| }, | |
| PlannedOutputValues: make(map[string]cty.Value), | |
| PlannedCheckResults: &states.CheckResults{}, | |
| PlanTimestamp: fakePlanTimestamp, | |
| }, | |
| &stackplan.PlannedChangeResourceInstancePlanned{ | |
| ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), | |
| PriorStateSrc: &states.ResourceInstanceObjectSrc{ | |
| Status: states.ObjectReady, | |
| AttrsJSON: mustMarshalJSONAttrs(map[string]any{ | |
| "id": "old", | |
| "value": "new", | |
| }), | |
| Dependencies: make([]addrs.ConfigResource, 0), | |
| }, | |
| ProviderConfigAddr: mustDefaultRootProvider("testing"), | |
| Schema: stacks_testing_provider.TestingResourceSchema, | |
| }, | |
| &stackplan.PlannedChangeHeader{ | |
| TerraformVersion: version.SemVer, | |
| }, | |
| &stackplan.PlannedChangePlannedTimestamp{ | |
| PlannedTimestamp: fakePlanTimestamp, | |
| }, | |
| &stackplan.PlannedChangeRootInputValue{ | |
| Addr: mustStackInputVariable("id"), | |
| Action: plans.Create, | |
| Before: cty.NullVal(cty.DynamicPseudoType), | |
| After: cty.StringVal("old"), | |
| }, | |
| &stackplan.PlannedChangeRootInputValue{ | |
| Addr: mustStackInputVariable("input"), | |
| Action: plans.Create, | |
| Before: cty.NullVal(cty.DynamicPseudoType), | |
| After: cty.StringVal("old"), | |
| }, | |
| }, | |
| }, | |
| }, | |
| "removed-component": { | |
| path: filepath.Join("with-single-input", "removed-component"), | |
| state: stackstate.NewStateBuilder(). | |
| AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self")). | |
| AddInputVariable("id", cty.StringVal("old")). | |
| AddInputVariable("input", cty.StringVal("old"))). | |
| AddResourceInstance(stackstate.NewResourceInstanceBuilder(). | |
| SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.data")). | |
| SetProviderAddr(mustDefaultRootProvider("testing")). | |
| SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ | |
| Status: states.ObjectReady, | |
| AttrsJSON: mustMarshalJSONAttrs(map[string]any{ | |
| "id": "old", | |
| "value": "old", | |
| }), | |
| })). | |
| Build(), | |
| store: stacks_testing_provider.NewResourceStoreBuilder(). | |
| AddResource("old", cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("old"), | |
| "value": cty.StringVal("new"), | |
| })). | |
| Build(), | |
| cycle: TestCycle{ | |
| wantPlannedChanges: []stackplan.PlannedChange{ | |
| &stackplan.PlannedChangeApplyable{ | |
| Applyable: true, | |
| }, | |
| &stackplan.PlannedChangeComponentInstance{ | |
| Addr: mustAbsComponentInstance("component.self"), | |
| PlanApplyable: true, | |
| PlanComplete: true, | |
| Action: plans.Read, | |
| Mode: plans.RefreshOnlyMode, | |
| PlannedInputValues: map[string]plans.DynamicValue{ | |
| "id": mustPlanDynamicValueDynamicType(cty.StringVal("old")), | |
| "input": mustPlanDynamicValueDynamicType(cty.StringVal("old")), | |
| }, | |
| PlannedInputValueMarks: map[string][]cty.PathValueMarks{ | |
| "id": nil, | |
| "input": nil, | |
| }, | |
| PlannedOutputValues: make(map[string]cty.Value), | |
| PlannedCheckResults: &states.CheckResults{}, | |
| PlanTimestamp: fakePlanTimestamp, | |
| }, | |
| &stackplan.PlannedChangeResourceInstancePlanned{ | |
| ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), | |
| PriorStateSrc: &states.ResourceInstanceObjectSrc{ | |
| Status: states.ObjectReady, | |
| AttrsJSON: mustMarshalJSONAttrs(map[string]any{ | |
| "id": "old", | |
| "value": "new", | |
| }), | |
| Dependencies: make([]addrs.ConfigResource, 0), | |
| }, | |
| ProviderConfigAddr: mustDefaultRootProvider("testing"), | |
| Schema: stacks_testing_provider.TestingResourceSchema, | |
| }, | |
| &stackplan.PlannedChangeHeader{ | |
| TerraformVersion: version.SemVer, | |
| }, | |
| &stackplan.PlannedChangePlannedTimestamp{ | |
| PlannedTimestamp: fakePlanTimestamp, | |
| }, | |
| }, | |
| }, | |
| }, | |
| "removed-stack": { | |
| path: filepath.Join("with-single-input", "removed-stack-instance-dynamic"), | |
| state: stackstate.NewStateBuilder(). | |
| AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.simple[\"old\"].component.self")). | |
| AddInputVariable("id", cty.StringVal("old")). | |
| AddInputVariable("input", cty.StringVal("old"))). | |
| AddResourceInstance(stackstate.NewResourceInstanceBuilder(). | |
| SetAddr(mustAbsResourceInstanceObject("stack.simple[\"old\"].component.self.testing_resource.data")). | |
| SetProviderAddr(mustDefaultRootProvider("testing")). | |
| SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ | |
| Status: states.ObjectReady, | |
| AttrsJSON: mustMarshalJSONAttrs(map[string]any{ | |
| "id": "old", | |
| "value": "old", | |
| }), | |
| })). | |
| Build(), | |
| store: stacks_testing_provider.NewResourceStoreBuilder(). | |
| AddResource("old", cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("old"), | |
| "value": cty.StringVal("new"), | |
| })). | |
| Build(), | |
| cycle: TestCycle{ | |
| planInputs: map[string]cty.Value{ | |
| "removed": cty.MapVal(map[string]cty.Value{ | |
| "old": cty.StringVal("old"), | |
| }), | |
| }, | |
| wantPlannedChanges: []stackplan.PlannedChange{ | |
| &stackplan.PlannedChangeApplyable{ | |
| Applyable: true, | |
| }, | |
| &stackplan.PlannedChangeHeader{ | |
| TerraformVersion: version.SemVer, | |
| }, | |
| &stackplan.PlannedChangePlannedTimestamp{ | |
| PlannedTimestamp: fakePlanTimestamp, | |
| }, | |
| &stackplan.PlannedChangeComponentInstance{ | |
| Addr: mustAbsComponentInstance("stack.simple[\"old\"].component.self"), | |
| PlanApplyable: true, | |
| PlanComplete: true, | |
| Action: plans.Read, | |
| Mode: plans.RefreshOnlyMode, | |
| PlannedInputValues: map[string]plans.DynamicValue{ | |
| "id": mustPlanDynamicValueDynamicType(cty.StringVal("old")), | |
| "input": mustPlanDynamicValueDynamicType(cty.StringVal("old")), | |
| }, | |
| PlannedInputValueMarks: map[string][]cty.PathValueMarks{ | |
| "id": nil, | |
| "input": nil, | |
| }, | |
| PlannedOutputValues: make(map[string]cty.Value), | |
| PlannedCheckResults: &states.CheckResults{}, | |
| PlanTimestamp: fakePlanTimestamp, | |
| }, | |
| &stackplan.PlannedChangeResourceInstancePlanned{ | |
| ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.simple[\"old\"].component.self.testing_resource.data"), | |
| PriorStateSrc: &states.ResourceInstanceObjectSrc{ | |
| Status: states.ObjectReady, | |
| AttrsJSON: mustMarshalJSONAttrs(map[string]any{ | |
| "id": "old", | |
| "value": "new", | |
| }), | |
| Dependencies: make([]addrs.ConfigResource, 0), | |
| }, | |
| ProviderConfigAddr: mustDefaultRootProvider("testing"), | |
| Schema: stacks_testing_provider.TestingResourceSchema, | |
| }, | |
| &stackplan.PlannedChangeRootInputValue{ | |
| Addr: stackaddrs.InputVariable{Name: "input"}, | |
| Action: plans.Create, | |
| Before: cty.NullVal(cty.DynamicPseudoType), | |
| After: cty.MapValEmpty(cty.String), | |
| }, | |
| &stackplan.PlannedChangeRootInputValue{ | |
| Addr: stackaddrs.InputVariable{Name: "removed"}, | |
| Action: plans.Create, | |
| Before: cty.NullVal(cty.DynamicPseudoType), | |
| After: cty.MapVal(map[string]cty.Value{ | |
| "old": cty.StringVal("old"), | |
| }), | |
| }, | |
| &stackplan.PlannedChangeRootInputValue{ | |
| Addr: stackaddrs.InputVariable{Name: "removed-direct"}, | |
| Action: plans.Create, | |
| Before: cty.NullVal(cty.DynamicPseudoType), | |
| After: cty.SetValEmpty(cty.String), | |
| }, | |
| }, | |
| }, | |
| }, | |
| } | |
| for name, tc := range tcs { | |
| t.Run(name, func(t *testing.T) { | |
| cycle := tc.cycle | |
| cycle.planMode = plans.RefreshOnlyMode // set this for all the tests here | |
| ctx := context.Background() | |
| lock := depsfile.NewLocks() | |
| lock.SetProvider( | |
| addrs.NewDefaultProvider("testing"), | |
| providerreqs.MustParseVersion("0.1.0"), | |
| providerreqs.MustParseVersionConstraints("0.1.0"), | |
| providerreqs.PreferredHashes([]providerreqs.Hash{}), | |
| ) | |
| store := tc.store | |
| if store == nil { | |
| store = stacks_testing_provider.NewResourceStore() | |
| } | |
| testContext := TestContext{ | |
| timestamp: &fakePlanTimestamp, | |
| config: loadMainBundleConfigForTest(t, tc.path), | |
| providers: map[addrs.Provider]providers.Factory{ | |
| addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { | |
| return stacks_testing_provider.NewProviderWithData(t, store), nil | |
| }, | |
| }, | |
| dependencyLocks: *lock, | |
| } | |
| testContext.Plan(t, ctx, tc.state, cycle) | |
| }) | |
| } | |
| } |