diff --git a/helper/resource/importstate/examplecloud_test.go b/helper/resource/importstate/examplecloud_test.go index c57012cf..bf1e2747 100644 --- a/helper/resource/importstate/examplecloud_test.go +++ b/helper/resource/importstate/examplecloud_test.go @@ -596,3 +596,27 @@ func examplecloudResourceWithNullIdentityAttr() testprovider.Resource { }, } } + +// This example resource, on update plans, will plan a different identity to test that +// our testing framework assertions catch an identity that differs after import/refresh. +func examplecloudResourceWithChangingIdentity() testprovider.Resource { + exampleCloudResource := examplecloudResource() + + exampleCloudResource.PlanChangeFunc = func(ctx context.Context, req resource.PlanChangeRequest, resp *resource.PlanChangeResponse) { + // Only on update + if !req.PriorState.IsNull() && !req.ProposedNewState.IsNull() { + resp.PlannedIdentity = teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "easteurope/someothervalue"), + }, + )) + } + } + + return exampleCloudResource +} diff --git a/helper/resource/importstate/import_block_with_resource_identity_test.go b/helper/resource/importstate/import_block_with_resource_identity_test.go index ed4afb04..c71a8b57 100644 --- a/helper/resource/importstate/import_block_with_resource_identity_test.go +++ b/helper/resource/importstate/import_block_with_resource_identity_test.go @@ -119,6 +119,39 @@ func TestImportBlock_WithResourceIdentity_WithEveryType(t *testing.T) { }) } +func TestImportBlock_WithResourceIdentity_ChangingIdentityError(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), // ImportBlockWithResourceIdentity requires Terraform 1.12.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": examplecloudResourceWithChangingIdentity(), + }, + }), + }, + Steps: []r.TestStep{ + { + Config: ` + resource "examplecloud_container" "test" { + location = "westeurope" + name = "somevalue" + }`, + }, + { + ResourceName: "examplecloud_container.test", + ImportState: true, + ImportStateKind: r.ImportBlockWithResourceIdentity, + // The plan following the import will produce a different identity value then test step 1 + ExpectError: regexp.MustCompile(`expected identity values map\[id:westeurope/somevalue\], got map\[id:easteurope/someothervalue\]`), + }, + }, + }) +} + func TestImportBlock_WithResourceIdentity_RequiresVersion1_12_0(t *testing.T) { t.Parallel() diff --git a/helper/resource/plugin.go b/helper/resource/plugin.go index 5b50306a..6e16e161 100644 --- a/helper/resource/plugin.go +++ b/helper/resource/plugin.go @@ -114,15 +114,6 @@ type providerFactories struct { protov6 protov6ProviderFactories } -func runProviderCommandApply(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, factories *providerFactories) error { - t.Helper() - - fn := func() error { - return wd.Apply(ctx) - } - return runProviderCommand(ctx, t, wd, factories, fn) -} - func runProviderCommandCreatePlan(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, factories *providerFactories) error { t.Helper() @@ -132,23 +123,6 @@ func runProviderCommandCreatePlan(ctx context.Context, t testing.T, wd *pluginte return runProviderCommand(ctx, t, wd, factories, fn) } -func runProviderCommandGetStateJSON(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, factories *providerFactories) (*tfjson.State, error) { - t.Helper() - - var stateJSON *tfjson.State - fn := func() error { - var err error - stateJSON, err = wd.State(ctx) - return err - } - err := runProviderCommand(ctx, t, wd, factories, fn) - if err != nil { - return nil, err - } - - return stateJSON, nil -} - func runProviderCommandSavedPlan(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, factories *providerFactories) (*tfjson.Plan, error) { t.Helper() diff --git a/helper/resource/testing_new_import_state.go b/helper/resource/testing_new_import_state.go index 9a5c0fb0..f2e265d5 100644 --- a/helper/resource/testing_new_import_state.go +++ b/helper/resource/testing_new_import_state.go @@ -99,7 +99,7 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest var priorIdentityValues map[string]any if kind.plannable() && kind.resourceIdentity() { - priorIdentityValues = identityValuesFromState(stateJSON, resourceName) + priorIdentityValues = identityValuesFromStateValues(stateJSON.Values, resourceName) if len(priorIdentityValues) == 0 { return fmt.Errorf("importing resource %s: expected prior state to have resource identity values, got none", resourceName) } @@ -231,32 +231,15 @@ func testImportBlock(ctx context.Context, t testing.T, workingDir *plugintest.Wo } if kind.resourceIdentity() { - if err := verifyIdentityValues(ctx, t, workingDir, providers, resourceName, priorIdentityValues); err != nil { - return err + newIdentityValues := identityValuesFromStateValues(plan.PlannedValues, resourceName) + if !cmp.Equal(priorIdentityValues, newIdentityValues) { + return fmt.Errorf("importing resource %s: expected identity values %v, got %v", resourceName, priorIdentityValues, newIdentityValues) } } return nil } -func verifyIdentityValues(ctx context.Context, t testing.T, workingDir *plugintest.WorkingDir, providers *providerFactories, resourceName string, priorIdentityValues map[string]any) error { - err := runProviderCommandApply(ctx, t, workingDir, providers) - if err != nil { - return fmt.Errorf("applying plan with import config: %s", err) - } - - newStateJSON, err := runProviderCommandGetStateJSON(ctx, t, workingDir, providers) - if err != nil { - return fmt.Errorf("getting state after applying plan with import config: %s", err) - } - - newIdentityValues := identityValuesFromState(newStateJSON, resourceName) - if !cmp.Equal(priorIdentityValues, newIdentityValues) { - return fmt.Errorf("importing resource %s: expected identity values %v, got %v", resourceName, priorIdentityValues, newIdentityValues) - } - return nil -} - func testImportCommand(ctx context.Context, t testing.T, workingDir *plugintest.WorkingDir, providers *providerFactories, resourceName string, importId string, step TestStep, state *terraform.State) error { err := runProviderCommand(ctx, t, workingDir, providers, func() error { return workingDir.Import(ctx, resourceName, importId) @@ -515,8 +498,7 @@ func importStatePreconditions(t testing.T, helper *plugintest.Helper, step TestS return nil } -func resourcesFromState(state *tfjson.State) []*tfjson.StateResource { - stateValues := state.Values +func resourcesFromState(stateValues *tfjson.StateValues) []*tfjson.StateResource { if stateValues == nil || stateValues.RootModule == nil { return []*tfjson.StateResource{} } @@ -524,9 +506,9 @@ func resourcesFromState(state *tfjson.State) []*tfjson.StateResource { return stateValues.RootModule.Resources } -func identityValuesFromState(state *tfjson.State, resourceName string) map[string]any { +func identityValuesFromStateValues(stateValues *tfjson.StateValues, resourceName string) map[string]any { var resource *tfjson.StateResource - resources := resourcesFromState(state) + resources := resourcesFromState(stateValues) for _, r := range resources { if r.Address == resourceName {