diff --git a/helper/resource/testing.go b/helper/resource/testing.go index 9e1961a46..4ec717d15 100644 --- a/helper/resource/testing.go +++ b/helper/resource/testing.go @@ -454,6 +454,14 @@ type ExternalProvider struct { Source string // the provider source } +type ImportStateKind byte + +const ( + ImportCommandWithId ImportStateKind = iota + ImportBlockWithId + ImportBlockWithResourceIdentity +) + // TestStep is a single apply sequence of a test, done within the // context of a state. // @@ -633,6 +641,12 @@ type TestStep struct { // ID of that resource. ImportState bool + ImportStateKind ImportStateKind // or ImportStateStrategy or ImportStateSubmode or ImportStateFlavor or ... + + // ImportStateBlockConfig, if non-empty, supplies declarative import + // configuration. This is (?mutually exclusive of ImportStateID + ResourceName?). + ImportStateBlockConfig string + // ImportStateId is the ID to perform an ImportState operation with. // This is optional. If it isn't set, then the resource ID is automatically // determined by inspecting the state for ResourceName's ID. diff --git a/helper/resource/testing_new.go b/helper/resource/testing_new.go index 0a7c7e7f7..183de0169 100644 --- a/helper/resource/testing_new.go +++ b/helper/resource/testing_new.go @@ -134,6 +134,7 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest // use this to track last step successfully applied // acts as default for import tests var appliedCfg teststep.Config + var appliedMergedCfg string var stepNumber int for stepIndex, step := range c.Steps { @@ -289,7 +290,7 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest if step.ImportState { logging.HelperResourceTrace(ctx, "TestStep is ImportState mode") - err := testStepNewImportState(ctx, t, helper, wd, step, appliedCfg, providers, stepIndex) + err := testStepNewImportState(ctx, t, helper, wd, step, appliedCfg, appliedMergedCfg, providers, stepIndex) if step.ExpectError != nil { logging.HelperResourceDebug(ctx, "Checking TestStep ExpectError") if err == nil { @@ -446,7 +447,8 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest }, }.Exec() - appliedCfg = teststep.Configuration(confRequest) + appliedCfg = teststep.Configuration(confRequest) // magical + appliedMergedCfg = mergedConfig logging.HelperResourceDebug(ctx, "Finished TestStep") diff --git a/helper/resource/testing_new_import_block_test.go b/helper/resource/testing_new_import_block_test.go new file mode 100644 index 000000000..350f0ab70 --- /dev/null +++ b/helper/resource/testing_new_import_block_test.go @@ -0,0 +1,115 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package resource + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestTest_TestStep_ImportBlockVerify(t *testing.T) { + t.Parallel() + + UnitTest(t, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + // import blocks are only available in v1.5.0 and later + tfversion.SkipBelow(tfversion.Version1_5_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_bucket": exampleCloudBucketResource(t), + }, + }), + }, + Steps: []TestStep{ + { + Config: ` + resource "examplecloud_bucket" "storage" { + bucket = "test-bucket" + description = "A bucket for testing." + }`, + }, + { + ImportState: true, + ImportStateKind: ImportBlockWithResourceIdentity, + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "bucket", // upgrade to resource identity + ResourceName: "examplecloud_bucket.storage", + }, + }, + }) +} + +func exampleCloudBucketResource(t *testing.T) testprovider.Resource { + t.Helper() + + return testprovider.Resource{ + CreateResponse: &resource.CreateResponse{ + NewResourceIdentityData: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "region": tftypes.String, + "bucket": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "region": tftypes.NewValue(tftypes.String, "test-region"), + "bucket": tftypes.NewValue(tftypes.String, "test-bucket"), + }, + ), + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "bucket": tftypes.String, + "description": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "bucket": tftypes.NewValue(tftypes.String, "test-bucket"), + "description": tftypes.NewValue(tftypes.String, "A bucket for testing."), + }, + ), + }, + ImportStateResponse: &resource.ImportStateResponse{ + State: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "bucket": tftypes.String, + "description": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "bucket": tftypes.NewValue(tftypes.String, "test-bucket"), + "description": tftypes.NewValue(tftypes.String, "A bucket for testing."), + }, + ), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "bucket", + Type: tftypes.String, + Required: true, + }, + { + Name: "description", + Type: tftypes.String, + Optional: true, + }, + }, + }, + }, + }, + } +} diff --git a/helper/resource/testing_new_import_state.go b/helper/resource/testing_new_import_state.go index 7dbc0b800..b382f4d6a 100644 --- a/helper/resource/testing_new_import_state.go +++ b/helper/resource/testing_new_import_state.go @@ -20,7 +20,7 @@ import ( "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" ) -func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest.Helper, wd *plugintest.WorkingDir, step TestStep, cfg teststep.Config, providers *providerFactories, stepIndex int) error { +func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest.Helper, wd *plugintest.WorkingDir, step TestStep, cfg teststep.Config, cfgHcl string, providers *providerFactories, stepIndex int) error { t.Helper() configRequest := teststep.PrepareConfigurationRequest{ @@ -31,9 +31,7 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest StepNumber: stepIndex + 1, TestName: t.Name(), }, - }.Exec() - - testStepConfig := teststep.Configuration(configRequest) + } if step.ResourceName == "" { t.Fatal("ResourceName is required for an import state test") @@ -93,13 +91,24 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest logging.HelperResourceTrace(ctx, fmt.Sprintf("Using import identifier: %s", importId)) - // Create working directory for import tests - if testStepConfig == nil { - logging.HelperResourceTrace(ctx, "Using prior TestStep Config for import") - - testStepConfig = cfg + var testStepConfig teststep.Config + if step.ImportStateKind == ImportBlockWithResourceIdentity { + cfgHcl += ` + import { + to = examplecloud_bucket.storage + id = "test-bucket" // to be replaced with identity + }` + configRequest.Raw = cfgHcl + testStepConfig = teststep.Configuration(configRequest.Exec()) + } else { + // Create working directory for import tests if testStepConfig == nil { - t.Fatal("Cannot import state with no specified config") + logging.HelperResourceTrace(ctx, "Using prior TestStep Config for import") + + testStepConfig = cfg + if testStepConfig == nil { + t.Fatal("Cannot import state with no specified config") + } } } @@ -118,22 +127,50 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest t.Fatalf("Error setting test config: %s", err) } - logging.HelperResourceDebug(ctx, "Running Terraform CLI init and import") + switch step.ImportStateKind { + case ImportCommandWithId: + logging.HelperResourceDebug(ctx, "Running Terraform CLI init and import") + + if !step.ImportStatePersist { + err = runProviderCommand(ctx, t, func() error { + return importWd.Init(ctx) + }, importWd, providers) + if err != nil { + t.Fatalf("Error running init: %s", err) + } + } - if !step.ImportStatePersist { err = runProviderCommand(ctx, t, func() error { - return importWd.Init(ctx) + return importWd.Import(ctx, step.ResourceName, importId) }, importWd, providers) if err != nil { - t.Fatalf("Error running init: %s", err) + return err } - } - - err = runProviderCommand(ctx, t, func() error { - return importWd.Import(ctx, step.ResourceName, importId) - }, importWd, providers) - if err != nil { - return err + case ImportBlockWithId: + t.Fatalf("not yet implemented") + case ImportBlockWithResourceIdentity: + if !step.ImportStatePersist { + err = runProviderCommand(ctx, t, func() error { + return importWd.Init(ctx) + }, importWd, providers) + if err != nil { + t.Fatalf("Error running init: %s", err) + } + } + err = runProviderCommand(ctx, t, func() error { + return importWd.CreatePlan(ctx) + }, importWd, providers) + if err != nil { + t.Fatalf("Error running plan: %s", err) + } + err = runProviderCommand(ctx, t, func() error { + return importWd.Apply(ctx) + }, importWd, providers) + if err != nil { + t.Fatalf("Error running apply: %s", err) + } + default: + t.Fatalf(`\o/`) } var importState *terraform.State diff --git a/internal/testing/testsdk/resource/resource.go b/internal/testing/testsdk/resource/resource.go index 5fea34468..aed372473 100644 --- a/internal/testing/testsdk/resource/resource.go +++ b/internal/testing/testsdk/resource/resource.go @@ -27,8 +27,9 @@ type CreateRequest struct { } type CreateResponse struct { - Diagnostics []*tfprotov6.Diagnostic - NewState tftypes.Value + Diagnostics []*tfprotov6.Diagnostic + NewState tftypes.Value + NewResourceIdentityData tftypes.Value } type DeleteRequest struct { @@ -70,6 +71,13 @@ type ReadResponse struct { NewState tftypes.Value } +type ResourceIdentitySchemaRequest struct{} + +type ResourceIdentitySchemaResponse struct { + Diagnostics []*tfprotov6.Diagnostic + ResourceIdentitySchema tftypes.Value // *tfprotov6.Schema +} + type SchemaRequest struct{} type SchemaResponse struct {