From c75a3db4cb20b5f19888d780eb63a5d9c4a03d31 Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Tue, 11 Feb 2025 10:31:47 -0500 Subject: [PATCH 1/6] What if ... ImportStateBlockConfig --- helper/resource/testing.go | 4 + .../resource/testing_new_import_block_test.go | 96 +++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 helper/resource/testing_new_import_block_test.go diff --git a/helper/resource/testing.go b/helper/resource/testing.go index 9e1961a46..9a9595d55 100644 --- a/helper/resource/testing.go +++ b/helper/resource/testing.go @@ -633,6 +633,10 @@ type TestStep struct { // ID of that resource. ImportState bool + // 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_import_block_test.go b/helper/resource/testing_new_import_block_test.go new file mode 100644 index 000000000..b25988df0 --- /dev/null +++ b/helper/resource/testing_new_import_block_test.go @@ -0,0 +1,96 @@ +// 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{ + tfversion.SkipBelow(tfversion.Version1_5_0), // import blocks are only available in v1.5.0 and later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_thing": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "other": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "resource-test"), + "other": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ImportStateResponse: &resource.ImportStateResponse{ + State: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "other": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "resource-test"), + "other": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + { + Name: "other", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + Steps: []TestStep{ + { + Config: `resource "examplecloud_thing" "test" {}`, + }, + { + ImportState: true, + ImportStateVerify: true, + ImportStateBlockConfig: `import { + to = examplecloud_thing.test + identity = { + hat = "derby" + cat = "garfield" + } + }`, + }, + }, + }) +} From 09bd9b5d1e03e72943470a830188db5d45a3a9ed Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Tue, 11 Feb 2025 10:36:16 -0500 Subject: [PATCH 2/6] fmt --- helper/resource/testing_new_import_block_test.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/helper/resource/testing_new_import_block_test.go b/helper/resource/testing_new_import_block_test.go index b25988df0..6eba8762c 100644 --- a/helper/resource/testing_new_import_block_test.go +++ b/helper/resource/testing_new_import_block_test.go @@ -83,12 +83,13 @@ func TestTest_TestStep_ImportBlockVerify(t *testing.T) { { ImportState: true, ImportStateVerify: true, - ImportStateBlockConfig: `import { - to = examplecloud_thing.test - identity = { - hat = "derby" - cat = "garfield" - } + ImportStateBlockConfig: ` + import { + to = examplecloud_thing.test + identity = { + hat = "derby" + cat = "garfield" + } }`, }, }, From 55b88aa3c4720e33283c2af515099e5d3b457569 Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Tue, 11 Feb 2025 10:36:16 -0500 Subject: [PATCH 3/6] fmt --- helper/resource/testing_new_import_block_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/helper/resource/testing_new_import_block_test.go b/helper/resource/testing_new_import_block_test.go index 6eba8762c..da8bc426b 100644 --- a/helper/resource/testing_new_import_block_test.go +++ b/helper/resource/testing_new_import_block_test.go @@ -87,10 +87,10 @@ func TestTest_TestStep_ImportBlockVerify(t *testing.T) { import { to = examplecloud_thing.test identity = { - hat = "derby" - cat = "garfield" - } - }`, + hat = "derby" + cat = "garfield" + } + }`, }, }, }) From daee136376d0b68e2c78c761fdf2ec5a9d29d722 Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Thu, 13 Feb 2025 00:56:08 -0500 Subject: [PATCH 4/6] how about this --- helper/resource/testing.go | 10 ++ .../resource/testing_new_import_block_test.go | 123 +++++++++--------- helper/resource/testing_new_import_state.go | 26 ++-- internal/testing/testsdk/resource/resource.go | 12 +- 4 files changed, 97 insertions(+), 74 deletions(-) diff --git a/helper/resource/testing.go b/helper/resource/testing.go index 9a9595d55..438b66d04 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 ( + TerraformImportCommand ImportStateKind = iota + ImportBlockWithId + ImportBlockWithResourceIdentity +) + // TestStep is a single apply sequence of a test, done within the // context of a state. // @@ -633,6 +641,8 @@ 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 diff --git a/helper/resource/testing_new_import_block_test.go b/helper/resource/testing_new_import_block_test.go index da8bc426b..69d59e440 100644 --- a/helper/resource/testing_new_import_block_test.go +++ b/helper/resource/testing_new_import_block_test.go @@ -20,78 +20,81 @@ func TestTest_TestStep_ImportBlockVerify(t *testing.T) { UnitTest(t, TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_5_0), // import blocks are only available in v1.5.0 and later + // 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_thing": { - CreateResponse: &resource.CreateResponse{ - NewState: tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - "other": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "resource-test"), - "other": tftypes.NewValue(tftypes.String, "testvalue"), - }, - ), - }, - ImportStateResponse: &resource.ImportStateResponse{ - State: tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - "other": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "resource-test"), - "other": tftypes.NewValue(tftypes.String, "testvalue"), - }, - ), - }, - SchemaResponse: &resource.SchemaResponse{ - Schema: &tfprotov6.Schema{ - Block: &tfprotov6.SchemaBlock{ - Attributes: []*tfprotov6.SchemaAttribute{ - { - Name: "id", - Type: tftypes.String, - Computed: true, - }, - { - Name: "other", - Type: tftypes.String, - Computed: true, - }, - }, - }, - }, - }, - }, + "examplecloud_bucket": exampleCloudBucketResource(t), }, }), }, Steps: []TestStep{ { - Config: `resource "examplecloud_thing" "test" {}`, + Config: ` + resource "examplecloud_bucket" "storage" { + bucket = "test-bucket" + description = "A bucket for testing." + }`, }, { - ImportState: true, - ImportStateVerify: true, - ImportStateBlockConfig: ` - import { - to = examplecloud_thing.test - identity = { - hat = "derby" - cat = "garfield" - } - }`, + ImportState: true, + ImportStateKind: ImportBlockWithResourceIdentity, + 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{}, + 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..a328b0e00 100644 --- a/helper/resource/testing_new_import_state.go +++ b/helper/resource/testing_new_import_state.go @@ -118,23 +118,25 @@ 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 TerraformImportCommand: + 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 - } var importState *terraform.State err = runProviderCommand(ctx, t, func() error { 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 { From ff949e2c050d502d8e62c5a39d06b3dfa3737bf1 Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Thu, 13 Feb 2025 13:34:20 -0500 Subject: [PATCH 5/6] stub the 2 new kinds --- helper/resource/testing.go | 2 +- helper/resource/testing_new_import_state.go | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/helper/resource/testing.go b/helper/resource/testing.go index 438b66d04..4ec717d15 100644 --- a/helper/resource/testing.go +++ b/helper/resource/testing.go @@ -457,7 +457,7 @@ type ExternalProvider struct { type ImportStateKind byte const ( - TerraformImportCommand ImportStateKind = iota + ImportCommandWithId ImportStateKind = iota ImportBlockWithId ImportBlockWithResourceIdentity ) diff --git a/helper/resource/testing_new_import_state.go b/helper/resource/testing_new_import_state.go index a328b0e00..7272fb203 100644 --- a/helper/resource/testing_new_import_state.go +++ b/helper/resource/testing_new_import_state.go @@ -119,7 +119,7 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest } switch step.ImportStateKind { - case TerraformImportCommand: + case ImportCommandWithId: logging.HelperResourceDebug(ctx, "Running Terraform CLI init and import") if !step.ImportStatePersist { @@ -137,6 +137,13 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest if err != nil { return err } + case ImportBlockWithId: + t.Fatalf("not yet implemented") + case ImportBlockWithResourceIdentity: + t.Fatalf("not yet implemented") + default: + t.Fatalf(`\o/`) + } var importState *terraform.State err = runProviderCommand(ctx, t, func() error { From bc4b46104244c0977d5cfbaf0a53a6b5d7cedf43 Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Thu, 13 Feb 2025 14:51:21 -0500 Subject: [PATCH 6/6] a passing ImportBlockWithResourceIdentity test that really uses an ID --- helper/resource/testing_new.go | 6 ++- .../resource/testing_new_import_block_test.go | 23 +++++++-- helper/resource/testing_new_import_state.go | 50 +++++++++++++++---- 3 files changed, 62 insertions(+), 17 deletions(-) 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 index 69d59e440..350f0ab70 100644 --- a/helper/resource/testing_new_import_block_test.go +++ b/helper/resource/testing_new_import_block_test.go @@ -39,9 +39,11 @@ func TestTest_TestStep_ImportBlockVerify(t *testing.T) { }`, }, { - ImportState: true, - ImportStateKind: ImportBlockWithResourceIdentity, - ResourceName: "examplecloud_bucket.storage", + ImportState: true, + ImportStateKind: ImportBlockWithResourceIdentity, + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "bucket", // upgrade to resource identity + ResourceName: "examplecloud_bucket.storage", }, }, }) @@ -77,7 +79,20 @@ func exampleCloudBucketResource(t *testing.T) testprovider.Resource { }, ), }, - ImportStateResponse: &resource.ImportStateResponse{}, + 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{ diff --git a/helper/resource/testing_new_import_state.go b/helper/resource/testing_new_import_state.go index 7272fb203..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") + } } } @@ -140,7 +149,26 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest case ImportBlockWithId: t.Fatalf("not yet implemented") case ImportBlockWithResourceIdentity: - t.Fatalf("not yet implemented") + 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/`) }