From ccc33e82f1aa65624e724cb630b2c607f7d58bf3 Mon Sep 17 00:00:00 2001 From: Steph Date: Fri, 19 Sep 2025 11:04:30 +0200 Subject: [PATCH 1/9] update examplecloud resource/list resource and add test files for querychecks --- .../resource/query/examplecloud_list_test.go | 182 ++++++++++++++++-- helper/resource/query/examplecloud_test.go | 78 +++++--- helper/resource/query/query_test.go | 136 +++++++------ helper/resource/query/types_test.go | 8 + querycheck/{contains.go => contains_name.go} | 0 querycheck/contains_name_test.go | 121 ++++++++++++ querycheck/expect_identity_test.go | 63 ++++++ querycheck/expect_known_value_test.go | 36 ++++ .../expect_result_length_atleast_test.go | 63 ++++++ querycheck/expect_result_length_exact_test.go | 63 ++++++ 10 files changed, 634 insertions(+), 116 deletions(-) rename querycheck/{contains.go => contains_name.go} (100%) create mode 100644 querycheck/contains_name_test.go create mode 100644 querycheck/expect_identity_test.go create mode 100644 querycheck/expect_known_value_test.go create mode 100644 querycheck/expect_result_length_atleast_test.go create mode 100644 querycheck/expect_result_length_exact_test.go diff --git a/helper/resource/query/examplecloud_list_test.go b/helper/resource/query/examplecloud_list_test.go index 7b18943e..ca3972f2 100644 --- a/helper/resource/query/examplecloud_list_test.go +++ b/helper/resource/query/examplecloud_list_test.go @@ -19,9 +19,9 @@ func examplecloudListResource() testprovider.ListResource { Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ { - Name: "id", + Name: "resource_group_name", Type: tftypes.String, - Computed: true, + Required: true, }, }, }, @@ -33,57 +33,199 @@ func examplecloudListResource() testprovider.ListResource { Resource: teststep.Pointer(tftypes.NewValue( tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - "location": tftypes.String, - "name": tftypes.String, + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + "resource_group_name": tftypes.String, + "instances": tftypes.Number, }, }, map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), - "location": tftypes.NewValue(tftypes.String, "westeurope"), - "name": tftypes.NewValue(tftypes.String, "somevalue"), + "id": tftypes.NewValue(tftypes.String, "foo/banane"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "banane"), + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "instances": tftypes.NewValue(tftypes.Number, 5), }, )), Identity: teststep.Pointer(tftypes.NewValue( tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - "location": tftypes.String, + "resource_group_name": tftypes.String, + "name": tftypes.String, }, }, map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue1"), - "location": tftypes.NewValue(tftypes.String, "westeurope"), + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "name": tftypes.NewValue(tftypes.String, "banane"), }, )), + DisplayName: "banane", }) push(list.ListResult{ + Resource: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + "resource_group_name": tftypes.String, + "instances": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "foo/ananas"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "ananas"), + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "instances": tftypes.NewValue(tftypes.Number, 9000), + }, + )), + Identity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "resource_group_name": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "name": tftypes.NewValue(tftypes.String, "ananas"), + }, + )), + DisplayName: "ananas", + }) + push(list.ListResult{ + Resource: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + "resource_group_name": tftypes.String, + "instances": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "foo/kiwi"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "kiwi"), + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "instances": tftypes.NewValue(tftypes.Number, 88), + }, + )), + Identity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "resource_group_name": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "name": tftypes.NewValue(tftypes.String, "kiwi"), + }, + )), + DisplayName: "kiwi", + }) + push(list.ListResult{ + Resource: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + "resource_group_name": tftypes.String, + "instances": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "bar/papaya"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "banane"), + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "instances": tftypes.NewValue(tftypes.Number, 3), + }, + )), Identity: teststep.Pointer(tftypes.NewValue( tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - "location": tftypes.String, + "resource_group_name": tftypes.String, + "name": tftypes.String, }, }, map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue2"), - "location": tftypes.NewValue(tftypes.String, "westeurope2"), + "resource_group_name": tftypes.NewValue(tftypes.String, "bar"), + "name": tftypes.NewValue(tftypes.String, "papaya"), }, )), + DisplayName: "papaya", }) push(list.ListResult{ + Resource: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + "resource_group_name": tftypes.String, + "instances": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "bar/birne"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "birne"), + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "instances": tftypes.NewValue(tftypes.Number, 8564), + }, + )), + Identity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "resource_group_name": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "resource_group_name": tftypes.NewValue(tftypes.String, "bar"), + "name": tftypes.NewValue(tftypes.String, "birne"), + }, + )), + DisplayName: "birne", + }) + push(list.ListResult{ + Resource: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + "resource_group_name": tftypes.String, + "instances": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "bar/kirsche"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "kirsche"), + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "instances": tftypes.NewValue(tftypes.Number, 500), + }, + )), Identity: teststep.Pointer(tftypes.NewValue( tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - "location": tftypes.String, + "resource_group_name": tftypes.String, + "name": tftypes.String, }, }, map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue3"), - "location": tftypes.NewValue(tftypes.String, "westeurope3"), + "resource_group_name": tftypes.NewValue(tftypes.String, "bar"), + "name": tftypes.NewValue(tftypes.String, "kirsche"), }, )), + DisplayName: "kirsche", }) }, }, diff --git a/helper/resource/query/examplecloud_test.go b/helper/resource/query/examplecloud_test.go index 3ad86c16..0376455a 100644 --- a/helper/resource/query/examplecloud_test.go +++ b/helper/resource/query/examplecloud_test.go @@ -17,27 +17,31 @@ func examplecloudResource() testprovider.Resource { NewState: tftypes.NewValue( tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - "location": tftypes.String, - "name": tftypes.String, + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + "resource_group_name": tftypes.String, + "instances": tftypes.Number, }, }, map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), - "location": tftypes.NewValue(tftypes.String, "westeurope"), - "name": tftypes.NewValue(tftypes.String, "somevalue"), + "id": tftypes.NewValue(tftypes.String, "foo/banana"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "banana"), + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "instances": tftypes.NewValue(tftypes.Number, int64(5)), }, ), NewIdentity: teststep.Pointer(tftypes.NewValue( tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - "location": tftypes.String, + "resource_group_name": tftypes.String, + "name": tftypes.String, }, }, map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), - "location": tftypes.NewValue(tftypes.String, "somelocation"), + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "name": tftypes.NewValue(tftypes.String, "banana"), }, )), }, @@ -45,27 +49,31 @@ func examplecloudResource() testprovider.Resource { NewState: tftypes.NewValue( tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - "location": tftypes.String, - "name": tftypes.String, + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + "resource_group_name": tftypes.String, + "instances": tftypes.Number, }, }, map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), - "location": tftypes.NewValue(tftypes.String, "westeurope"), - "name": tftypes.NewValue(tftypes.String, "somevalue"), + "id": tftypes.NewValue(tftypes.String, "foo/banana"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "banana"), + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "instances": tftypes.NewValue(tftypes.Number, int64(5)), }, ), NewIdentity: teststep.Pointer(tftypes.NewValue( tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - "location": tftypes.String, + "resource_group_name": tftypes.String, + "name": tftypes.String, }, }, map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), - "location": tftypes.NewValue(tftypes.String, "westeurope"), + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "name": tftypes.NewValue(tftypes.String, "banana"), }, )), }, @@ -73,27 +81,31 @@ func examplecloudResource() testprovider.Resource { State: tftypes.NewValue( tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - "location": tftypes.String, - "name": tftypes.String, + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + "resource_group_name": tftypes.String, + "instances": tftypes.Number, }, }, map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), - "location": tftypes.NewValue(tftypes.String, "westeurope"), - "name": tftypes.NewValue(tftypes.String, "somevalue"), + "id": tftypes.NewValue(tftypes.String, "foo/banana"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "banana"), + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "instances": tftypes.NewValue(tftypes.Number, int64(5)), }, ), Identity: teststep.Pointer(tftypes.NewValue( tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - "location": tftypes.String, + "resource_group_name": tftypes.String, + "name": tftypes.String, }, }, map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), - "location": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "name": tftypes.NewValue(tftypes.String, "banana"), }, )), }, @@ -104,6 +116,8 @@ func examplecloudResource() testprovider.Resource { ComputedStringAttribute("id"), RequiredStringAttribute("location"), RequiredStringAttribute("name"), + RequiredStringAttribute("resource_group_name"), + OptionalNumberAttribute("instances"), }, }, }, @@ -113,12 +127,12 @@ func examplecloudResource() testprovider.Resource { Version: 1, IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ { - Name: "id", + Name: "resource_group_name", Type: tftypes.String, RequiredForImport: true, }, { - Name: "location", + Name: "name", Type: tftypes.String, RequiredForImport: true, }, diff --git a/helper/resource/query/query_test.go b/helper/resource/query/query_test.go index 1fa76381..35549fd4 100644 --- a/helper/resource/query/query_test.go +++ b/helper/resource/query/query_test.go @@ -34,11 +34,15 @@ func TestQuery(t *testing.T) { }, Steps: []r.TestStep{ { // config mode step 1 needs tf file with terraform providers block + // this step should provision all the resources that the query is support to list + // for simplicity we're only "provisioning" one here Config: ` resource "examplecloud_containerette" "primary" { - id = "westeurope/somevalue" - location = "westeurope" - name = "somevalue" + name = "banana" + resource_group_name = "foo" + location = "westeurope" + + instances = 5 }`, }, { // Query mode step 2, operates on .tfquery.hcl files (needs tf file with terraform providers block) @@ -49,96 +53,100 @@ func TestQuery(t *testing.T) { Query: true, Config: ` provider "examplecloud" {} + list "examplecloud_containerette" "test" { provider = examplecloud config { - id = "westeurope/somevalue" + resource_group_name = "foo" } } + list "examplecloud_containerette" "test2" { provider = examplecloud config { - id = "foo" + resource_group_name = "bar" } } `, QueryResultChecks: []querycheck.QueryResultCheck{ querycheck.ExpectIdentity("examplecloud_containerette.test", map[string]knownvalue.Check{ - "id": knownvalue.StringExact("westeurope/somevalue1"), - "location": knownvalue.StringExact("westeurope"), + "name": knownvalue.StringExact("banane"), + "resource_group_name": knownvalue.StringExact("foo"), }), querycheck.ExpectIdentity("examplecloud_containerette.test", map[string]knownvalue.Check{ - "id": knownvalue.StringExact("westeurope/somevalue2"), - "location": knownvalue.StringExact("westeurope2"), + "name": knownvalue.StringExact("ananas"), + "resource_group_name": knownvalue.StringExact("foo"), }), querycheck.ExpectIdentity("examplecloud_containerette.test", map[string]knownvalue.Check{ - "id": knownvalue.StringExact("westeurope/somevalue3"), - "location": knownvalue.StringExact("westeurope3"), + "name": knownvalue.StringExact("kiwi"), + "resource_group_name": knownvalue.StringExact("foo"), }), querycheck.ExpectIdentity("examplecloud_containerette.test2", map[string]knownvalue.Check{ - "id": knownvalue.StringExact("westeurope/somevalue1"), - "location": knownvalue.StringExact("westeurope"), + "name": knownvalue.StringExact("papaya"), + "resource_group_name": knownvalue.StringExact("bar"), }), querycheck.ExpectIdentity("examplecloud_containerette.test2", map[string]knownvalue.Check{ - "id": knownvalue.StringExact("westeurope/somevalue2"), - "location": knownvalue.StringExact("westeurope2"), + "name": knownvalue.StringExact("birne"), + "resource_group_name": knownvalue.StringExact("bar"), }), querycheck.ExpectIdentity("examplecloud_containerette.test2", map[string]knownvalue.Check{ - "id": knownvalue.StringExact("westeurope/somevalue3"), - "location": knownvalue.StringExact("westeurope3"), + "name": knownvalue.StringExact("kirsche"), + "resource_group_name": knownvalue.StringExact("bar"), }), }, }, - { - Query: true, - Config: ` - provider "examplecloud" {} - list "examplecloud_containerette" "test" { - provider = examplecloud - - config { - id = "westeurope/somevalue" - } - } - list "examplecloud_containerette" "test2" { - provider = examplecloud - config { - id = "foo" - } - } - `, - QueryResultChecks: []querycheck.QueryResultCheck{ - querycheck.ExpectLength("examplecloud_containerette.test", 3), - querycheck.ExpectLength("examplecloud_containerette.test2", 3), - }, - }, - { - Query: true, - Config: ` - provider "examplecloud" {} - list "examplecloud_containerette" "test" { - provider = examplecloud - - config { - id = "westeurope/somevalue" - } - } - list "examplecloud_containerette" "test2" { - provider = examplecloud - - config { - id = "foo" - } - } - `, - QueryResultChecks: []querycheck.QueryResultCheck{ - querycheck.ExpectLengthAtLeast("examplecloud_containerette.test", 2), - querycheck.ExpectLengthAtLeast("examplecloud_containerette.test2", 1), - }, - }, + // Commented out since these will fail now + //{ + // Query: true, + // Config: ` + // provider "examplecloud" {} + // list "examplecloud_containerette" "test" { + // provider = examplecloud + // + // config { + // id = "westeurope/somevalue" + // } + // } + // list "examplecloud_containerette" "test2" { + // provider = examplecloud + // + // config { + // id = "foo" + // } + // } + // `, + // QueryResultChecks: []querycheck.QueryResultCheck{ + // querycheck.ExpectLength("examplecloud_containerette.test", 3), + // querycheck.ExpectLength("examplecloud_containerette.test2", 3), + // }, + //}, + //{ + // Query: true, + // Config: ` + // provider "examplecloud" {} + // list "examplecloud_containerette" "test" { + // provider = examplecloud + // + // config { + // id = "westeurope/somevalue" + // } + // } + // list "examplecloud_containerette" "test2" { + // provider = examplecloud + // + // config { + // id = "foo" + // } + // } + // `, + // QueryResultChecks: []querycheck.QueryResultCheck{ + // querycheck.ExpectLengthAtLeast("examplecloud_containerette.test", 2), + // querycheck.ExpectLengthAtLeast("examplecloud_containerette.test2", 1), + // }, + //}, }, }) } diff --git a/helper/resource/query/types_test.go b/helper/resource/query/types_test.go index 7620d4d7..a4612db3 100644 --- a/helper/resource/query/types_test.go +++ b/helper/resource/query/types_test.go @@ -41,6 +41,14 @@ func RequiredNumberAttribute(name string) *tfprotov6.SchemaAttribute { } } +func OptionalNumberAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.Number, + Optional: true, + } +} + func ComputedStringAttribute(name string) *tfprotov6.SchemaAttribute { return &tfprotov6.SchemaAttribute{ Name: name, diff --git a/querycheck/contains.go b/querycheck/contains_name.go similarity index 100% rename from querycheck/contains.go rename to querycheck/contains_name.go diff --git a/querycheck/contains_name_test.go b/querycheck/contains_name_test.go new file mode 100644 index 00000000..6b9cdb73 --- /dev/null +++ b/querycheck/contains_name_test.go @@ -0,0 +1,121 @@ +package querycheck_test + +import ( + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "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/querycheck" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + "regexp" + "testing" +) + +func TestContainsResourceWithName(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + ListResources: map[string]testprovider.ListResource{ + + // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use + + //"examplecloud_containerette": examplecloudListResource(), + }, + Resources: map[string]testprovider.Resource{ + //"examplecloud_containerette": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + // We'll skip the first test step where we simulate creating the resource that will be returned when we query for it for simplicity. + { + Query: true, + Config: ` + provider "examplecloud" {} + + list "examplecloud_containerette" "test" { + provider = examplecloud + + config { + resource_group_name = "foo" + } + } + + list "examplecloud_containerette" "test2" { + provider = examplecloud + + config { + resource_group_name = "bar" + } + } + `, + QueryResultChecks: []querycheck.QueryResultCheck{ + querycheck.ContainsResourceWithName("examplecloud_containerette.test", "banane"), + querycheck.ContainsResourceWithName("examplecloud_containerette.test", "ananas"), + querycheck.ContainsResourceWithName("examplecloud_containerette.test", "kiwi"), + querycheck.ContainsResourceWithName("examplecloud_containerette.test2", "papaya"), + querycheck.ContainsResourceWithName("examplecloud_containerette.test2", "birne"), + querycheck.ContainsResourceWithName("examplecloud_containerette.test2", "kirsche"), + }, + }, + }, + }) +} + +// Let's add a test case that checks the failure scenario when a resource of a given name is not found. +func TestContainsResourceWithName_NotFound(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + ListResources: map[string]testprovider.ListResource{ + + // TODO: define a resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use + + //"examplecloud_containerette": examplecloudListResource(), + }, + Resources: map[string]testprovider.Resource{ + //"examplecloud_containerette": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + { + Query: true, + Config: ` + provider "examplecloud" {} + + list "examplecloud_containerette" "test" { + provider = examplecloud + + config { + resource_group_name = "foo" + } + } + + list "examplecloud_containerette" "test2" { + provider = examplecloud + + config { + resource_group_name = "bar" + } + } + `, + QueryResultChecks: []querycheck.QueryResultCheck{ + querycheck.ContainsResourceWithName("examplecloud_containerette.test", "pflaume"), + }, + // TODO update expected error message to match what we output + ExpectError: regexp.MustCompile("examplecloud_containerette.test - there are no pflaumen here!"), + }, + }, + }) +} diff --git a/querycheck/expect_identity_test.go b/querycheck/expect_identity_test.go new file mode 100644 index 00000000..d296949a --- /dev/null +++ b/querycheck/expect_identity_test.go @@ -0,0 +1,63 @@ +package querycheck + +import ( + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "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/tfversion" + "testing" +) + +func TestExpectIdentity(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + ListResources: map[string]testprovider.ListResource{ + + // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use + + //"examplecloud_containerette": examplecloudListResource(), + }, + Resources: map[string]testprovider.Resource{ + //"examplecloud_containerette": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + // TODO + }, + }) +} + +// Let's add a test case that checks the failure scenario when an identity is not found. +func TestExpectIdentity_NotFound(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + ListResources: map[string]testprovider.ListResource{ + + // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use + + //"examplecloud_containerette": examplecloudListResource(), + }, + Resources: map[string]testprovider.Resource{ + //"examplecloud_containerette": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + // TODO + }, + }) +} diff --git a/querycheck/expect_known_value_test.go b/querycheck/expect_known_value_test.go new file mode 100644 index 00000000..19d18dea --- /dev/null +++ b/querycheck/expect_known_value_test.go @@ -0,0 +1,36 @@ +package querycheck + +import ( + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "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/tfversion" + "testing" +) + +func TestExpectKnownValue(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + ListResources: map[string]testprovider.ListResource{ + + // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use + + //"examplecloud_containerette": examplecloudListResource(), + }, + Resources: map[string]testprovider.Resource{ + //"examplecloud_containerette": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + // TODO + }, + }) +} diff --git a/querycheck/expect_result_length_atleast_test.go b/querycheck/expect_result_length_atleast_test.go new file mode 100644 index 00000000..afb351bb --- /dev/null +++ b/querycheck/expect_result_length_atleast_test.go @@ -0,0 +1,63 @@ +package querycheck + +import ( + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "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/tfversion" + "testing" +) + +func TestResultLengthAtLeast(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + ListResources: map[string]testprovider.ListResource{ + + // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use + + //"examplecloud_containerette": examplecloudListResource(), + }, + Resources: map[string]testprovider.Resource{ + //"examplecloud_containerette": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + // TODO + }, + }) +} + +// Let's add a test case that checks the failure scenario when there are too few results. +func TestResultLengthAtLeast_TooFewResults(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + ListResources: map[string]testprovider.ListResource{ + + // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use + + //"examplecloud_containerette": examplecloudListResource(), + }, + Resources: map[string]testprovider.Resource{ + //"examplecloud_containerette": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + // TODO + }, + }) +} diff --git a/querycheck/expect_result_length_exact_test.go b/querycheck/expect_result_length_exact_test.go new file mode 100644 index 00000000..e3346838 --- /dev/null +++ b/querycheck/expect_result_length_exact_test.go @@ -0,0 +1,63 @@ +package querycheck + +import ( + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "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/tfversion" + "testing" +) + +func TestResultLengthExact(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + ListResources: map[string]testprovider.ListResource{ + + // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use + + //"examplecloud_containerette": examplecloudListResource(), + }, + Resources: map[string]testprovider.Resource{ + //"examplecloud_containerette": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + // TODO + }, + }) +} + +// Let's add a test case that checks the failure scenario when there are the wrong amount of results. +func TestResultLengthExact_WrongAmount(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + ListResources: map[string]testprovider.ListResource{ + + // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use + + //"examplecloud_containerette": examplecloudListResource(), + }, + Resources: map[string]testprovider.Resource{ + //"examplecloud_containerette": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + // TODO + }, + }) +} From 45b23affc357258a257e4b3360bffaeacea97110 Mon Sep 17 00:00:00 2001 From: Steph Date: Fri, 19 Sep 2025 11:06:25 +0200 Subject: [PATCH 2/9] forgot one, oops --- querycheck/expect_known_value_test.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/querycheck/expect_known_value_test.go b/querycheck/expect_known_value_test.go index 19d18dea..d0121e39 100644 --- a/querycheck/expect_known_value_test.go +++ b/querycheck/expect_known_value_test.go @@ -34,3 +34,30 @@ func TestExpectKnownValue(t *testing.T) { }, }) } + +// Let's add a test case that checks the failure scenario when the value is incorrect. +func TestExpectKnownValue_ValueIncorrect(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + ListResources: map[string]testprovider.ListResource{ + + // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use + + //"examplecloud_containerette": examplecloudListResource(), + }, + Resources: map[string]testprovider.Resource{ + //"examplecloud_containerette": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + // TODO + }, + }) +} From ccc4965d1ad46a9d41e7585019f3acec4a4e57d8 Mon Sep 17 00:00:00 2001 From: Rain Date: Thu, 25 Sep 2025 14:05:22 -0400 Subject: [PATCH 3/9] Saving my place --- querycheck/additional_cli_options.go | 29 + querycheck/contains_name_test.go | 49 +- querycheck/environment_variables.go | 45 + querycheck/examplecloud_list_test.go | 233 ++ querycheck/examplecloud_test.go | 143 ++ querycheck/expect_identity_test.go | 24 +- querycheck/expect_known_value_test.go | 24 +- .../expect_result_length_atleast_test.go | 24 +- querycheck/expect_result_length_exact_test.go | 24 +- querycheck/plan_checks.go | 28 + querycheck/plugin.go | 528 ++++ querycheck/query_checks.go | 50 + querycheck/state_checks.go | 29 + querycheck/state_shim.go | 344 +++ querycheck/testcase_providers.go | 61 + querycheck/testcase_test.go | 122 + querycheck/testcase_validate.go | 113 + querycheck/testing.go | 2161 +++++++++++++++++ querycheck/testing_config.go | 29 + querycheck/testing_new.go | 701 ++++++ querycheck/testing_new_config.go | 469 ++++ querycheck/testing_new_import_state.go | 571 +++++ querycheck/testing_new_refresh_state.go | 105 + querycheck/teststep_providers.go | 258 ++ querycheck/teststep_validate.go | 245 ++ querycheck/tfversion_checks.go | 31 + querycheck/types_test.go | 74 + 27 files changed, 6431 insertions(+), 83 deletions(-) create mode 100644 querycheck/additional_cli_options.go create mode 100644 querycheck/environment_variables.go create mode 100644 querycheck/examplecloud_list_test.go create mode 100644 querycheck/examplecloud_test.go create mode 100644 querycheck/plan_checks.go create mode 100644 querycheck/plugin.go create mode 100644 querycheck/query_checks.go create mode 100644 querycheck/state_checks.go create mode 100644 querycheck/state_shim.go create mode 100644 querycheck/testcase_providers.go create mode 100644 querycheck/testcase_test.go create mode 100644 querycheck/testcase_validate.go create mode 100644 querycheck/testing.go create mode 100644 querycheck/testing_config.go create mode 100644 querycheck/testing_new.go create mode 100644 querycheck/testing_new_config.go create mode 100644 querycheck/testing_new_import_state.go create mode 100644 querycheck/testing_new_refresh_state.go create mode 100644 querycheck/teststep_providers.go create mode 100644 querycheck/teststep_validate.go create mode 100644 querycheck/tfversion_checks.go create mode 100644 querycheck/types_test.go diff --git a/querycheck/additional_cli_options.go b/querycheck/additional_cli_options.go new file mode 100644 index 00000000..e1b9b5bf --- /dev/null +++ b/querycheck/additional_cli_options.go @@ -0,0 +1,29 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +// AdditionalCLIOptions allows an intentionally limited set of options to be passed +// to the Terraform CLI when executing test steps. +type AdditionalCLIOptions struct { + // Apply represents options to be passed to the `terraform apply` command. + Apply ApplyOptions + + // Plan represents options to be passed to the `terraform plan` command. + Plan PlanOptions +} + +// ApplyOptions represents options to be passed to the `terraform apply` command. +type ApplyOptions struct { + // AllowDeferral will pass the experimental `-allow-deferral` flag to the apply command. + AllowDeferral bool +} + +// PlanOptions represents options to be passed to the `terraform plan` command. +type PlanOptions struct { + // AllowDeferral will pass the experimental `-allow-deferral` flag to the plan command. + AllowDeferral bool + + // NoRefresh will pass the `-refresh=false` flag to the plan command. + NoRefresh bool +} diff --git a/querycheck/contains_name_test.go b/querycheck/contains_name_test.go index 6b9cdb73..1c40873e 100644 --- a/querycheck/contains_name_test.go +++ b/querycheck/contains_name_test.go @@ -1,37 +1,34 @@ -package querycheck_test +package querycheck import ( + "regexp" + "testing" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" - r "github.com/hashicorp/terraform-plugin-testing/helper/resource" "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/querycheck" "github.com/hashicorp/terraform-plugin-testing/tfversion" - "regexp" - "testing" ) func TestContainsResourceWithName(t *testing.T) { t.Parallel() - r.UnitTest(t, r.TestCase{ + UnitTest(t, TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ ListResources: map[string]testprovider.ListResource{ - // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use - - //"examplecloud_containerette": examplecloudListResource(), + "examplecloud_containerette": examplecloudListResource(), }, Resources: map[string]testprovider.Resource{ - //"examplecloud_containerette": examplecloudResource(), + "examplecloud_containerette": examplecloudResource(), }, }), }, - Steps: []r.TestStep{ + Steps: []TestStep{ // We'll skip the first test step where we simulate creating the resource that will be returned when we query for it for simplicity. { Query: true, @@ -54,13 +51,13 @@ func TestContainsResourceWithName(t *testing.T) { } } `, - QueryResultChecks: []querycheck.QueryResultCheck{ - querycheck.ContainsResourceWithName("examplecloud_containerette.test", "banane"), - querycheck.ContainsResourceWithName("examplecloud_containerette.test", "ananas"), - querycheck.ContainsResourceWithName("examplecloud_containerette.test", "kiwi"), - querycheck.ContainsResourceWithName("examplecloud_containerette.test2", "papaya"), - querycheck.ContainsResourceWithName("examplecloud_containerette.test2", "birne"), - querycheck.ContainsResourceWithName("examplecloud_containerette.test2", "kirsche"), + QueryResultChecks: []QueryResultCheck{ + ContainsResourceWithName("examplecloud_containerette.test", "banane"), + ContainsResourceWithName("examplecloud_containerette.test", "ananas"), + ContainsResourceWithName("examplecloud_containerette.test", "kiwi"), + ContainsResourceWithName("examplecloud_containerette.test2", "papaya"), + ContainsResourceWithName("examplecloud_containerette.test2", "birne"), + ContainsResourceWithName("examplecloud_containerette.test2", "kirsche"), }, }, }, @@ -71,24 +68,22 @@ func TestContainsResourceWithName(t *testing.T) { func TestContainsResourceWithName_NotFound(t *testing.T) { t.Parallel() - r.UnitTest(t, r.TestCase{ + UnitTest(t, TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ ListResources: map[string]testprovider.ListResource{ - - // TODO: define a resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use - - //"examplecloud_containerette": examplecloudListResource(), + // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use + "examplecloud_containerette": examplecloudListResource(), }, Resources: map[string]testprovider.Resource{ - //"examplecloud_containerette": examplecloudResource(), + "examplecloud_containerette": examplecloudResource(), }, }), }, - Steps: []r.TestStep{ + Steps: []TestStep{ { Query: true, Config: ` @@ -110,8 +105,8 @@ func TestContainsResourceWithName_NotFound(t *testing.T) { } } `, - QueryResultChecks: []querycheck.QueryResultCheck{ - querycheck.ContainsResourceWithName("examplecloud_containerette.test", "pflaume"), + QueryResultChecks: []QueryResultCheck{ + ContainsResourceWithName("examplecloud_containerette.test", "pflaume"), }, // TODO update expected error message to match what we output ExpectError: regexp.MustCompile("examplecloud_containerette.test - there are no pflaumen here!"), diff --git a/querycheck/environment_variables.go b/querycheck/environment_variables.go new file mode 100644 index 00000000..35bea66f --- /dev/null +++ b/querycheck/environment_variables.go @@ -0,0 +1,45 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +// Environment variables for acceptance testing. Additional environment +// variable constants can be found in the internal/plugintest package. +const ( + // Environment variable to enable acceptance tests using this package's + // ParallelTest and Test functions whose TestCase does not enable the + // IsUnitTest field. Defaults to disabled, in which each test will call + // (*testing.T).Skip(). Can be set to any value to enable acceptance tests, + // however "1" is conventional. + EnvTfAcc = "TF_ACC" + + // Environment variable with hostname for the provider under acceptance + // test. The hostname is the first portion of the full provider source + // address, such as "example.com" in example.com/myorg/myprovider. Defaults + // to "registry.terraform.io". + // + // Only required if any Terraform configuration set via the TestStep + // type Config field includes a provider source, such as the terraform + // configuration block required_providers attribute. + EnvTfAccProviderHost = "TF_ACC_PROVIDER_HOST" + + // Environment variable with namespace for the provider under acceptance + // test. The namespace is the second portion of the full provider source + // address, such as "myorg" in registry.terraform.io/myorg/myprovider. + // Defaults to "-" for Terraform 0.12-0.13 compatibility and "hashicorp". + // + // Only required if any Terraform configuration set via the TestStep + // type Config field includes a provider source, such as the terraform + // configuration block required_providers attribute. + EnvTfAccProviderNamespace = "TF_ACC_PROVIDER_NAMESPACE" + + // This is an undocumented compatibility flag. When this is set, a + // `Config`-mode test step will invoke a refresh before successful + // completion. + // + // This is a compatibility measure for test cases that have different -- + // but semantically-equal -- state representations in their test steps. + // When comparing two states, the testing framework is not aware of + // semantic equality or set equality. + EnvTfAccRefreshAfterApply = "TF_ACC_REFRESH_AFTER_APPLY" +) diff --git a/querycheck/examplecloud_list_test.go b/querycheck/examplecloud_list_test.go new file mode 100644 index 00000000..0e6302b6 --- /dev/null +++ b/querycheck/examplecloud_list_test.go @@ -0,0 +1,233 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "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/list" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" +) + +func examplecloudListResource() testprovider.ListResource { + return testprovider.ListResource{ + IncludeResource: true, + SchemaResponse: &list.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "resource_group_name", + Type: tftypes.String, + Required: true, + }, + }, + }, + }, + }, + ListResultsStream: &list.ListResultsStream{ + Results: func(push func(list.ListResult) bool) { + push(list.ListResult{ + Resource: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + "resource_group_name": tftypes.String, + "instances": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "foo/banane"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "banane"), + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "instances": tftypes.NewValue(tftypes.Number, 5), + }, + )), + Identity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "resource_group_name": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "name": tftypes.NewValue(tftypes.String, "banane"), + }, + )), + DisplayName: "banane", + }) + push(list.ListResult{ + Resource: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + "resource_group_name": tftypes.String, + "instances": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "foo/ananas"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "ananas"), + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "instances": tftypes.NewValue(tftypes.Number, 9000), + }, + )), + Identity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "resource_group_name": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "name": tftypes.NewValue(tftypes.String, "ananas"), + }, + )), + DisplayName: "ananas", + }) + push(list.ListResult{ + Resource: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + "resource_group_name": tftypes.String, + "instances": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "foo/kiwi"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "kiwi"), + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "instances": tftypes.NewValue(tftypes.Number, 88), + }, + )), + Identity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "resource_group_name": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "name": tftypes.NewValue(tftypes.String, "kiwi"), + }, + )), + DisplayName: "kiwi", + }) + push(list.ListResult{ + Resource: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + "resource_group_name": tftypes.String, + "instances": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "bar/papaya"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "banane"), + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "instances": tftypes.NewValue(tftypes.Number, 3), + }, + )), + Identity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "resource_group_name": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "resource_group_name": tftypes.NewValue(tftypes.String, "bar"), + "name": tftypes.NewValue(tftypes.String, "papaya"), + }, + )), + DisplayName: "papaya", + }) + push(list.ListResult{ + Resource: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + "resource_group_name": tftypes.String, + "instances": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "bar/birne"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "birne"), + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "instances": tftypes.NewValue(tftypes.Number, 8564), + }, + )), + Identity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "resource_group_name": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "resource_group_name": tftypes.NewValue(tftypes.String, "bar"), + "name": tftypes.NewValue(tftypes.String, "birne"), + }, + )), + DisplayName: "birne", + }) + push(list.ListResult{ + Resource: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + "resource_group_name": tftypes.String, + "instances": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "bar/kirsche"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "kirsche"), + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "instances": tftypes.NewValue(tftypes.Number, 500), + }, + )), + Identity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "resource_group_name": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "resource_group_name": tftypes.NewValue(tftypes.String, "bar"), + "name": tftypes.NewValue(tftypes.String, "kirsche"), + }, + )), + DisplayName: "kirsche", + }) + }, + }, + } +} diff --git a/querycheck/examplecloud_test.go b/querycheck/examplecloud_test.go new file mode 100644 index 00000000..cdd4ce1d --- /dev/null +++ b/querycheck/examplecloud_test.go @@ -0,0 +1,143 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "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/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" +) + +func examplecloudResource() testprovider.Resource { + return testprovider.Resource{ + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + "resource_group_name": tftypes.String, + "instances": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "foo/banana"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "banana"), + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "instances": tftypes.NewValue(tftypes.Number, int64(5)), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "resource_group_name": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "name": tftypes.NewValue(tftypes.String, "banana"), + }, + )), + }, + ReadResponse: &resource.ReadResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + "resource_group_name": tftypes.String, + "instances": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "foo/banana"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "banana"), + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "instances": tftypes.NewValue(tftypes.Number, int64(5)), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "resource_group_name": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "name": tftypes.NewValue(tftypes.String, "banana"), + }, + )), + }, + ImportStateResponse: &resource.ImportStateResponse{ + State: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + "resource_group_name": tftypes.String, + "instances": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "foo/banana"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "banana"), + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "instances": tftypes.NewValue(tftypes.Number, int64(5)), + }, + ), + Identity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "resource_group_name": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "name": tftypes.NewValue(tftypes.String, "banana"), + }, + )), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + ComputedStringAttribute("id"), + RequiredStringAttribute("location"), + RequiredStringAttribute("name"), + RequiredStringAttribute("resource_group_name"), + OptionalNumberAttribute("instances"), + }, + }, + }, + }, + IdentitySchemaResponse: &resource.IdentitySchemaResponse{ + Schema: &tfprotov6.ResourceIdentitySchema{ + Version: 1, + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "resource_group_name", + Type: tftypes.String, + RequiredForImport: true, + }, + { + Name: "name", + Type: tftypes.String, + RequiredForImport: true, + }, + }, + }, + }, + } +} diff --git a/querycheck/expect_identity_test.go b/querycheck/expect_identity_test.go index d296949a..a2288921 100644 --- a/querycheck/expect_identity_test.go +++ b/querycheck/expect_identity_test.go @@ -1,35 +1,33 @@ package querycheck import ( + "testing" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" - r "github.com/hashicorp/terraform-plugin-testing/helper/resource" "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/tfversion" - "testing" ) func TestExpectIdentity(t *testing.T) { t.Parallel() - r.UnitTest(t, r.TestCase{ + UnitTest(t, TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ ListResources: map[string]testprovider.ListResource{ - // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use - - //"examplecloud_containerette": examplecloudListResource(), + "examplecloud_containerette": examplecloudListResource(), }, Resources: map[string]testprovider.Resource{ - //"examplecloud_containerette": examplecloudResource(), + "examplecloud_containerette": examplecloudResource(), }, }), }, - Steps: []r.TestStep{ + Steps: []TestStep{ // TODO }, }) @@ -39,24 +37,22 @@ func TestExpectIdentity(t *testing.T) { func TestExpectIdentity_NotFound(t *testing.T) { t.Parallel() - r.UnitTest(t, r.TestCase{ + UnitTest(t, TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ ListResources: map[string]testprovider.ListResource{ - // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use - - //"examplecloud_containerette": examplecloudListResource(), + "examplecloud_containerette": examplecloudListResource(), }, Resources: map[string]testprovider.Resource{ - //"examplecloud_containerette": examplecloudResource(), + "examplecloud_containerette": examplecloudResource(), }, }), }, - Steps: []r.TestStep{ + Steps: []TestStep{ // TODO }, }) diff --git a/querycheck/expect_known_value_test.go b/querycheck/expect_known_value_test.go index d0121e39..6c26bcf9 100644 --- a/querycheck/expect_known_value_test.go +++ b/querycheck/expect_known_value_test.go @@ -1,35 +1,33 @@ package querycheck import ( + "testing" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" - r "github.com/hashicorp/terraform-plugin-testing/helper/resource" "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/tfversion" - "testing" ) func TestExpectKnownValue(t *testing.T) { t.Parallel() - r.UnitTest(t, r.TestCase{ + UnitTest(t, TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ ListResources: map[string]testprovider.ListResource{ - // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use - - //"examplecloud_containerette": examplecloudListResource(), + "examplecloud_containerette": examplecloudListResource(), }, Resources: map[string]testprovider.Resource{ - //"examplecloud_containerette": examplecloudResource(), + "examplecloud_containerette": examplecloudResource(), }, }), }, - Steps: []r.TestStep{ + Steps: []TestStep{ // TODO }, }) @@ -39,24 +37,22 @@ func TestExpectKnownValue(t *testing.T) { func TestExpectKnownValue_ValueIncorrect(t *testing.T) { t.Parallel() - r.UnitTest(t, r.TestCase{ + UnitTest(t, TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ ListResources: map[string]testprovider.ListResource{ - // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use - - //"examplecloud_containerette": examplecloudListResource(), + "examplecloud_containerette": examplecloudListResource(), }, Resources: map[string]testprovider.Resource{ - //"examplecloud_containerette": examplecloudResource(), + "examplecloud_containerette": examplecloudResource(), }, }), }, - Steps: []r.TestStep{ + Steps: []TestStep{ // TODO }, }) diff --git a/querycheck/expect_result_length_atleast_test.go b/querycheck/expect_result_length_atleast_test.go index afb351bb..7b770f9a 100644 --- a/querycheck/expect_result_length_atleast_test.go +++ b/querycheck/expect_result_length_atleast_test.go @@ -1,35 +1,33 @@ package querycheck import ( + "testing" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" - r "github.com/hashicorp/terraform-plugin-testing/helper/resource" "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/tfversion" - "testing" ) func TestResultLengthAtLeast(t *testing.T) { t.Parallel() - r.UnitTest(t, r.TestCase{ + UnitTest(t, TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ ListResources: map[string]testprovider.ListResource{ - // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use - - //"examplecloud_containerette": examplecloudListResource(), + "examplecloud_containerette": examplecloudListResource(), }, Resources: map[string]testprovider.Resource{ - //"examplecloud_containerette": examplecloudResource(), + "examplecloud_containerette": examplecloudResource(), }, }), }, - Steps: []r.TestStep{ + Steps: []TestStep{ // TODO }, }) @@ -39,24 +37,22 @@ func TestResultLengthAtLeast(t *testing.T) { func TestResultLengthAtLeast_TooFewResults(t *testing.T) { t.Parallel() - r.UnitTest(t, r.TestCase{ + UnitTest(t, TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ ListResources: map[string]testprovider.ListResource{ - // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use - - //"examplecloud_containerette": examplecloudListResource(), + "examplecloud_containerette": examplecloudListResource(), }, Resources: map[string]testprovider.Resource{ - //"examplecloud_containerette": examplecloudResource(), + "examplecloud_containerette": examplecloudResource(), }, }), }, - Steps: []r.TestStep{ + Steps: []TestStep{ // TODO }, }) diff --git a/querycheck/expect_result_length_exact_test.go b/querycheck/expect_result_length_exact_test.go index e3346838..cd1811dc 100644 --- a/querycheck/expect_result_length_exact_test.go +++ b/querycheck/expect_result_length_exact_test.go @@ -1,35 +1,33 @@ package querycheck import ( + "testing" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" - r "github.com/hashicorp/terraform-plugin-testing/helper/resource" "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/tfversion" - "testing" ) func TestResultLengthExact(t *testing.T) { t.Parallel() - r.UnitTest(t, r.TestCase{ + UnitTest(t, TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ ListResources: map[string]testprovider.ListResource{ - // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use - - //"examplecloud_containerette": examplecloudListResource(), + "examplecloud_containerette": examplecloudListResource(), }, Resources: map[string]testprovider.Resource{ - //"examplecloud_containerette": examplecloudResource(), + "examplecloud_containerette": examplecloudResource(), }, }), }, - Steps: []r.TestStep{ + Steps: []TestStep{ // TODO }, }) @@ -39,24 +37,22 @@ func TestResultLengthExact(t *testing.T) { func TestResultLengthExact_WrongAmount(t *testing.T) { t.Parallel() - r.UnitTest(t, r.TestCase{ + UnitTest(t, TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ ListResources: map[string]testprovider.ListResource{ - // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use - - //"examplecloud_containerette": examplecloudListResource(), + "examplecloud_containerette": examplecloudListResource(), }, Resources: map[string]testprovider.Resource{ - //"examplecloud_containerette": examplecloudResource(), + "examplecloud_containerette": examplecloudResource(), }, }), }, - Steps: []r.TestStep{ + Steps: []TestStep{ // TODO }, }) diff --git a/querycheck/plan_checks.go b/querycheck/plan_checks.go new file mode 100644 index 00000000..61754453 --- /dev/null +++ b/querycheck/plan_checks.go @@ -0,0 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "errors" + + tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/mitchellh/go-testing-interface" +) + +func runPlanChecks(ctx context.Context, t testing.T, plan *tfjson.Plan, planChecks []plancheck.PlanCheck) error { + t.Helper() + + var result []error + + for _, planCheck := range planChecks { + resp := plancheck.CheckPlanResponse{} + planCheck.CheckPlan(ctx, plancheck.CheckPlanRequest{Plan: plan}, &resp) + + result = append(result, resp.Error) + } + + return errors.Join(result...) +} diff --git a/querycheck/plugin.go b/querycheck/plugin.go new file mode 100644 index 00000000..2583d5be --- /dev/null +++ b/querycheck/plugin.go @@ -0,0 +1,528 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "fmt" + "io" + "os" + "strings" + "sync" + + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/terraform-exec/tfexec" + tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/plugin" + "github.com/mitchellh/go-testing-interface" + + "github.com/hashicorp/terraform-plugin-testing/internal/logging" + "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" +) + +// protov5ProviderFactory is a function which is called to start a protocol +// version 5 provider server. +type protov5ProviderFactory func() (tfprotov5.ProviderServer, error) + +// protov5ProviderFactories is a mapping of provider addresses to provider +// factory for protocol version 5 provider servers. +type protov5ProviderFactories map[string]func() (tfprotov5.ProviderServer, error) + +// merge combines provider factories. +// +// In case of an overlapping entry, the later entry will overwrite the previous +// value. +func (pf protov5ProviderFactories) merge(otherPfs ...protov5ProviderFactories) protov5ProviderFactories { + result := make(protov5ProviderFactories) + + for name, providerFactory := range pf { + result[name] = providerFactory + } + + for _, otherPf := range otherPfs { + for name, providerFactory := range otherPf { + result[name] = providerFactory + } + } + + return result +} + +// protov6ProviderFactory is a function which is called to start a protocol +// version 6 provider server. +type protov6ProviderFactory func() (tfprotov6.ProviderServer, error) + +// protov6ProviderFactories is a mapping of provider addresses to provider +// factory for protocol version 6 provider servers. +type protov6ProviderFactories map[string]func() (tfprotov6.ProviderServer, error) + +// merge combines provider factories. +// +// In case of an overlapping entry, the later entry will overwrite the previous +// value. +func (pf protov6ProviderFactories) merge(otherPfs ...protov6ProviderFactories) protov6ProviderFactories { + result := make(protov6ProviderFactories) + + for name, providerFactory := range pf { + result[name] = providerFactory + } + + for _, otherPf := range otherPfs { + for name, providerFactory := range otherPf { + result[name] = providerFactory + } + } + + return result +} + +// sdkProviderFactory is a function which is called to start a SDK provider +// server. +type sdkProviderFactory func() (*schema.Provider, error) + +// protov6ProviderFactories is a mapping of provider addresses to provider +// factory for protocol version 6 provider servers. +type sdkProviderFactories map[string]func() (*schema.Provider, error) + +// merge combines provider factories. +// +// In case of an overlapping entry, the later entry will overwrite the previous +// value. +func (pf sdkProviderFactories) merge(otherPfs ...sdkProviderFactories) sdkProviderFactories { + result := make(sdkProviderFactories) + + for name, providerFactory := range pf { + result[name] = providerFactory + } + + for _, otherPf := range otherPfs { + for name, providerFactory := range otherPf { + result[name] = providerFactory + } + } + + return result +} + +type providerFactories struct { + legacy sdkProviderFactories + protov5 protov5ProviderFactories + protov6 protov6ProviderFactories +} + +func runProviderCommandApplyRefreshOnly(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, factories *providerFactories) error { + t.Helper() + + fn := func() error { + return wd.Apply(ctx, tfexec.Refresh(true), tfexec.RefreshOnly(true)) + } + return runProviderCommand(ctx, t, wd, factories, fn) +} + +func runProviderCommandCreatePlan(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, factories *providerFactories) error { + t.Helper() + + fn := func() error { + return wd.CreatePlan(ctx) + } + return runProviderCommand(ctx, t, wd, factories, fn) +} + +func runProviderCommandSavedPlan(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, factories *providerFactories) (*tfjson.Plan, error) { + t.Helper() + + var plan *tfjson.Plan + fn := func() error { + var err error + plan, err = wd.SavedPlan(ctx) + return err + } + err := runProviderCommand(ctx, t, wd, factories, fn) + if err != nil { + return nil, err + } + + return plan, nil +} + +func runProviderCommand(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, factories *providerFactories, f func() error) error { + // don't point to this as a test failure location + // point to whatever called it + t.Helper() + + // This should not happen, but prevent panics just in case. + if factories == nil { + err := fmt.Errorf("Provider factories are missing to run Terraform command. Please report this bug in the testing framework.") + logging.HelperResourceError(ctx, err.Error()) + return err + } + + // Run the providers in the same process as the test runner using the + // reattach behavior in Terraform. This ensures we get test coverage + // and enables the use of delve as a debugger. + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + // this is needed so Terraform doesn't default to expecting protocol 4; + // we're skipping the handshake because Terraform didn't launch the + // plugins. + os.Setenv("PLUGIN_PROTOCOL_VERSIONS", "5") + + // Acceptance testing does not need to call checkpoint as the output + // is not accessible, nor desirable if explicitly using + // TF_ACC_TERRAFORM_PATH or TF_ACC_TERRAFORM_VERSION environment variables. + // + // Avoid calling (tfexec.Terraform).SetEnv() as it will stop copying + // os.Environ() and prevents TF_VAR_ environment variable usage. + os.Setenv("CHECKPOINT_DISABLE", "1") + + // Terraform 0.12.X and 0.13.X+ treat namespaceless providers + // differently in terms of what namespace they default to. So we're + // going to set both variations, as we don't know which version of + // Terraform we're talking to. We're also going to allow overriding + // the host or namespace using environment variables. + var namespaces []string + host := "registry.terraform.io" + if v := os.Getenv(EnvTfAccProviderNamespace); v != "" { + namespaces = append(namespaces, v) + } else { + namespaces = append(namespaces, "-", "hashicorp") + } + if v := os.Getenv(EnvTfAccProviderHost); v != "" { + host = v + } + + // schema.Provider have a global stop context that is created outside + // the server context and have their own associated goroutine. Since + // Terraform does not call the StopProvider RPC to stop the server in + // reattach mode, ensure that we save these servers to later call that + // RPC and end those goroutines. + legacyProviderServers := make([]*schema.GRPCProviderServer, 0, len(factories.legacy)) + + // Spin up gRPC servers for every provider factory, start a + // WaitGroup to listen for all of the close channels. + var wg sync.WaitGroup + reattachInfo := map[string]tfexec.ReattachConfig{} + for providerName, factory := range factories.legacy { + // providerName may be returned as terraform-provider-foo, and + // we need just foo. So let's fix that. + providerName = strings.TrimPrefix(providerName, "terraform-provider-") + providerAddress := getProviderAddr(providerName) + + logging.HelperResourceTrace(ctx, "Creating sdkv2 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + + provider, err := factory() + if err != nil { + return fmt.Errorf("unable to create provider %q from factory: %w", providerName, err) + } + + logging.HelperResourceTrace(ctx, "Created sdkv2 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + + // keep track of the running factory, so we can make sure it's + // shut down. + wg.Add(1) + + grpcProviderServer := schema.NewGRPCProviderServer(provider) + legacyProviderServers = append(legacyProviderServers, grpcProviderServer) + + // Ensure StopProvider is always called when returning early. + defer grpcProviderServer.StopProvider(ctx, nil) //nolint:errcheck // does not return errors + + // configure the settings our plugin will be served with + // the GRPCProviderFunc wraps a non-gRPC provider server + // into a gRPC interface, and the logger just discards logs + // from go-plugin. + opts := &plugin.ServeOpts{ + GRPCProviderFunc: func() tfprotov5.ProviderServer { + return grpcProviderServer + }, + Logger: hclog.New(&hclog.LoggerOptions{ + Name: "plugintest", + Level: hclog.Trace, + Output: io.Discard, + }), + NoLogOutputOverride: true, + UseTFLogSink: t, + ProviderAddr: providerAddress, + } + + logging.HelperResourceTrace(ctx, "Starting sdkv2 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + + config, closeCh, err := plugin.DebugServe(ctx, opts) + if err != nil { + return fmt.Errorf("unable to serve provider %q: %w", providerName, err) + } + + logging.HelperResourceTrace(ctx, "Started sdkv2 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + + tfexecConfig := tfexec.ReattachConfig{ + Protocol: config.Protocol, + ProtocolVersion: config.ProtocolVersion, + Pid: config.Pid, + Test: config.Test, + Addr: tfexec.ReattachConfigAddr{ + Network: config.Addr.Network, + String: config.Addr.String, + }, + } + + // when the provider exits, remove one from the waitgroup + // so we can track when everything is done + go func(c <-chan struct{}) { + <-c + wg.Done() + }(closeCh) + + // set our provider's reattachinfo in our map, once + // for every namespace that different Terraform versions + // may expect. + for _, ns := range namespaces { + reattachInfo[strings.TrimSuffix(host, "/")+"/"+ + strings.TrimSuffix(ns, "/")+"/"+ + providerName] = tfexecConfig + } + } + + // Now spin up gRPC servers for every protov5 provider factory + // in the same way. + for providerName, factory := range factories.protov5 { + // providerName may be returned as terraform-provider-foo, and + // we need just foo. So let's fix that. + providerName = strings.TrimPrefix(providerName, "terraform-provider-") + providerAddress := getProviderAddr(providerName) + + // If the user has supplied the same provider in both + // ProviderFactories and ProtoV5ProviderFactories, they made a + // mistake and we should exit early. + for _, ns := range namespaces { + reattachString := strings.TrimSuffix(host, "/") + "/" + + strings.TrimSuffix(ns, "/") + "/" + + providerName + if _, ok := reattachInfo[reattachString]; ok { + return fmt.Errorf("Provider %s registered in both TestCase.ProviderFactories and TestCase.ProtoV5ProviderFactories: please use one or the other, or supply a muxed provider to TestCase.ProtoV5ProviderFactories.", providerName) + } + } + + logging.HelperResourceTrace(ctx, "Creating tfprotov5 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + + provider, err := factory() + if err != nil { + return fmt.Errorf("unable to create provider %q from factory: %w", providerName, err) + } + + logging.HelperResourceTrace(ctx, "Created tfprotov5 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + + // keep track of the running factory, so we can make sure it's + // shut down. + wg.Add(1) + + // configure the settings our plugin will be served with + // the GRPCProviderFunc wraps a non-gRPC provider server + // into a gRPC interface, and the logger just discards logs + // from go-plugin. + opts := &plugin.ServeOpts{ + GRPCProviderFunc: func() tfprotov5.ProviderServer { + return provider + }, + Logger: hclog.New(&hclog.LoggerOptions{ + Name: "plugintest", + Level: hclog.Trace, + Output: io.Discard, + }), + NoLogOutputOverride: true, + UseTFLogSink: t, + ProviderAddr: providerAddress, + } + + logging.HelperResourceTrace(ctx, "Starting tfprotov5 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + + config, closeCh, err := plugin.DebugServe(ctx, opts) + if err != nil { + return fmt.Errorf("unable to serve provider %q: %w", providerName, err) + } + + logging.HelperResourceTrace(ctx, "Started tfprotov5 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + + tfexecConfig := tfexec.ReattachConfig{ + Protocol: config.Protocol, + ProtocolVersion: config.ProtocolVersion, + Pid: config.Pid, + Test: config.Test, + Addr: tfexec.ReattachConfigAddr{ + Network: config.Addr.Network, + String: config.Addr.String, + }, + } + + // when the provider exits, remove one from the waitgroup + // so we can track when everything is done + go func(c <-chan struct{}) { + <-c + wg.Done() + }(closeCh) + + // set our provider's reattachinfo in our map, once + // for every namespace that different Terraform versions + // may expect. + for _, ns := range namespaces { + reattachString := strings.TrimSuffix(host, "/") + "/" + + strings.TrimSuffix(ns, "/") + "/" + + providerName + reattachInfo[reattachString] = tfexecConfig + } + } + + // Now spin up gRPC servers for every protov6 provider factory + // in the same way. + for providerName, factory := range factories.protov6 { + // providerName may be returned as terraform-provider-foo, and + // we need just foo. So let's fix that. + providerName = strings.TrimPrefix(providerName, "terraform-provider-") + providerAddress := getProviderAddr(providerName) + + // If the user has already registered this provider in + // ProviderFactories or ProtoV5ProviderFactories, they made a + // mistake and we should exit early. + for _, ns := range namespaces { + reattachString := strings.TrimSuffix(host, "/") + "/" + + strings.TrimSuffix(ns, "/") + "/" + + providerName + if _, ok := reattachInfo[reattachString]; ok { + return fmt.Errorf("Provider %s registered in both TestCase.ProtoV6ProviderFactories and either TestCase.ProviderFactories or TestCase.ProtoV5ProviderFactories: please use one of the three, or supply a muxed provider to TestCase.ProtoV5ProviderFactories.", providerName) + } + } + + logging.HelperResourceTrace(ctx, "Creating tfprotov6 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + + provider, err := factory() + if err != nil { + return fmt.Errorf("unable to create provider %q from factory: %w", providerName, err) + } + + logging.HelperResourceTrace(ctx, "Created tfprotov6 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + + // keep track of the running factory, so we can make sure it's + // shut down. + wg.Add(1) + + opts := &plugin.ServeOpts{ + GRPCProviderV6Func: func() tfprotov6.ProviderServer { + return provider + }, + Logger: hclog.New(&hclog.LoggerOptions{ + Name: "plugintest", + Level: hclog.Trace, + Output: io.Discard, + }), + NoLogOutputOverride: true, + UseTFLogSink: t, + ProviderAddr: providerAddress, + } + + logging.HelperResourceTrace(ctx, "Starting tfprotov6 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + + config, closeCh, err := plugin.DebugServe(ctx, opts) + if err != nil { + return fmt.Errorf("unable to serve provider %q: %w", providerName, err) + } + + logging.HelperResourceTrace(ctx, "Started tfprotov6 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + + tfexecConfig := tfexec.ReattachConfig{ + Protocol: config.Protocol, + ProtocolVersion: config.ProtocolVersion, + Pid: config.Pid, + Test: config.Test, + Addr: tfexec.ReattachConfigAddr{ + Network: config.Addr.Network, + String: config.Addr.String, + }, + } + + // when the provider exits, remove one from the waitgroup + // so we can track when everything is done + go func(c <-chan struct{}) { + <-c + wg.Done() + }(closeCh) + + // set our provider's reattachinfo in our map, once + // for every namespace that different Terraform versions + // may expect. + for _, ns := range namespaces { + reattachString := strings.TrimSuffix(host, "/") + "/" + + strings.TrimSuffix(ns, "/") + "/" + + providerName + reattachInfo[reattachString] = tfexecConfig + } + } + + // set the working directory reattach info that will tell Terraform how to + // connect to our various running servers. + wd.SetReattachInfo(ctx, reattachInfo) + + logging.HelperResourceTrace(ctx, "Calling wrapped Terraform CLI command") + + // ok, let's call whatever Terraform command the test was trying to + // call, now that we know it'll attach back to those servers we just + // started. + err := f() + if err != nil { + logging.HelperResourceWarn(ctx, "Error running Terraform CLI command", map[string]interface{}{logging.KeyError: err}) + } + + logging.HelperResourceTrace(ctx, "Called wrapped Terraform CLI command") + logging.HelperResourceTrace(ctx, "Stopping providers") + + // cancel the servers so they'll return. Otherwise, this closeCh won't + // get closed, and we'll hang here. + cancel() + + // For legacy providers, call the StopProvider RPC so the StopContext + // goroutine is cleaned up properly. + for _, legacyProviderServer := range legacyProviderServers { + legacyProviderServer.StopProvider(ctx, nil) //nolint:errcheck // does not return errors + } + + logging.HelperResourceTrace(ctx, "Waiting for providers to stop") + + // wait for the servers to actually shut down; it may take a moment for + // them to clean up, or whatever. + // TODO: add a timeout here? + // PC: do we need one? The test will time out automatically... + wg.Wait() + + logging.HelperResourceTrace(ctx, "Providers have successfully stopped") + + // once we've run the Terraform command, let's remove the reattach + // information from the WorkingDir's environment. The WorkingDir will + // persist until the next call, but the server in the reattach info + // doesn't exist anymore at this point, so the reattach info is no + // longer valid. In theory it should be overwritten in the next call, + // but just to avoid any confusing bug reports, let's just unset the + // environment variable altogether. + wd.UnsetReattachInfo() + + // return any error returned from the orchestration code running + // Terraform commands + return err +} + +func getProviderAddr(name string) string { + host := "registry.terraform.io" + namespace := "hashicorp" + if v := os.Getenv(EnvTfAccProviderNamespace); v != "" { + namespace = v + } + if v := os.Getenv(EnvTfAccProviderHost); v != "" { + host = v + } + return strings.TrimSuffix(host, "/") + "/" + + strings.TrimSuffix(namespace, "/") + "/" + + name +} diff --git a/querycheck/query_checks.go b/querycheck/query_checks.go new file mode 100644 index 00000000..dcf18cb9 --- /dev/null +++ b/querycheck/query_checks.go @@ -0,0 +1,50 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "errors" + "fmt" + + tfjson "github.com/hashicorp/terraform-json" + "github.com/mitchellh/go-testing-interface" +) + +func RunQueryChecks(ctx context.Context, t testing.T, query []tfjson.LogMsg, queryChecks []QueryResultCheck) error { + t.Helper() + + var result []error + + if query == nil { + result = append(result, fmt.Errorf("no query results found")) + } + + found := make([]tfjson.ListResourceFoundData, 0) + summary := tfjson.ListCompleteData{} + + for _, msg := range query { + switch v := msg.(type) { + case tfjson.ListResourceFoundMessage: + found = append(found, v.ListResourceFound) + case tfjson.ListCompleteMessage: + summary = v.ListComplete + // TODO diagnostics and errors? + default: + continue + } + } + + for _, queryCheck := range queryChecks { + resp := CheckQueryResponse{} + queryCheck.CheckQuery(ctx, CheckQueryRequest{ + Query: found, + QuerySummary: &summary, + }, &resp) + + result = append(result, resp.Error) + } + + return errors.Join(result...) +} diff --git a/querycheck/state_checks.go b/querycheck/state_checks.go new file mode 100644 index 00000000..91095473 --- /dev/null +++ b/querycheck/state_checks.go @@ -0,0 +1,29 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "errors" + + tfjson "github.com/hashicorp/terraform-json" + "github.com/mitchellh/go-testing-interface" + + "github.com/hashicorp/terraform-plugin-testing/statecheck" +) + +func runStateChecks(ctx context.Context, t testing.T, state *tfjson.State, stateChecks []statecheck.StateCheck) error { + t.Helper() + + var result []error + + for _, stateCheck := range stateChecks { + resp := statecheck.CheckStateResponse{} + stateCheck.CheckState(ctx, statecheck.CheckStateRequest{State: state}, &resp) + + result = append(result, resp.Error) + } + + return errors.Join(result...) +} diff --git a/querycheck/state_shim.go b/querycheck/state_shim.go new file mode 100644 index 00000000..b0f1b067 --- /dev/null +++ b/querycheck/state_shim.go @@ -0,0 +1,344 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "encoding/json" + "fmt" + "strconv" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/terraform" + + "github.com/hashicorp/terraform-plugin-testing/internal/addrs" + "github.com/hashicorp/terraform-plugin-testing/internal/tfdiags" +) + +type shimmedState struct { + state *terraform.State +} + +func shimStateFromJson(jsonState *tfjson.State) (*terraform.State, error) { + state := terraform.NewState() //nolint:staticcheck // legacy usage + state.TFVersion = jsonState.TerraformVersion + + if jsonState.Values == nil { + // the state is empty + return state, nil + } + + for key, output := range jsonState.Values.Outputs { + os, err := shimOutputState(output) + if err != nil { + return nil, err + } + state.RootModule().Outputs[key] = os + } + + ss := &shimmedState{state} + err := ss.shimStateModule(jsonState.Values.RootModule) + if err != nil { + return nil, err + } + + return state, nil +} + +func shimOutputState(so *tfjson.StateOutput) (*terraform.OutputState, error) { + os := &terraform.OutputState{ + Sensitive: so.Sensitive, + } + + switch v := so.Value.(type) { + case string: + os.Type = "string" + os.Value = v + return os, nil + case []interface{}: + os.Type = "list" + if len(v) == 0 { + os.Value = v + return os, nil + } + + switch firstElem := v[0].(type) { + case string: + elements := make([]interface{}, len(v)) + for i, el := range v { + strElement, ok := el.(string) + // If the type of the element doesn't match the first elem, it's a tuple, return the original value + if !ok { + os.Value = v + return os, nil + } + elements[i] = strElement + } + os.Value = elements + case bool: + elements := make([]interface{}, len(v)) + for i, el := range v { + boolElement, ok := el.(bool) + // If the type of the element doesn't match the first elem, it's a tuple, return the original value + if !ok { + os.Value = v + return os, nil + } + + elements[i] = boolElement + } + os.Value = elements + // unmarshalled number from JSON will always be json.Number + case json.Number: + elements := make([]interface{}, len(v)) + for i, el := range v { + numberElement, ok := el.(json.Number) + // If the type of the element doesn't match the first elem, it's a tuple, return the original value + if !ok { + os.Value = v + return os, nil + } + + elements[i] = numberElement + } + os.Value = elements + case []interface{}: + os.Value = v + case map[string]interface{}: + os.Value = v + default: + return nil, fmt.Errorf("unexpected output list element type: %T", firstElem) + } + return os, nil + case map[string]interface{}: + os.Type = "map" + os.Value = v + return os, nil + case bool: + os.Type = "string" + os.Value = strconv.FormatBool(v) + return os, nil + // unmarshalled number from JSON will always be json.Number + case json.Number: + os.Type = "string" + os.Value = v.String() + return os, nil + } + + return nil, fmt.Errorf("unexpected output type: %T", so.Value) +} + +func (ss *shimmedState) shimStateModule(sm *tfjson.StateModule) error { + var path addrs.ModuleInstance + + if sm.Address == "" { + path = addrs.RootModuleInstance + } else { + var diags tfdiags.Diagnostics + path, diags = addrs.ParseModuleInstanceStr(sm.Address) + if diags.HasErrors() { + return diags.Err() + } + } + + mod := ss.state.AddModule(path) //nolint:staticcheck // legacy usage + for _, res := range sm.Resources { + resourceState, err := shimResourceState(res) + if err != nil { + return err + } + + key, err := shimResourceStateKey(res) + if err != nil { + return err + } + + mod.Resources[key] = resourceState + } + + if len(sm.ChildModules) > 0 { + return fmt.Errorf("Modules are not supported. Found %d modules.", + len(sm.ChildModules)) + } + return nil +} + +func shimResourceStateKey(res *tfjson.StateResource) (string, error) { + if res.Index == nil { + return res.Address, nil + } + + var mode terraform.ResourceMode + switch res.Mode { + case tfjson.DataResourceMode: + mode = terraform.DataResourceMode + case tfjson.ManagedResourceMode: + mode = terraform.ManagedResourceMode + default: + return "", fmt.Errorf("unexpected resource mode for %q", res.Address) + } + + var index int + switch idx := res.Index.(type) { + case json.Number: + i, err := idx.Int64() + if err != nil { + return "", fmt.Errorf("unexpected index value (%q) for %q, ", + idx, res.Address) + } + index = int(i) + default: + return "", fmt.Errorf("unexpected index type (%T) for %q, "+ + "for_each is not supported", res.Index, res.Address) + } + + rsk := &terraform.ResourceStateKey{ + Mode: mode, + Type: res.Type, + Name: res.Name, + Index: index, + } + + return rsk.String(), nil +} + +func shimResourceState(res *tfjson.StateResource) (*terraform.ResourceState, error) { + sf := &shimmedFlatmap{} + err := sf.FromMap(res.AttributeValues) + if err != nil { + return nil, err + } + attributes := sf.Flatmap() + + // The instance state identifier was a Terraform versions 0.11 and earlier + // concept which helped core and the then SDK determine if the resource + // should be removed and as an identifier value in the human readable + // output. This concept unfortunately carried over to the testing logic when + // the testing logic was mostly changed to use the public, machine-readable + // JSON interface with Terraform, rather than reusing prior internal logic + // from Terraform. Using the "id" attribute value for this identifier was + // the default implementation and therefore those older versions of + // Terraform required the attribute. This is no longer necessary after + // Terraform versions 0.12 and later. + // + // If the "id" attribute is not found, set the instance state identifier to + // a synthetic value that can hopefully lead someone encountering the value + // to these comments. The prior logic used to raise an error if the + // attribute was not present, but this value should now only be present in + // legacy logic of this Go module, such as unintentionally exported logic in + // the terraform package, and not encountered during normal testing usage. + // + // Reference: https://github.com/hashicorp/terraform-plugin-testing/issues/84 + instanceStateID, ok := attributes["id"] + + if !ok { + instanceStateID = "id-attribute-not-set" + } + + return &terraform.ResourceState{ + Provider: res.ProviderName, + Type: res.Type, + Primary: &terraform.InstanceState{ + ID: instanceStateID, + Attributes: attributes, + Meta: map[string]interface{}{ + "schema_version": int(res.SchemaVersion), + }, + Tainted: res.Tainted, + }, + Dependencies: res.DependsOn, + }, nil +} + +type shimmedFlatmap struct { + m map[string]string +} + +func (sf *shimmedFlatmap) FromMap(attributes map[string]interface{}) error { + if sf.m == nil { + sf.m = make(map[string]string, len(attributes)) + } + + return sf.AddMap("", attributes) +} + +func (sf *shimmedFlatmap) AddMap(prefix string, m map[string]interface{}) error { + for key, value := range m { + k := key + if prefix != "" { + k = fmt.Sprintf("%s.%s", prefix, key) + } + + err := sf.AddEntry(k, value) + if err != nil { + return fmt.Errorf("unable to add map key %q entry: %w", k, err) + } + } + + mapLength := "%" + if prefix != "" { + mapLength = fmt.Sprintf("%s.%s", prefix, "%") + } + + if err := sf.AddEntry(mapLength, strconv.Itoa(len(m))); err != nil { + return fmt.Errorf("unable to add map length %q entry: %w", mapLength, err) + } + + return nil +} + +func (sf *shimmedFlatmap) AddSlice(name string, elements []interface{}) error { + for i, elem := range elements { + key := fmt.Sprintf("%s.%d", name, i) + err := sf.AddEntry(key, elem) + if err != nil { + return fmt.Errorf("unable to add slice key %q entry: %w", key, err) + } + } + + sliceLength := fmt.Sprintf("%s.#", name) + if err := sf.AddEntry(sliceLength, strconv.Itoa(len(elements))); err != nil { + return fmt.Errorf("unable to add slice length %q entry: %w", sliceLength, err) + } + + return nil +} + +func (sf *shimmedFlatmap) AddEntry(key string, value interface{}) error { + switch el := value.(type) { + case nil: + // omit the entry + return nil + case bool: + sf.m[key] = strconv.FormatBool(el) + case json.Number: + sf.m[key] = el.String() + case string: + sf.m[key] = el + case map[string]interface{}: + err := sf.AddMap(key, el) + if err != nil { + return err + } + case []interface{}: + err := sf.AddSlice(key, el) + if err != nil { + return err + } + default: + // This should never happen unless terraform-json + // changes how attributes (types) are represented. + // + // We handle all types which the JSON unmarshaler + // can possibly produce + // https://golang.org/pkg/encoding/json/#Unmarshal + + return fmt.Errorf("%q: unexpected type (%T)", key, el) + } + return nil +} + +func (sf *shimmedFlatmap) Flatmap() map[string]string { + return sf.m +} diff --git a/querycheck/testcase_providers.go b/querycheck/testcase_providers.go new file mode 100644 index 00000000..1ea783cc --- /dev/null +++ b/querycheck/testcase_providers.go @@ -0,0 +1,61 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "fmt" + "strings" +) + +// providerConfig takes the list of providers in a TestCase and returns a +// config with only empty provider blocks. This is useful for Import, where no +// config is provided, but the providers must be defined. +func (c TestCase) providerConfig(_ context.Context, skipProviderBlock bool) string { + var providerBlocks, requiredProviderBlocks strings.Builder + + // [BF] The Providers field handling predates the logic being moved to this + // method. It's not entirely clear to me at this time why this field + // is being used and not the others, but leaving it here just in case + // it does have a special purpose that wasn't being unit tested prior. + for name := range c.Providers { + providerBlocks.WriteString(fmt.Sprintf("provider %q {}\n", name)) + } + + for name, externalProvider := range c.ExternalProviders { + if !skipProviderBlock { + providerBlocks.WriteString(fmt.Sprintf("provider %q {}\n", name)) + } + + if externalProvider.Source == "" && externalProvider.VersionConstraint == "" { + continue + } + + requiredProviderBlocks.WriteString(fmt.Sprintf(" %s = {\n", name)) + + if externalProvider.Source != "" { + requiredProviderBlocks.WriteString(fmt.Sprintf(" source = %q\n", externalProvider.Source)) + } + + if externalProvider.VersionConstraint != "" { + requiredProviderBlocks.WriteString(fmt.Sprintf(" version = %q\n", externalProvider.VersionConstraint)) + } + + requiredProviderBlocks.WriteString(" }\n") + } + + if requiredProviderBlocks.Len() > 0 { + return fmt.Sprintf(` +terraform { + required_providers { +%[1]s + } +} + +%[2]s +`, strings.TrimSuffix(requiredProviderBlocks.String(), "\n"), providerBlocks.String()) + } + + return providerBlocks.String() +} diff --git a/querycheck/testcase_test.go b/querycheck/testcase_test.go new file mode 100644 index 00000000..5a7d70e2 --- /dev/null +++ b/querycheck/testcase_test.go @@ -0,0 +1,122 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +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/datasource" + "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" +) + +// Reference: https://github.com/hashicorp/terraform-plugin-testing/issues/84 +func TestTestCase_NoDataSourceIdRequirement(t *testing.T) { + t.Parallel() + + UnitTest(t, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + }, + Steps: []TestStep{ + { + Check: ComposeAggregateTestCheckFunc( + TestCheckNoResourceAttr("data.test_datasource.test", "id"), + TestCheckResourceAttr("data.test_datasource.test", "not_id", "test"), + ), + Config: `data "test_datasource" "test" {}`, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + DataSources: map[string]testprovider.DataSource{ + "test_datasource": { + ReadResponse: &datasource.ReadResponse{ + State: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "not_id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "not_id": tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, + SchemaResponse: &datasource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "not_id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + }, + }, + }) +} + +// Reference: https://github.com/hashicorp/terraform-plugin-testing/issues/84 +func TestTestCase_NoResourceIdRequirement(t *testing.T) { + t.Parallel() + + UnitTest(t, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + }, + Steps: []TestStep{ + { + Check: ComposeAggregateTestCheckFunc( + TestCheckNoResourceAttr("test_resource.test", "id"), + TestCheckResourceAttr("test_resource.test", "not_id", "test"), + ), + Config: `resource "test_resource" "test" {}`, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "not_id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "not_id": tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "not_id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + }, + }, + }) +} diff --git a/querycheck/testcase_validate.go b/querycheck/testcase_validate.go new file mode 100644 index 00000000..c6f908da --- /dev/null +++ b/querycheck/testcase_validate.go @@ -0,0 +1,113 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "fmt" + + "github.com/mitchellh/go-testing-interface" + + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/internal/logging" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" +) + +// hasProviders returns true if the TestCase has ExternalProviders set. +func (c TestCase) hasExternalProviders(_ context.Context) bool { + return len(c.ExternalProviders) > 0 +} + +// hasProviders returns true if the TestCase has set any of the +// ExternalProviders, ProtoV5ProviderFactories, ProtoV6ProviderFactories, +// ProviderFactories, or Providers fields. +func (c TestCase) hasProviders(_ context.Context) bool { + if len(c.ExternalProviders) > 0 { + return true + } + + if len(c.ProtoV5ProviderFactories) > 0 { + return true + } + + if len(c.ProtoV6ProviderFactories) > 0 { + return true + } + + if len(c.ProviderFactories) > 0 { + return true + } + + if len(c.Providers) > 0 { + return true + } + + return false +} + +// validate ensures the TestCase is valid based on the following criteria: +// +// - No overlapping ExternalProviders and Providers entries +// - No overlapping ExternalProviders and ProviderFactories entries +// - TestStep validations performed by the (TestStep).validate() method. +func (c TestCase) validate(ctx context.Context, t testing.T) error { + logging.HelperResourceTrace(ctx, "Validating TestCase") + + if len(c.Steps) == 0 { + err := fmt.Errorf("TestCase missing Steps") + logging.HelperResourceError(ctx, "TestCase validation error", map[string]interface{}{logging.KeyError: err}) + return err + } + + for name := range c.ExternalProviders { + if _, ok := c.Providers[name]; ok { + err := fmt.Errorf("TestCase provider %q set in both ExternalProviders and Providers", name) + logging.HelperResourceError(ctx, "TestCase validation error", map[string]interface{}{logging.KeyError: err}) + return err + } + + if _, ok := c.ProviderFactories[name]; ok { + err := fmt.Errorf("TestCase provider %q set in both ExternalProviders and ProviderFactories", name) + logging.HelperResourceError(ctx, "TestCase validation error", map[string]interface{}{logging.KeyError: err}) + return err + } + } + + testCaseHasExternalProviders := c.hasExternalProviders(ctx) + testCaseHasProviders := c.hasProviders(ctx) + + for stepIndex, step := range c.Steps { + stepNumber := stepIndex + 1 // Use 1-based index for humans + + configRequest := teststep.PrepareConfigurationRequest{ + Directory: step.ConfigDirectory, + File: step.ConfigFile, + Raw: step.Config, + TestStepConfigRequest: config.TestStepConfigRequest{ + StepNumber: stepNumber, + TestName: t.Name(), + }, + }.Exec() + + stepConfiguration := teststep.Configuration(configRequest) + + stepValidateReq := testStepValidateRequest{ + StepConfiguration: stepConfiguration, + StepNumber: stepNumber, + TestCaseHasExternalProviders: testCaseHasExternalProviders, + TestCaseHasProviders: testCaseHasProviders, + TestName: t.Name(), + } + + err := step.validate(ctx, stepValidateReq) + + if err != nil { + err := fmt.Errorf("TestStep %d/%d validation error: %w", stepNumber, len(c.Steps), err) + logging.HelperResourceError(ctx, "TestCase validation error", map[string]interface{}{logging.KeyError: err}) + return err + } + } + + return nil +} diff --git a/querycheck/testing.go b/querycheck/testing.go new file mode 100644 index 00000000..a9d1b9ef --- /dev/null +++ b/querycheck/testing.go @@ -0,0 +1,2161 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "errors" + "flag" + "fmt" + "log" + "os" + "regexp" + "strconv" + "strings" + "time" + + "github.com/mitchellh/go-testing-interface" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + + "github.com/hashicorp/terraform-plugin-testing/internal/addrs" + "github.com/hashicorp/terraform-plugin-testing/internal/logging" + "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" +) + +// flagSweep is a flag available when running tests on the command line. It +// contains a comma separated list of regions to for the sweeper functions to +// run in. This flag bypasses the normal Test path and instead runs functions designed to +// clean up any leaked resources a testing environment could have created. It is +// a best effort attempt, and relies on Provider authors to implement "Sweeper" +// methods for resources. + +// Adding Sweeper methods with AddTestSweepers will +// construct a list of sweeper funcs to be called here. We iterate through +// regions provided by the sweep flag, and for each region we iterate through the +// tests, and exit on any errors. At time of writing, sweepers are ran +// sequentially, however they can list dependencies to be ran first. We track +// the sweepers that have been ran, so as to not run a sweeper twice for a given +// region. +// +// WARNING: +// Sweepers are designed to be destructive. You should not use the -sweep flag +// in any environment that is not strictly a test environment. Resources will be +// destroyed. + +var flagSweep = flag.String("sweep", "", "List of Regions to run available Sweepers") +var flagSweepAllowFailures = flag.Bool("sweep-allow-failures", false, "Enable to allow Sweeper Tests to continue after failures") +var flagSweepRun = flag.String("sweep-run", "", "Comma separated list of Sweeper Tests to run") +var sweeperFuncs map[string]*Sweeper + +// SweeperFunc is a signature for a function that acts as a sweeper. It +// accepts a string for the region that the sweeper is to be ran in. This +// function must be able to construct a valid client for that region. +type SweeperFunc func(r string) error + +type Sweeper struct { + // Name for sweeper. Must be unique to be ran by the Sweeper Runner + Name string + + // Dependencies list the const names of other Sweeper functions that must be ran + // prior to running this Sweeper. This is an ordered list that will be invoked + // recursively at the helper/resource level + Dependencies []string + + // Sweeper function that when invoked sweeps the Provider of specific + // resources + F SweeperFunc +} + +func init() { + sweeperFuncs = make(map[string]*Sweeper) +} + +// AddTestSweepers function adds a given name and Sweeper configuration +// pair to the internal sweeperFuncs map. Invoke this function to register a +// resource sweeper to be available for running when the -sweep flag is used +// with `go test`. Sweeper names must be unique to help ensure a given sweeper +// is only ran once per run. +func AddTestSweepers(name string, s *Sweeper) { + if _, ok := sweeperFuncs[name]; ok { + log.Fatalf("[ERR] Error adding (%s) to sweeperFuncs: function already exists in map", name) + } + + sweeperFuncs[name] = s +} + +// TestMain adds sweeper functionality to the "go test" command, otherwise +// tests are executed as normal. Most provider acceptance tests are written +// using the Test() function of this package, which imposes its own +// requirements and Terraform CLI behavior. Refer to that function's +// documentation for additional details. +// +// Sweepers enable infrastructure cleanup functions to be included with +// resource definitions, typically so developers can remove all resources of +// that resource type from testing infrastructure in case of failures that +// prevented the normal resource destruction behavior of acceptance tests. +// Use the AddTestSweepers() function to configure available sweepers. +// +// Sweeper flags added to the "go test" command: +// +// -sweep: Comma-separated list of locations/regions to run available sweepers. +// -sweep-allow-failures: Enable to allow other sweepers to run after failures. +// -sweep-run: Comma-separated list of resource type sweepers to run. Defaults +// to all sweepers. +// +// Refer to the Env prefixed constants for environment variables that further +// control testing functionality. +func TestMain(m interface { + Run() int +}) { + flag.Parse() + if *flagSweep != "" { + // parse flagSweep contents for regions to run + regions := strings.Split(*flagSweep, ",") + + // get filtered list of sweepers to run based on sweep-run flag + sweepers := filterSweepers(*flagSweepRun, sweeperFuncs) + + if _, err := runSweepers(regions, sweepers, *flagSweepAllowFailures); err != nil { + os.Exit(1) + } + } else { + exitCode := m.Run() + os.Exit(exitCode) + } +} + +func runSweepers(regions []string, sweepers map[string]*Sweeper, allowFailures bool) (map[string]map[string]error, error) { + var sweeperErrorFound bool + sweeperRunList := make(map[string]map[string]error) + + for _, region := range regions { + region = strings.TrimSpace(region) + + var regionSweeperErrorFound bool + regionSweeperRunList := make(map[string]error) + + start := time.Now() + log.Printf("[DEBUG] Running Sweepers for region (%s):\n", region) + for _, sweeper := range sweepers { + if err := runSweeperWithRegion(region, sweeper, sweepers, regionSweeperRunList, allowFailures); err != nil { + if allowFailures { + continue + } + + sweeperRunList[region] = regionSweeperRunList + return sweeperRunList, fmt.Errorf("sweeper (%s) for region (%s) failed: %s", sweeper.Name, region, err) + } + } + elapsed := time.Since(start) + log.Printf("Completed Sweepers for region (%s) in %s", region, elapsed) + + log.Printf("Sweeper Tests for region (%s) ran successfully:\n", region) + for sweeper, sweeperErr := range regionSweeperRunList { + if sweeperErr == nil { + log.Printf("\t- %s\n", sweeper) + } else { + regionSweeperErrorFound = true + } + } + + if regionSweeperErrorFound { + sweeperErrorFound = true + log.Printf("Sweeper Tests for region (%s) ran unsuccessfully:\n", region) + for sweeper, sweeperErr := range regionSweeperRunList { + if sweeperErr != nil { + log.Printf("\t- %s: %s\n", sweeper, sweeperErr) + } + } + } + + sweeperRunList[region] = regionSweeperRunList + } + + if sweeperErrorFound { + return sweeperRunList, errors.New("at least one sweeper failed") + } + + return sweeperRunList, nil +} + +// filterSweepers takes a comma separated string listing the names of sweepers +// to be ran, and returns a filtered set from the list of all of sweepers to +// run based on the names given. +func filterSweepers(f string, source map[string]*Sweeper) map[string]*Sweeper { + filterSlice := strings.Split(strings.ToLower(f), ",") + if len(filterSlice) == 1 && filterSlice[0] == "" { + // if the filter slice is a single element of "" then no sweeper list was + // given, so just return the full list + return source + } + + sweepers := make(map[string]*Sweeper) + for name := range source { + for _, s := range filterSlice { + if strings.Contains(strings.ToLower(name), s) { + for foundName, foundSweeper := range filterSweeperWithDependencies(name, source) { + sweepers[foundName] = foundSweeper + } + } + } + } + return sweepers +} + +// filterSweeperWithDependencies recursively returns sweeper and all dependencies. +// Since filterSweepers performs fuzzy matching, this function is used +// to perform exact sweeper and dependency lookup. +func filterSweeperWithDependencies(name string, source map[string]*Sweeper) map[string]*Sweeper { + result := make(map[string]*Sweeper) + + currentSweeper, ok := source[name] + if !ok { + log.Printf("[WARN] Sweeper has dependency (%s), but that sweeper was not found", name) + return result + } + + result[name] = currentSweeper + + for _, dependency := range currentSweeper.Dependencies { + for foundName, foundSweeper := range filterSweeperWithDependencies(dependency, source) { + result[foundName] = foundSweeper + } + } + + return result +} + +// runSweeperWithRegion receives a sweeper and a region, and recursively calls +// itself with that region for every dependency found for that sweeper. If there +// are no dependencies, invoke the contained sweeper fun with the region, and +// add the success/fail status to the sweeperRunList. +func runSweeperWithRegion(region string, s *Sweeper, sweepers map[string]*Sweeper, sweeperRunList map[string]error, allowFailures bool) error { + for _, dep := range s.Dependencies { + depSweeper, ok := sweepers[dep] + + if !ok { + log.Printf("[ERROR] Sweeper (%s) has dependency (%s), but that sweeper was not found", s.Name, dep) + return fmt.Errorf("sweeper (%s) has dependency (%s), but that sweeper was not found", s.Name, dep) + } + + log.Printf("[DEBUG] Sweeper (%s) has dependency (%s), running..", s.Name, dep) + err := runSweeperWithRegion(region, depSweeper, sweepers, sweeperRunList, allowFailures) + + if err != nil { + if allowFailures { + log.Printf("[ERROR] Error running Sweeper (%s) in region (%s): %s", depSweeper.Name, region, err) + continue + } + + return err + } + } + + if _, ok := sweeperRunList[s.Name]; ok { + log.Printf("[DEBUG] Sweeper (%s) already ran in region (%s)", s.Name, region) + return nil + } + + log.Printf("[DEBUG] Running Sweeper (%s) in region (%s)", s.Name, region) + + start := time.Now() + runE := s.F(region) + elapsed := time.Since(start) + + log.Printf("[DEBUG] Completed Sweeper (%s) in region (%s) in %s", s.Name, region, elapsed) + + sweeperRunList[s.Name] = runE + + if runE != nil { + log.Printf("[ERROR] Error running Sweeper (%s) in region (%s): %s", s.Name, region, runE) + } + + return runE +} + +// Deprecated: Use EnvTfAcc instead. +const TestEnvVar = EnvTfAcc + +// TestCheckFunc is the callback type used with acceptance tests to check +// the state of a resource. The state passed in is the latest state known, +// or in the case of being after a destroy, it is the last known state when +// it was created. +type TestCheckFunc func(*terraform.State) error + +// ImportStateCheckFunc is the check function for ImportState tests +type ImportStateCheckFunc func([]*terraform.InstanceState) error + +// ImportStateIdFunc is an ID generation function to help with complex ID +// generation for ImportState tests. +type ImportStateIdFunc func(*terraform.State) (string, error) + +// ErrorCheckFunc is a function providers can use to handle errors. +type ErrorCheckFunc func(error) error + +// TestCase is a single acceptance test case used to test the apply/destroy +// lifecycle of a resource in a specific configuration. +// +// When the destroy plan is executed, the config from the last TestStep +// is used to plan it. +// +// Refer to the Env prefixed constants for environment variables that further +// control testing functionality. +type TestCase struct { + // IsUnitTest allows a test to run regardless of the TF_ACC + // environment variable. This should be used with care - only for + // fast tests on local resources (e.g. remote state with a local + // backend) but can be used to increase confidence in correct + // operation of Terraform without waiting for a full acctest run. + IsUnitTest bool + + // PreCheck, if non-nil, will be called before any test steps are + // executed. It will only be executed in the case that the steps + // would run, so it can be used for some validation before running + // acceptance tests, such as verifying that keys are setup. + PreCheck func() + + // TerraformVersionChecks is a list of checks to run against + // the Terraform CLI version which is running the testing. + // Each check is executed in order, respecting the first skip + // or fail response, unless the Any() meta check is also used. + TerraformVersionChecks []tfversion.TerraformVersionCheck + + // ProviderFactories can be specified for the providers that are valid. + // + // This can also be specified at the TestStep level to enable per-step + // differences in providers, however all provider specifications must + // be done either at the TestCase level or TestStep level, otherwise the + // testing framework will raise an error and fail the test. + // + // These are the providers that can be referenced within the test. Each key + // is an individually addressable provider. Typically you will only pass a + // single value here for the provider you are testing. Aliases are not + // supported by the test framework, so to use multiple provider instances, + // you should add additional copies to this map with unique names. To set + // their configuration, you would reference them similar to the following: + // + // provider "my_factory_key" { + // # ... + // } + // + // resource "my_resource" "mr" { + // provider = my_factory_key + // + // # ... + // } + ProviderFactories map[string]func() (*schema.Provider, error) + + // ProtoV5ProviderFactories serves the same purpose as ProviderFactories, + // but for protocol v5 providers defined using the terraform-plugin-go + // ProviderServer interface. + // + // This can also be specified at the TestStep level to enable per-step + // differences in providers, however all provider specifications must + // be done either at the TestCase level or TestStep level, otherwise the + // testing framework will raise an error and fail the test. + ProtoV5ProviderFactories map[string]func() (tfprotov5.ProviderServer, error) + + // ProtoV6ProviderFactories serves the same purpose as ProviderFactories, + // but for protocol v6 providers defined using the terraform-plugin-go + // ProviderServer interface. + // The version of Terraform used in acceptance testing must be greater + // than or equal to v0.15.4 to use ProtoV6ProviderFactories. + // + // This can also be specified at the TestStep level to enable per-step + // differences in providers, however all provider specifications must + // be done either at the TestCase level or TestStep level, otherwise the + // testing framework will raise an error and fail the test. + ProtoV6ProviderFactories map[string]func() (tfprotov6.ProviderServer, error) + + // Providers is the ResourceProvider that will be under test. + // + // Deprecated: Providers is deprecated, please use ProviderFactories + Providers map[string]*schema.Provider + + // ExternalProviders are providers the TestCase relies on that should + // be downloaded from the registry during init. + // + // This can also be specified at the TestStep level to enable per-step + // differences in providers, however all provider specifications must + // be done either at the TestCase level or TestStep level, otherwise the + // testing framework will raise an error and fail the test. + // + // This is generally unnecessary to set at the TestCase level, however + // it has existing in the testing framework prior to the introduction of + // TestStep level specification and was only necessary for performing + // import testing where the configuration contained a provider outside the + // one under test. + ExternalProviders map[string]ExternalProvider + + // PreventPostDestroyRefresh can be set to true for cases where data sources + // are tested alongside real resources + PreventPostDestroyRefresh bool + + // CheckDestroy is called after the resource is finally destroyed + // to allow the tester to test that the resource is truly gone. + CheckDestroy TestCheckFunc + + // ErrorCheck allows providers the option to handle errors such as skipping + // tests based on certain errors. + // + // This functionality is only intended for provider-controlled error + // messaging. While in certain scenarios this can also catch testing logic + // error messages, those messages are not protected by compatibility + // promises. + ErrorCheck ErrorCheckFunc + + // Steps are the apply sequences done within the context of the + // same state. Each step can have its own check to verify correctness. + Steps []TestStep + + // IDRefreshName is the name of the resource to check during ID-only + // refresh testing, which ensures that a resource can be refreshed solely + // by its identifier. This will default to the first non-nil primary + // resource in the state. It runs every TestStep. + // + // While not deprecated, most resource tests should instead prefer using + // TestStep.ImportState based testing as it works with multiple attribute + // identifiers and also verifies resource import functionality. + IDRefreshName string + + // IDRefreshIgnore is a list of configuration keys that will be ignored + // during ID-only refresh testing. + IDRefreshIgnore []string + + // WorkingDir sets the base directory where testing files used by the testing + // module are generated. If WorkingDir is unset, a randomized, temporary + // directory is used. + // + // Use the TF_ACC_PERSIST_WORKING_DIR environment variable, conventionally + // set to "1", to persist any working directory files. Otherwise, this directory is + // automatically cleaned up at the end of the TestCase. + WorkingDir string + + // AdditionalCLIOptions allows an intentionally limited set of options to be passed + // to the Terraform CLI when executing test steps. + AdditionalCLIOptions *AdditionalCLIOptions +} + +// ExternalProvider holds information about third-party providers that should +// be downloaded by Terraform as part of running the test step. +type ExternalProvider struct { + VersionConstraint string // the version constraint for the provider + Source string // the provider source +} + +type ImportStateKind byte + +const ( + // ImportCommandWithID tests import by using the ID string with the `terraform import` command + ImportCommandWithID ImportStateKind = iota + + // ImportBlockWithID tests import by using the ID string in an import configuration block with the `terraform plan` command + ImportBlockWithID + + // ImportBlockWithResourceIdentity imports the state using an import block with a resource identity + ImportBlockWithResourceIdentity +) + +// plannable reports whether this kind indicates the use of plannable import blocks +func (kind ImportStateKind) plannable() bool { + return kind == ImportBlockWithID || kind == ImportBlockWithResourceIdentity +} + +// resourceIdentity reports whether this kind indicates the use of resource identity in import blocks +func (kind ImportStateKind) resourceIdentity() bool { + return kind == ImportBlockWithResourceIdentity +} + +func (kind ImportStateKind) String() string { + return map[ImportStateKind]string{ + ImportCommandWithID: "ImportCommandWithID", + ImportBlockWithID: "ImportBlockWithID", + ImportBlockWithResourceIdentity: "ImportBlockWithResourceIdentity", + }[kind] +} + +// TestStep is a single apply sequence of a test, done within the +// context of a state. +// +// Multiple TestSteps can be sequenced in a Test to allow testing +// potentially complex update logic. In general, simply create/destroy +// tests will only need one step. +// +// Refer to the Env prefixed constants for environment variables that further +// control testing functionality. +type TestStep struct { + // ResourceName should be set to the name of the resource + // that is being tested. Example: "aws_instance.foo". Various test + // modes use this to auto-detect state information. + // + // This is only required if the test mode settings below say it is + // for the mode you're using. + ResourceName string + + // PreConfig is called before the Config is applied to perform any per-step + // setup that needs to happen. This is called regardless of "test mode" + // below. + PreConfig func() + + // Taint is a list of resource addresses to taint prior to the execution of + // the step. Be sure to only include this at a step where the referenced + // address will be present in state, as it will fail the test if the resource + // is missing. + // + // This option is ignored on ImportState tests, and currently only works for + // resources in the root module path. + Taint []string + + //--------------------------------------------------------------- + // Test modes. One of the following groups of settings must be + // set to determine what the test step will do. Ideally we would've + // used Go interfaces here but there are now hundreds of tests we don't + // want to re-type so instead we just determine which step logic + // to run based on what settings below are set. + //--------------------------------------------------------------- + + //--------------------------------------------------------------- + // Plan, Apply testing + //--------------------------------------------------------------- + + // Config a string of the configuration to give to Terraform. If this + // is set, then the TestCase will execute this step with the same logic + // as a `terraform apply`. If both Config and ConfigDirectory are set + // an error will be returned. + // + // JSON Configuration Syntax can be used and is assumed whenever Config + // contains valid JSON. + // + // Only one of Config, ConfigDirectory or ConfigFile can be set + // otherwise an error will be returned. + Config string + + // ConfigDirectory is a function which returns a function that + // accepts config.TestStepProviderConfig and returns a string + // representing a directory that contains Terraform + // configuration files. + // + // There are helper functions in the [config] package that can be used, + // such as: + // + // - [config.StaticDirectory] + // - [config.TestNameDirectory] + // - [config.TestStepDirectory] + // + // When running Terraform operations for the test, Terraform will + // be executed with copies of the files of this directory as its + // working directory. Only one of Config, ConfigDirectory or + // ConfigFile can be set otherwise an error will be returned. + ConfigDirectory config.TestStepConfigFunc + + // ConfigFile is a function which returns a function that + // accepts config.TestStepProviderConfig and returns a string + // representing a file that contains Terraform configuration. + // + // There are helper functions in the [config] package that can be used, + // such as: + // + // - [config.StaticFile] + // - [config.TestNameFile] + // - [config.TestStepFile] + // + // When running Terraform operations for the test, Terraform will + // be executed with a copy of the file as its working directory. + // Only one of Config, ConfigDirectory or ConfigFile can be set + // otherwise an error will be returned. + ConfigFile config.TestStepConfigFunc + + // ImportStateConfigExact indicates that the test framework should use the exact + // content of the Config, ConfigFile, or ConfigDirectory inputs and should + // not modify it at test run time. + // + // The default is false. At test run time, the test framework will generate + // specific kinds of configuration, such as import blocks, and append them + // to the given Config, ConfigFile, or ConfigDirectory inputs. Using this + // default improves test readability and removes duplication of setup. + ImportStateConfigExact bool + + // ConfigVariables is a map defining variables for use in conjunction + // with Terraform configuration. If this map is populated then it + // will be used to assemble an *.auto.tfvars.json which will be + // written into the working directory. Any variables that are + // defined within the Terraform configuration that have a matching + // variable definition in *.auto.tfvars.json will have their value + // substituted when the acceptance test is executed. + ConfigVariables config.Variables + + // Check is called after the Config is applied. Use this step to + // make your own API calls to check the status of things, and to + // inspect the format of the ResourceState itself. + // + // If an error is returned, the test will fail. In this case, a + // destroy plan will still be attempted. + // + // If this is nil, no check is done on this step. + Check TestCheckFunc + + // Destroy will create a destroy plan if set to true. + Destroy bool + + // ExpectNonEmptyPlan can be set to true for specific types of tests that are + // looking to verify that a diff occurs + ExpectNonEmptyPlan bool + + // ExpectError allows the construction of test cases that we expect to fail + // with an error. The specified regexp must match against the error for the + // test to pass. + // + // This functionality is only intended for provider-controlled error + // messaging. While in certain scenarios this can also catch testing logic + // error messages, those messages are not protected by compatibility + // promises. + ExpectError *regexp.Regexp + + // ConfigPlanChecks allows assertions to be made against the plan file at different points of a Config (apply) test using a plan check. + // Custom plan checks can be created by implementing the [PlanCheck] interface, or by using a PlanCheck implementation from the provided [plancheck] package + // + // [PlanCheck]: https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck#PlanCheck + // [plancheck]: https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck + ConfigPlanChecks ConfigPlanChecks + + // RefreshPlanChecks allows assertions to be made against the plan file at different points of a Refresh test using a plan check. + // Custom plan checks can be created by implementing the [PlanCheck] interface, or by using a PlanCheck implementation from the provided [plancheck] package + // + // [PlanCheck]: https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck#PlanCheck + // [plancheck]: https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck + RefreshPlanChecks RefreshPlanChecks + + // ConfigStateChecks allow assertions to be made against the state file during a Config (apply) test using a state check. + // Custom state checks can be created by implementing the [statecheck.StateCheck] interface, or by using a StateCheck implementation from the provided [statecheck] package. + ConfigStateChecks []statecheck.StateCheck + + // QueryResultChecks allow assertions to be made against a collection of found resources that were returned by a query using a query check. + // Custom query checks can be created by implementing the [querycheck.QueryResultCheck] interface, or by using a QueryResultCheck implementation from the provided [querycheck] package. + QueryResultChecks []QueryResultCheck + + // PlanOnly can be set to only run `plan` with this configuration, and not + // actually apply it. This is useful for ensuring config changes result in + // no-op plans + PlanOnly bool + + // PreventDiskCleanup can be set to true for testing terraform modules which + // require access to disk at runtime. Note that this will leave files in the + // temp folder + PreventDiskCleanup bool + + // PreventPostDestroyRefresh can be set to true for cases where data sources + // are tested alongside real resources + PreventPostDestroyRefresh bool + + // SkipFunc enables skipping the TestStep, based on environment criteria. + // For example, this can prevent running certain steps that may be runtime + // platform or API configuration dependent. + // + // Return true with no error to skip the test step. The error return + // should be used to signify issues that prevented the function from + // completing as expected. + // + // SkipFunc is called after PreConfig but before applying the Config. + SkipFunc func() (bool, error) + + //--------------------------------------------------------------- + // ImportState testing + //--------------------------------------------------------------- + + // ImportState, if true, will test the functionality of ImportState + // by importing the resource with ResourceName (must be set) and the + // ID of that resource. + ImportState bool + + // ImportStateKind controls the method of import that is used in combination with the other import-related fields on the TestStep struct. + // + // - By default, ImportCommandWithID is used, which tests import by using the ID string with the `terraform import` command. This was the original behavior prior to introducing the ImportStateKind field. + // - ImportBlockWithID tests import by using the ID string in an import configuration block with the `terraform plan` command. + // - ImportBlockWithResourceIdentity imports the state using an import configuration block with a resource identity. + ImportStateKind ImportStateKind + + // 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. + ImportStateId string + + // ImportStateIdPrefix is the prefix added in front of ImportStateId. + // This can be useful in complex import cases, where more than one + // attribute needs to be passed on as the Import ID. Mainly in cases + // where the ID is not known, and a known prefix needs to be added to + // the unset ImportStateId field. + ImportStateIdPrefix string + + // ImportStateIdFunc is a function that can be used to dynamically generate + // the ID for the ImportState tests. It is sent the state, which can be + // checked to derive the attributes necessary and generate the string in the + // desired format. + ImportStateIdFunc ImportStateIdFunc + + // ImportStateCheck checks the results of ImportState. It should be + // used to verify that the resulting value of ImportState has the + // proper resources, IDs, and attributes. + // + // Prefer ImportStateVerify over ImportStateCheck, unless the resource + // import explicitly is expected to create multiple resources (not a + // recommended resource implementation) or if attributes are imported with + // syntactically different but semantically/functionally equivalent values + // where special logic is needed. + // + // Terraform versions 1.3 and later can include data source states during + // import, which the testing framework will skip to prevent the need for + // Terraform version specific logic in provider testing. + ImportStateCheck ImportStateCheckFunc + + // ImportPlanChecks allows assertions to be made against the plan file at different points of a plannable import test using a plan check. + // Custom plan checks can be created by implementing the [PlanCheck] interface, or by using a PlanCheck implementation from the provided [plancheck] package + // + // [PlanCheck]: https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck#PlanCheck + // [plancheck]: https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck + ImportPlanChecks ImportPlanChecks + + // ImportStateVerify, if true, will also check that the state values + // that are finally put into the state after import match for all the + // IDs returned by the Import. Note that this checks for strict equality + // and does not respect DiffSuppressFunc or CustomizeDiff. + // + // By default, the prior resource state and import resource state are + // matched by the "id" attribute. If the "id" attribute is not implemented + // or another attribute more uniquely identifies the resource, set the + // ImportStateVerifyIdentifierAttribute field to adjust the attribute for + // matching. + // + // If certain attributes cannot be correctly imported, set the + // ImportStateVerifyIgnore field. + ImportStateVerify bool + + // ImportStateVerifyIdentifierAttribute is the resource attribute for + // matching the prior resource state and import resource state during import + // verification. By default, the "id" attribute is used. + ImportStateVerifyIdentifierAttribute string + + // ImportStateVerifyIgnore is a list of prefixes of fields that should + // not be verified to be equal. These can be set to ephemeral fields or + // fields that can't be refreshed and don't matter. + ImportStateVerifyIgnore []string + + // ImportStatePersist, if true, will update the persisted state with the + // state generated by the import operation (i.e., terraform import). When + // false (default) the state generated by the import operation is discarded + // at the end of the test step that is verifying import behavior. + ImportStatePersist bool + + //--------------------------------------------------------------- + // RefreshState testing + //--------------------------------------------------------------- + + // RefreshState, if true, will test the functionality of `terraform + // refresh` by refreshing the state, running any checks against the + // refreshed state, and running a plan to verify against unexpected plan + // differences. + // + // If the refresh is expected to result in a non-empty plan + // ExpectNonEmptyPlan should be set to true in the same TestStep. + // + // RefreshState cannot be the first TestStep and, it is mutually exclusive + // with ImportState. + RefreshState bool + + // ProviderFactories can be specified for the providers that are valid for + // this TestStep. When providers are specified at the TestStep level, all + // TestStep within a TestCase must declare providers. + // + // This can also be specified at the TestCase level for all TestStep, + // however all provider specifications must be done either at the TestCase + // level or TestStep level, otherwise the testing framework will raise an + // error and fail the test. + // + // These are the providers that can be referenced within the test. Each key + // is an individually addressable provider. Typically you will only pass a + // single value here for the provider you are testing. Aliases are not + // supported by the test framework, so to use multiple provider instances, + // you should add additional copies to this map with unique names. To set + // their configuration, you would reference them similar to the following: + // + // provider "my_factory_key" { + // # ... + // } + // + // resource "my_resource" "mr" { + // provider = my_factory_key + // + // # ... + // } + ProviderFactories map[string]func() (*schema.Provider, error) + + // ProtoV5ProviderFactories serves the same purpose as ProviderFactories, + // but for protocol v5 providers defined using the terraform-plugin-go + // ProviderServer interface. When providers are specified at the TestStep + // level, all TestStep within a TestCase must declare providers. + // + // This can also be specified at the TestCase level for all TestStep, + // however all provider specifications must be done either at the TestCase + // level or TestStep level, otherwise the testing framework will raise an + // error and fail the test. + ProtoV5ProviderFactories map[string]func() (tfprotov5.ProviderServer, error) + + // ProtoV6ProviderFactories serves the same purpose as ProviderFactories, + // but for protocol v6 providers defined using the terraform-plugin-go + // ProviderServer interface. + // The version of Terraform used in acceptance testing must be greater + // than or equal to v0.15.4 to use ProtoV6ProviderFactories. When providers + // are specified at the TestStep level, all TestStep within a TestCase must + // declare providers. + // + // This can also be specified at the TestCase level for all TestStep, + // however all provider specifications must be done either at the TestCase + // level or TestStep level, otherwise the testing framework will raise an + // error and fail the test. + ProtoV6ProviderFactories map[string]func() (tfprotov6.ProviderServer, error) + + // ExternalProviders are providers the TestStep relies on that should + // be downloaded from the registry during init. When providers are + // specified at the TestStep level, all TestStep within a TestCase must + // declare providers. + // + // This can also be specified at the TestCase level for all TestStep, + // however all provider specifications must be done either at the TestCase + // level or TestStep level, otherwise the testing framework will raise an + // error and fail the test. + // + // Outside specifying an earlier version of the provider under test, + // typically for state upgrader testing, this is generally only necessary + // for performing import testing where the prior TestStep configuration + // contained a provider outside the one under test. + ExternalProviders map[string]ExternalProvider + + // If true, the test step will run the query command + Query bool +} + +// ConfigPlanChecks defines the different points in a Config TestStep when plan checks can be run. +type ConfigPlanChecks struct { + // PreApply runs all plan checks in the slice. This occurs before the apply of a Config test is run. This slice cannot be populated + // with TestStep.PlanOnly, as there is no PreApply plan run with that flag set. All errors by plan checks in this slice are aggregated, reported, and will result in a test failure. + PreApply []plancheck.PlanCheck + + // PostApplyPreRefresh runs all plan checks in the slice. This occurs after the apply and before the refresh of a Config test is run. + // All errors by plan checks in this slice are aggregated, reported, and will result in a test failure. + PostApplyPreRefresh []plancheck.PlanCheck + + // PostApplyPostRefresh runs all plan checks in the slice. This occurs after the apply and refresh of a Config test are run. + // All errors by plan checks in this slice are aggregated, reported, and will result in a test failure. + PostApplyPostRefresh []plancheck.PlanCheck +} + +// ImportPlanChecks defines the different points in an Import TestStep when plan checks can be run. +type ImportPlanChecks struct { + // PreApply runs all plan checks in the slice. This occurs after the plan of an Import test is computed. This slice cannot be populated + // with TestStep.PlanOnly, as there is no PreApply plan run with that flag set. All errors by plan checks in this slice are aggregated, reported, and will result in a test failure. + PreApply []plancheck.PlanCheck +} + +// RefreshPlanChecks defines the different points in a Refresh TestStep when plan checks can be run. +type RefreshPlanChecks struct { + // PostRefresh runs all plan checks in the slice. This occurs after the refresh of the Refresh test is run. + // All errors by plan checks in this slice are aggregated, reported, and will result in a test failure. + PostRefresh []plancheck.PlanCheck +} + +// ParallelTest performs an acceptance test on a resource, allowing concurrency +// with other ParallelTest. The number of concurrent tests is controlled by the +// "go test" command -parallel flag. +// +// Tests will fail if they do not properly handle conditions to allow multiple +// tests to occur against the same resource or service (e.g. random naming). +// +// Test() function requirements and documentation also apply to this function. +func ParallelTest(t testing.T, c TestCase) { + t.Helper() + t.Parallel() + Test(t, c) +} + +// Test performs an acceptance test on a resource. +// +// Tests are not run unless an environmental variable "TF_ACC" is +// set to some non-empty value. This is to avoid test cases surprising +// a user by creating real resources. +// +// Use the ParallelTest() function to automatically set (*testing.T).Parallel() +// to enable testing concurrency. Use the UnitTest() function to automatically +// set the TestCase type IsUnitTest field. +// +// This function will automatically find or install Terraform CLI into a +// temporary directory, based on the following behavior: +// +// - If the TF_ACC_TERRAFORM_PATH environment variable is set, that +// Terraform CLI binary is used if found and executable. If not found or +// executable, an error will be returned unless the +// TF_ACC_TERRAFORM_VERSION environment variable is also set. +// - If the TF_ACC_TERRAFORM_VERSION environment variable is set, install +// and use that Terraform CLI version. +// - If both the TF_ACC_TERRAFORM_PATH and TF_ACC_TERRAFORM_VERSION +// environment variables are unset, perform a lookup for the Terraform +// CLI binary based on the operating system PATH. If not found, the +// latest available Terraform CLI binary is installed. +// +// Refer to the Env prefixed constants for additional details about these +// environment variables, and others, that control testing functionality. +func Test(t testing.T, c TestCase) { + t.Helper() + + ctx := context.Background() + ctx = logging.InitTestContext(ctx, t) + + err := c.validate(ctx, t) + + if err != nil { + logging.HelperResourceError(ctx, + "Test validation error", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("Test validation error: %s", err) + } + + // We only run acceptance tests if an env var is set because they're + // slow and generally require some outside configuration. You can opt out + // of this with OverrideEnvVar on individual TestCases. + if os.Getenv(EnvTfAcc) == "" && !c.IsUnitTest { + t.Skip(fmt.Sprintf( + "Acceptance tests skipped unless env '%s' set", + EnvTfAcc)) + return + } + + // Copy any explicitly passed providers to factories, this is for backwards compatibility. + if len(c.Providers) > 0 { + c.ProviderFactories = map[string]func() (*schema.Provider, error){} + + for name, p := range c.Providers { + prov := p + c.ProviderFactories[name] = func() (*schema.Provider, error) { //nolint:unparam // required signature + return prov, nil + } + } + } + + logging.HelperResourceDebug(ctx, "Starting TestCase") + + // Run the PreCheck if we have it. + // This is done after the auto-configure to allow providers + // to override the default auto-configure parameters. + if c.PreCheck != nil { + logging.HelperResourceDebug(ctx, "Calling TestCase PreCheck") + + c.PreCheck() + + logging.HelperResourceDebug(ctx, "Called TestCase PreCheck") + } + + sourceDir, err := os.Getwd() + if err != nil { + t.Fatalf("Error getting working dir: %s", err) + } + helper := plugintest.AutoInitProviderHelper(ctx, sourceDir) + defer func(helper *plugintest.Helper) { + err := helper.Close() + if err != nil { + logging.HelperResourceError(ctx, "Unable to clean up temporary test files", map[string]interface{}{logging.KeyError: err}) + } + }(helper) + + // Run the TerraformVersionChecks if we have it. + // This is done after creating the helper because a working directory is required + // to retrieve the Terraform version. + if c.TerraformVersionChecks != nil { + runTFVersionChecks(ctx, t, helper.TerraformVersion(), c.TerraformVersionChecks) + } + + runNewTest(ctx, t, c, helper) + + logging.HelperResourceDebug(ctx, "Finished TestCase") +} + +// UnitTest is a helper to force the acceptance testing harness to run in the +// normal unit test suite. This should only be used for resource that don't +// have any external dependencies. +// +// Test() function requirements and documentation also apply to this function. +func UnitTest(t testing.T, c TestCase) { + t.Helper() + + c.IsUnitTest = true + Test(t, c) +} + +func testResource(name string, state *terraform.State) (*terraform.ResourceState, error) { + for _, m := range state.Modules { + if len(m.Resources) > 0 { + if v, ok := m.Resources[name]; ok { + return v, nil + } + } + } + + return nil, fmt.Errorf( + "Resource specified by ResourceName couldn't be found: %s", name) +} + +// ComposeTestCheckFunc lets you compose multiple TestCheckFuncs into +// a single TestCheckFunc. +// +// As a user testing their provider, this lets you decompose your checks +// into smaller pieces more easily. +// +// ComposeTestCheckFunc returns immediately on the first TestCheckFunc error. +// To aggregrate all errors, use ComposeAggregateTestCheckFunc instead. +func ComposeTestCheckFunc(fs ...TestCheckFunc) TestCheckFunc { + return func(s *terraform.State) error { + for i, f := range fs { + if err := f(s); err != nil { + return fmt.Errorf("Check %d/%d error: %w", i+1, len(fs), err) + } + } + + return nil + } +} + +// ComposeAggregateTestCheckFunc lets you compose multiple TestCheckFuncs into +// a single TestCheckFunc. +// +// As a user testing their provider, this lets you decompose your checks +// into smaller pieces more easily. +// +// Unlike ComposeTestCheckFunc, ComposeAggergateTestCheckFunc runs _all_ of the +// TestCheckFuncs and aggregates failures. +func ComposeAggregateTestCheckFunc(fs ...TestCheckFunc) TestCheckFunc { + return func(s *terraform.State) error { + var result []error + + for i, f := range fs { + if err := f(s); err != nil { + result = append(result, fmt.Errorf("Check %d/%d error: %w", i+1, len(fs), err)) + } + } + + return errors.Join(result...) + } +} + +// TestCheckResourceAttrSet ensures any value exists in the state for the +// given name and key combination. The opposite of this TestCheckFunc is +// TestCheckNoResourceAttr. State value checking is only recommended for +// testing Computed attributes and attribute defaults. +// +// Use this as a last resort when a more specific TestCheckFunc cannot be +// implemented, such as: +// +// - TestCheckResourceAttr: Equality checking of non-TypeSet state value. +// - TestCheckResourceAttrPair: Equality checking of non-TypeSet state +// value, based on another state value. +// - TestCheckTypeSet*: Equality checking of TypeSet state values. +// - TestMatchResourceAttr: Regular expression checking of non-TypeSet +// state value. +// - TestMatchTypeSet*: Regular expression checking on TypeSet state values. +// +// For managed resources, the name parameter is combination of the resource +// type, a period (.), and the name label. The name for the below example +// configuration would be "myprovider_thing.example". +// +// resource "myprovider_thing" "example" { ... } +// +// For data sources, the name parameter is a combination of the keyword "data", +// a period (.), the data source type, a period (.), and the name label. The +// name for the below example configuration would be +// "data.myprovider_thing.example". +// +// data "myprovider_thing" "example" { ... } +// +// The key parameter is an attribute path in Terraform CLI 0.11 and earlier +// "flatmap" syntax. Keys start with the attribute name of a top-level +// attribute. Use the following special key syntax to inspect underlying +// values of a list or map attribute: +// +// - .{NUMBER}: List value at index, e.g. .0 to inspect the first element +// - .{KEY}: Map value at key, e.g. .example to inspect the example key +// value +// +// While it is possible to check nested attributes under list and map +// attributes using the special key syntax, checking a list, map, or set +// attribute directly is not supported. Use TestCheckResourceAttr with +// the special .# or .% key syntax for those situations instead. +// +// An experimental interface exists to potentially replace the +// TestCheckResourceAttrSet functionality in the future and feedback +// would be appreciated. This example performs the same check as +// TestCheckResourceAttrSet with that experimental interface, by +// using [ExpectKnownValue] with [knownvalue.NotNull]: +// +// package example_test +// +// import ( +// "testing" +// +// "github.com/hashicorp/terraform-plugin-testing/helper/resource" +// "github.com/hashicorp/terraform-plugin-testing/knownvalue" +// "github.com/hashicorp/terraform-plugin-testing/statecheck" +// "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +// ) +// +// func TestExpectKnownValue_CheckState_AttributeFound(t *testing.T) { +// t.Parallel() +// +// resource.Test(t, resource.TestCase{ +// // Provider definition omitted. +// Steps: []resource.TestStep{ +// { +// // Example resource containing a computed attribute named "computed_attribute" +// Config: `resource "test_resource" "one" {}`, +// ConfigStateChecks: []statecheck.StateCheck{ +// statecheck.ExpectKnownValue( +// "test_resource.one", +// tfjsonpath.New("computed_attribute"), +// knownvalue.NotNull(), +// ), +// }, +// }, +// }, +// }) +// } +func TestCheckResourceAttrSet(name, key string) TestCheckFunc { + return checkIfIndexesIntoTypeSet(key, func(s *terraform.State) error { + is, err := primaryInstanceState(s, name) + if err != nil { + return err + } + + return testCheckResourceAttrSet(is, name, key) + }) +} + +// TestCheckModuleResourceAttrSet - as per TestCheckResourceAttrSet but with +// support for non-root modules +// +// Deprecated: This functionality is deprecated without replacement. The +// terraform-plugin-testing Go module is intended for provider testing, which +// should always be possible within the root module of a configuration. This +// functionality is a carryover of when this code was used within Terraform +// core to test both providers and modules. Modern testing implementations to +// verify interactions between modules should be tested in Terraform core or +// using tooling outside this Go module. +func TestCheckModuleResourceAttrSet(mp []string, name string, key string) TestCheckFunc { + mpt := addrs.Module(mp).UnkeyedInstanceShim() + return checkIfIndexesIntoTypeSet(key, func(s *terraform.State) error { + is, err := modulePathPrimaryInstanceState(s, mpt, name) + if err != nil { + return err + } + + return testCheckResourceAttrSet(is, name, key) + }) +} + +func testCheckResourceAttrSet(is *terraform.InstanceState, name string, key string) error { + val, ok := is.Attributes[key] + + if ok && val != "" { + return nil + } + + if _, ok := is.Attributes[key+".#"]; ok { + return fmt.Errorf( + "%s: list or set attribute '%s' must be checked by element count key (%s) or element value keys (e.g. %s). Set element value checks should use TestCheckTypeSet functions instead.", + name, + key, + key+".#", + key+".0", + ) + } + + if _, ok := is.Attributes[key+".%"]; ok { + return fmt.Errorf( + "%s: map attribute '%s' must be checked by element count key (%s) or element value keys (e.g. %s).", + name, + key, + key+".%", + key+".examplekey", + ) + } + + return fmt.Errorf("%s: Attribute '%s' expected to be set", name, key) +} + +// TestCheckResourceAttr ensures a specific value is stored in state for the +// given name and key combination. State value checking is only recommended for +// testing Computed attributes and attribute defaults. +// +// For managed resources, the name parameter is combination of the resource +// type, a period (.), and the name label. The name for the below example +// configuration would be "myprovider_thing.example". +// +// resource "myprovider_thing" "example" { ... } +// +// For data sources, the name parameter is a combination of the keyword "data", +// a period (.), the data source type, a period (.), and the name label. The +// name for the below example configuration would be +// "data.myprovider_thing.example". +// +// data "myprovider_thing" "example" { ... } +// +// The key parameter is an attribute path in Terraform CLI 0.11 and earlier +// "flatmap" syntax. Keys start with the attribute name of a top-level +// attribute. Use the following special key syntax to inspect list, map, and +// set attributes: +// +// - .{NUMBER}: List value at index, e.g. .0 to inspect the first element. +// Use the TestCheckTypeSet* and TestMatchTypeSet* functions instead +// for sets. +// - .{KEY}: Map value at key, e.g. .example to inspect the example key +// value. +// - .#: Number of elements in list or set. +// - .%: Number of elements in map. +// +// The value parameter is the stringified data to check at the given key. Use +// the following attribute type rules to set the value: +// +// - Boolean: "false" or "true". +// - Float/Integer: Stringified number, such as "1.2" or "123". +// - String: No conversion necessary. +// +// An experimental interface exists to potentially replace the +// TestCheckResourceAttr functionality in the future and feedback +// would be appreciated. This example performs the same check as +// TestCheckResourceAttr with that experimental interface, by +// using [statecheck.ExpectKnownValue]: +// +// package example_test +// +// import ( +// "testing" +// +// "github.com/hashicorp/terraform-plugin-testing/helper/resource" +// "github.com/hashicorp/terraform-plugin-testing/knownvalue" +// "github.com/hashicorp/terraform-plugin-testing/statecheck" +// "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +// ) +// +// func TestExpectKnownValue_CheckState_Bool(t *testing.T) { +// t.Parallel() +// +// resource.Test(t, resource.TestCase{ +// // Provider definition omitted. +// Steps: []resource.TestStep{ +// { +// // Example resource containing a computed boolean attribute named "computed_attribute" +// Config: `resource "test_resource" "one" {}`, +// ConfigStateChecks: []statecheck.StateCheck{ +// statecheck.ExpectKnownValue( +// "test_resource.one", +// tfjsonpath.New("computed_attribute"), +// knownvalue.Bool(true), +// ), +// }, +// }, +// }, +// }) +// } +func TestCheckResourceAttr(name, key, value string) TestCheckFunc { + return checkIfIndexesIntoTypeSet(key, func(s *terraform.State) error { + is, err := primaryInstanceState(s, name) + if err != nil { + return err + } + + return testCheckResourceAttr(is, name, key, value) + }) +} + +// TestCheckModuleResourceAttr - as per TestCheckResourceAttr but with +// support for non-root modules +// +// Deprecated: This functionality is deprecated without replacement. The +// terraform-plugin-testing Go module is intended for provider testing, which +// should always be possible within the root module of a configuration. This +// functionality is a carryover of when this code was used within Terraform +// core to test both providers and modules. Modern testing implementations to +// verify interactions between modules should be tested in Terraform core or +// using tooling outside this Go module. +func TestCheckModuleResourceAttr(mp []string, name string, key string, value string) TestCheckFunc { + mpt := addrs.Module(mp).UnkeyedInstanceShim() + return checkIfIndexesIntoTypeSet(key, func(s *terraform.State) error { + is, err := modulePathPrimaryInstanceState(s, mpt, name) + if err != nil { + return err + } + + return testCheckResourceAttr(is, name, key, value) + }) +} + +func testCheckResourceAttr(is *terraform.InstanceState, name string, key string, value string) error { + v, ok := is.Attributes[key] + + if !ok { + // Empty containers may be elided from the state. + // If the intent here is to check for an empty container, allow the key to + // also be non-existent. + if value == "0" && (strings.HasSuffix(key, ".#") || strings.HasSuffix(key, ".%")) { + return nil + } + + if _, ok := is.Attributes[key+".#"]; ok { + return fmt.Errorf( + "%s: list or set attribute '%s' must be checked by element count key (%s) or element value keys (e.g. %s). Set element value checks should use TestCheckTypeSet functions instead.", + name, + key, + key+".#", + key+".0", + ) + } + + if _, ok := is.Attributes[key+".%"]; ok { + return fmt.Errorf( + "%s: map attribute '%s' must be checked by element count key (%s) or element value keys (e.g. %s).", + name, + key, + key+".%", + key+".examplekey", + ) + } + + return fmt.Errorf("%s: Attribute '%s' not found", name, key) + } + + if v != value { + return fmt.Errorf( + "%s: Attribute '%s' expected %#v, got %#v", + name, + key, + value, + v) + } + + return nil +} + +// CheckResourceAttrWithFunc is the callback type used to apply a custom checking logic +// when using TestCheckResourceAttrWith and a value is found for the given name and key. +// +// When this function returns an error, TestCheckResourceAttrWith will fail the check. +// +// An experimental interface exists to potentially replace the +// CheckResourceAttrWithFunc functionality in the future and feedback +// would be appreciated. This example performs the same check as +// TestCheckResourceAttrWith with that experimental interface, by +// using [statecheck.ExpectKnownValue] in combination with +// [knownvalue.StringRegexp]: +// +// package example_test +// +// import ( +// "testing" +// +// "github.com/hashicorp/terraform-plugin-testing/helper/resource" +// "github.com/hashicorp/terraform-plugin-testing/statecheck" +// "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +// ) +// +// func TestExpectKnownValue_CheckState_String_Custom(t *testing.T) { +// t.Parallel() +// +// resource.Test(t, resource.TestCase{ +// // Provider definition omitted. +// Steps: []resource.TestStep{ +// { +// // Example resource containing a computed string attribute named "computed_attribute" +// Config: `resource "test_resource" "one" {}`, +// ConfigStateChecks: []statecheck.StateCheck{ +// statecheck.ExpectKnownValue( +// "test_resource.one", +// tfjsonpath.New("computed_attribute"), +// knownvalue.StringRegexp(regexp.MustCompile("str")), +// }, +// }, +// }, +// }) +// } +type CheckResourceAttrWithFunc func(value string) error + +// TestCheckResourceAttrWith ensures a value stored in state for the +// given name and key combination, is checked against a custom logic. +// State value checking is only recommended for testing Computed attributes +// and attribute defaults. +// +// For managed resources, the name parameter is combination of the resource +// type, a period (.), and the name label. The name for the below example +// configuration would be "myprovider_thing.example". +// +// resource "myprovider_thing" "example" { ... } +// +// For data sources, the name parameter is a combination of the keyword "data", +// a period (.), the data source type, a period (.), and the name label. The +// name for the below example configuration would be +// "data.myprovider_thing.example". +// +// data "myprovider_thing" "example" { ... } +// +// The key parameter is an attribute path in Terraform CLI 0.11 and earlier +// "flatmap" syntax. Keys start with the attribute name of a top-level +// attribute. Use the following special key syntax to inspect list, map, and +// set attributes: +// +// - .{NUMBER}: List value at index, e.g. .0 to inspect the first element. +// Use the TestCheckTypeSet* and TestMatchTypeSet* functions instead +// for sets. +// - .{KEY}: Map value at key, e.g. .example to inspect the example key +// value. +// - .#: Number of elements in list or set. +// - .%: Number of elements in map. +// +// The checkValueFunc parameter is a CheckResourceAttrWithFunc, +// and it's provided with the attribute value to apply a custom checking logic, +// if it was found in the state. The function must return an error for the +// check to fail, or `nil` to succeed. +// +// An experimental interface exists to potentially replace the +// TestCheckResourceAttrWith functionality in the future and feedback +// would be appreciated. This example performs the same check as +// TestCheckResourceAttrWith with that experimental interface, by +// using [statecheck.ExpectKnownValue] in combination with +// [knownvalue.StringRegexp]: +// +// package example_test +// +// import ( +// "testing" +// +// "github.com/hashicorp/terraform-plugin-testing/helper/resource" +// "github.com/hashicorp/terraform-plugin-testing/statecheck" +// "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +// ) +// +// func TestExpectKnownValue_CheckState_String_Custom(t *testing.T) { +// t.Parallel() +// +// resource.Test(t, resource.TestCase{ +// // Provider definition omitted. +// Steps: []resource.TestStep{ +// { +// // Example resource containing a computed string attribute named "computed_attribute" +// Config: `resource "test_resource" "one" {}`, +// ConfigStateChecks: []statecheck.StateCheck{ +// statecheck.ExpectKnownValue( +// "test_resource.one", +// tfjsonpath.New("computed_attribute"), +// knownvalue.StringRegexp(regexp.MustCompile("str")), +// }, +// }, +// }, +// }) +// } +func TestCheckResourceAttrWith(name, key string, checkValueFunc CheckResourceAttrWithFunc) TestCheckFunc { + return checkIfIndexesIntoTypeSet(key, func(s *terraform.State) error { + is, err := primaryInstanceState(s, name) + if err != nil { + return err + } + + err = testCheckResourceAttrSet(is, name, key) + if err != nil { + return err + } + + err = checkValueFunc(is.Attributes[key]) + if err != nil { + return fmt.Errorf("%s: Attribute %q value: %w", name, key, err) + } + + return nil + }) +} + +// TestCheckNoResourceAttr ensures no value exists in the state for the +// given name and key combination. The opposite of this TestCheckFunc is +// TestCheckResourceAttrSet. State value checking is only recommended for +// testing Computed attributes and attribute defaults. +// +// For managed resources, the name parameter is combination of the resource +// type, a period (.), and the name label. The name for the below example +// configuration would be "myprovider_thing.example". +// +// resource "myprovider_thing" "example" { ... } +// +// For data sources, the name parameter is a combination of the keyword "data", +// a period (.), the data source type, a period (.), and the name label. The +// name for the below example configuration would be +// "data.myprovider_thing.example". +// +// data "myprovider_thing" "example" { ... } +// +// The key parameter is an attribute path in Terraform CLI 0.11 and earlier +// "flatmap" syntax. Keys start with the attribute name of a top-level +// attribute. Use the following special key syntax to inspect underlying +// values of a list or map attribute: +// +// - .{NUMBER}: List value at index, e.g. .0 to inspect the first element. +// - .{KEY}: Map value at key, e.g. .example to inspect the example key +// value. +// +// While it is possible to check nested attributes under list and map +// attributes using the special key syntax, checking a list, map, or set +// attribute directly is not supported. Use TestCheckResourceAttr with +// the special .# or .% key syntax for those situations instead. +// +// An experimental interface exists to potentially replace the +// TestCheckNoResourceAttr functionality in the future and feedback +// would be appreciated. This example performs the same check as +// TestCheckNoResourceAttr with that experimental interface, by +// using [statecheck.ExpectKnownValue] with [knownvalue.Null]: +// +// package example_test +// +// import ( +// "testing" +// +// "github.com/hashicorp/terraform-plugin-testing/helper/resource" +// "github.com/hashicorp/terraform-plugin-testing/statecheck" +// "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +// ) +// +// func TestExpectKnownValue_CheckState_AttributeNull(t *testing.T) { +// t.Parallel() +// +// resource.Test(t, resource.TestCase{ +// // Provider definition omitted. +// Steps: []resource.TestStep{ +// { +// // Example resource containing a computed attribute named "computed_attribute" that has a null value +// Config: `resource "test_resource" "one" {}`, +// ConfigStateChecks: []statecheck.StateCheck{ +// statecheck.ExpectKnownValue( +// "test_resource.one", +// tfjsonpath.New("computed_attribute"), +// knownvalue.Null(), +// ), +// }, +// }, +// }, +// }) +// } +func TestCheckNoResourceAttr(name, key string) TestCheckFunc { + return checkIfIndexesIntoTypeSet(key, func(s *terraform.State) error { + is, err := primaryInstanceState(s, name) + if err != nil { + return err + } + + return testCheckNoResourceAttr(is, name, key) + }) +} + +// TestCheckModuleNoResourceAttr - as per TestCheckNoResourceAttr but with +// support for non-root modules +// +// Deprecated: This functionality is deprecated without replacement. The +// terraform-plugin-testing Go module is intended for provider testing, which +// should always be possible within the root module of a configuration. This +// functionality is a carryover of when this code was used within Terraform +// core to test both providers and modules. Modern testing implementations to +// verify interactions between modules should be tested in Terraform core or +// using tooling outside this Go module. +func TestCheckModuleNoResourceAttr(mp []string, name string, key string) TestCheckFunc { + mpt := addrs.Module(mp).UnkeyedInstanceShim() + return checkIfIndexesIntoTypeSet(key, func(s *terraform.State) error { + is, err := modulePathPrimaryInstanceState(s, mpt, name) + if err != nil { + return err + } + + return testCheckNoResourceAttr(is, name, key) + }) +} + +func testCheckNoResourceAttr(is *terraform.InstanceState, name string, key string) error { + v, ok := is.Attributes[key] + + // Empty containers may sometimes be included in the state. + // If the intent here is to check for an empty container, allow the value to + // also be "0". + if v == "0" && (strings.HasSuffix(key, ".#") || strings.HasSuffix(key, ".%")) { + return nil + } + + if ok { + return fmt.Errorf("%s: Attribute '%s' found when not expected", name, key) + } + + if _, ok := is.Attributes[key+".#"]; ok { + return fmt.Errorf( + "%s: list or set attribute '%s' must be checked by element count key (%s) or element value keys (e.g. %s). Set element value checks should use TestCheckTypeSet functions instead.", + name, + key, + key+".#", + key+".0", + ) + } + + if _, ok := is.Attributes[key+".%"]; ok { + return fmt.Errorf( + "%s: map attribute '%s' must be checked by element count key (%s) or element value keys (e.g. %s).", + name, + key, + key+".%", + key+".examplekey", + ) + } + + return nil +} + +// TestMatchResourceAttr ensures a value matching a regular expression is +// stored in state for the given name and key combination. State value checking +// is only recommended for testing Computed attributes and attribute defaults. +// +// For managed resources, the name parameter is combination of the resource +// type, a period (.), and the name label. The name for the below example +// configuration would be "myprovider_thing.example". +// +// resource "myprovider_thing" "example" { ... } +// +// For data sources, the name parameter is a combination of the keyword "data", +// a period (.), the data source type, a period (.), and the name label. The +// name for the below example configuration would be +// "data.myprovider_thing.example". +// +// data "myprovider_thing" "example" { ... } +// +// The key parameter is an attribute path in Terraform CLI 0.11 and earlier +// "flatmap" syntax. Keys start with the attribute name of a top-level +// attribute. Use the following special key syntax to inspect list, map, and +// set attributes: +// +// - .{NUMBER}: List value at index, e.g. .0 to inspect the first element. +// Use the TestCheckTypeSet* and TestMatchTypeSet* functions instead +// for sets. +// - .{KEY}: Map value at key, e.g. .example to inspect the example key +// value. +// - .#: Number of elements in list or set. +// - .%: Number of elements in map. +// +// The value parameter is a compiled regular expression. A typical pattern is +// using the regexp.MustCompile() function, which will automatically ensure the +// regular expression is supported by the Go regular expression handlers during +// compilation. +// +// An experimental interface exists to potentially replace the +// TestMatchResourceAttr functionality in the future and feedback +// would be appreciated. This example performs the same check as +// TestMatchResourceAttr with that experimental interface, by +// using [statecheck.ExpectKnownValue] in combination with +// [knownvalue.StringRegexp]: +// +// package example_test +// +// import ( +// "testing" +// +// "github.com/hashicorp/terraform-plugin-testing/helper/resource" +// "github.com/hashicorp/terraform-plugin-testing/statecheck" +// "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +// ) +// +// func TestExpectKnownValue_CheckState_String_Custom(t *testing.T) { +// t.Parallel() +// +// resource.Test(t, resource.TestCase{ +// // Provider definition omitted. +// Steps: []resource.TestStep{ +// { +// // Example resource containing a computed string attribute named "computed_attribute" +// Config: `resource "test_resource" "one" {}`, +// ConfigStateChecks: []statecheck.StateCheck{ +// statecheck.ExpectKnownValue( +// "test_resource.one", +// tfjsonpath.New("computed_attribute"), +// knownvalue.StringRegexp(regexp.MustCompile("str")), +// }, +// }, +// }, +// }) +// } +func TestMatchResourceAttr(name, key string, r *regexp.Regexp) TestCheckFunc { + return checkIfIndexesIntoTypeSet(key, func(s *terraform.State) error { + is, err := primaryInstanceState(s, name) + if err != nil { + return err + } + + return testMatchResourceAttr(is, name, key, r) + }) +} + +// TestModuleMatchResourceAttr - as per TestMatchResourceAttr but with +// support for non-root modules +// +// Deprecated: This functionality is deprecated without replacement. The +// terraform-plugin-testing Go module is intended for provider testing, which +// should always be possible within the root module of a configuration. This +// functionality is a carryover of when this code was used within Terraform +// core to test both providers and modules. Modern testing implementations to +// verify interactions between modules should be tested in Terraform core or +// using tooling outside this Go module. +func TestModuleMatchResourceAttr(mp []string, name string, key string, r *regexp.Regexp) TestCheckFunc { + mpt := addrs.Module(mp).UnkeyedInstanceShim() + return checkIfIndexesIntoTypeSet(key, func(s *terraform.State) error { + is, err := modulePathPrimaryInstanceState(s, mpt, name) + if err != nil { + return err + } + + return testMatchResourceAttr(is, name, key, r) + }) +} + +func testMatchResourceAttr(is *terraform.InstanceState, name string, key string, r *regexp.Regexp) error { + if !r.MatchString(is.Attributes[key]) { + return fmt.Errorf( + "%s: Attribute '%s' didn't match %q, got %#v", + name, + key, + r.String(), + is.Attributes[key]) + } + + return nil +} + +// TestCheckResourceAttrPtr is like TestCheckResourceAttr except the +// value is a pointer so that it can be updated while the test is running. +// It will only be dereferenced at the point this step is run. +// +// Refer to the TestCheckResourceAttr documentation for more information about +// setting the name, key, and value parameters. +func TestCheckResourceAttrPtr(name string, key string, value *string) TestCheckFunc { + return func(s *terraform.State) error { + return TestCheckResourceAttr(name, key, *value)(s) + } +} + +// TestCheckModuleResourceAttrPtr - as per TestCheckResourceAttrPtr but with +// support for non-root modules +// +// Deprecated: This functionality is deprecated without replacement. The +// terraform-plugin-testing Go module is intended for provider testing, which +// should always be possible within the root module of a configuration. This +// functionality is a carryover of when this code was used within Terraform +// core to test both providers and modules. Modern testing implementations to +// verify interactions between modules should be tested in Terraform core or +// using tooling outside this Go module. +func TestCheckModuleResourceAttrPtr(mp []string, name string, key string, value *string) TestCheckFunc { + return func(s *terraform.State) error { + return TestCheckModuleResourceAttr(mp, name, key, *value)(s) + } +} + +// TestCheckResourceAttrPair ensures value equality in state between the first +// given name and key combination and the second name and key combination. +// State value checking is only recommended for testing Computed attributes +// and attribute defaults. +// +// For managed resources, the name parameter is combination of the resource +// type, a period (.), and the name label. The name for the below example +// configuration would be "myprovider_thing.example". +// +// resource "myprovider_thing" "example" { ... } +// +// For data sources, the name parameter is a combination of the keyword "data", +// a period (.), the data source type, a period (.), and the name label. The +// name for the below example configuration would be +// "data.myprovider_thing.example". +// +// data "myprovider_thing" "example" { ... } +// +// The first and second names may use any combination of managed resources +// and/or data sources. +// +// The key parameter is an attribute path in Terraform CLI 0.11 and earlier +// "flatmap" syntax. Keys start with the attribute name of a top-level +// attribute. Use the following special key syntax to inspect list, map, and +// set attributes: +// +// - .{NUMBER}: List value at index, e.g. .0 to inspect the first element. +// Use the TestCheckTypeSet* and TestMatchTypeSet* functions instead +// for sets. +// - .{KEY}: Map value at key, e.g. .example to inspect the example key +// value. +// - .#: Number of elements in list or set. +// - .%: Number of elements in map. +func TestCheckResourceAttrPair(nameFirst, keyFirst, nameSecond, keySecond string) TestCheckFunc { + return checkIfIndexesIntoTypeSetPair(keyFirst, keySecond, func(s *terraform.State) error { + isFirst, err := primaryInstanceState(s, nameFirst) + if err != nil { + return err + } + + isSecond, err := primaryInstanceState(s, nameSecond) + if err != nil { + return err + } + + return testCheckResourceAttrPair(isFirst, nameFirst, keyFirst, isSecond, nameSecond, keySecond) + }) +} + +// TestCheckModuleResourceAttrPair - as per TestCheckResourceAttrPair but with +// support for non-root modules +// +// Deprecated: This functionality is deprecated without replacement. The +// terraform-plugin-testing Go module is intended for provider testing, which +// should always be possible within the root module of a configuration. This +// functionality is a carryover of when this code was used within Terraform +// core to test both providers and modules. Modern testing implementations to +// verify interactions between modules should be tested in Terraform core or +// using tooling outside this Go module. +func TestCheckModuleResourceAttrPair(mpFirst []string, nameFirst string, keyFirst string, mpSecond []string, nameSecond string, keySecond string) TestCheckFunc { + mptFirst := addrs.Module(mpFirst).UnkeyedInstanceShim() + mptSecond := addrs.Module(mpSecond).UnkeyedInstanceShim() + return checkIfIndexesIntoTypeSetPair(keyFirst, keySecond, func(s *terraform.State) error { + isFirst, err := modulePathPrimaryInstanceState(s, mptFirst, nameFirst) + if err != nil { + return err + } + + isSecond, err := modulePathPrimaryInstanceState(s, mptSecond, nameSecond) + if err != nil { + return err + } + + return testCheckResourceAttrPair(isFirst, nameFirst, keyFirst, isSecond, nameSecond, keySecond) + }) +} + +func testCheckResourceAttrPair(isFirst *terraform.InstanceState, nameFirst string, keyFirst string, isSecond *terraform.InstanceState, nameSecond string, keySecond string) error { + if nameFirst == nameSecond && keyFirst == keySecond { + return fmt.Errorf( + "comparing self: resource %s attribute %s", + nameFirst, + keyFirst, + ) + } + + vFirst, okFirst := isFirst.Attributes[keyFirst] + vSecond, okSecond := isSecond.Attributes[keySecond] + + // Container count values of 0 should not be relied upon, and not reliably + // maintained by helper/schema. For the purpose of tests, consider unset and + // 0 to be equal. + if len(keyFirst) > 2 && len(keySecond) > 2 && keyFirst[len(keyFirst)-2:] == keySecond[len(keySecond)-2:] && + (strings.HasSuffix(keyFirst, ".#") || strings.HasSuffix(keyFirst, ".%")) { + // they have the same suffix, and it is a collection count key. + if vFirst == "0" || vFirst == "" { + okFirst = false + } + if vSecond == "0" || vSecond == "" { + okSecond = false + } + } + + if okFirst != okSecond { + if !okFirst { + return fmt.Errorf("%s: Attribute %q not set, but %q is set in %s as %q", nameFirst, keyFirst, keySecond, nameSecond, vSecond) + } + return fmt.Errorf("%s: Attribute %q is %q, but %q is not set in %s", nameFirst, keyFirst, vFirst, keySecond, nameSecond) + } + if !(okFirst || okSecond) { + // If they both don't exist then they are equally unset, so that's okay. + return nil + } + + if vFirst != vSecond { + return fmt.Errorf( + "%s: Attribute '%s' expected %#v, got %#v", + nameFirst, + keyFirst, + vSecond, + vFirst) + } + + return nil +} + +// TestCheckOutput checks an output in the Terraform configuration +// +// An experimental interface exists to potentially replace the +// TestCheckOutput functionality in the future and feedback +// would be appreciated. This example performs the same check as +// TestCheckOutput with that experimental interface, by +// using [statecheck.ExpectKnownOutputValue]: +// +// package example_test +// +// import ( +// "testing" +// +// "github.com/hashicorp/terraform-plugin-testing/helper/resource" +// "github.com/hashicorp/terraform-plugin-testing/knownvalue" +// "github.com/hashicorp/terraform-plugin-testing/statecheck" +// "github.com/hashicorp/terraform-plugin-testing/tfversion" +// ) +// +// func TestExpectKnownOutputValue_CheckState_Bool(t *testing.T) { +// t.Parallel() +// +// resource.Test(t, resource.TestCase{ +// TerraformVersionChecks: []tfversion.TerraformVersionCheck{ +// tfversion.SkipBelow(tfversion.Version1_8_0), +// }, +// // Provider definition omitted. +// Steps: []resource.TestStep{ +// { +// // Example provider containing a provider-defined function named "bool" +// Config: `output "test" { +// value = provider::example::bool(true) +// }`, +// ConfigStateChecks: []statecheck.StateCheck{ +// statecheck.ExpectKnownOutputValue("test", knownvalue.Bool(true)), +// }, +// }, +// }, +// }) +// } +// +// An experimental interface exists to potentially replace the +// TestCheckOutput functionality in the future and feedback +// would be appreciated. This example performs the same check as +// TestCheckOutput with that experimental interface, by using +// [statecheck.ExpectKnownOutputValueAtPath]: +// +// package example_test +// +// import ( +// "testing" +// +// "github.com/hashicorp/terraform-plugin-testing/helper/resource" +// "github.com/hashicorp/terraform-plugin-testing/knownvalue" +// "github.com/hashicorp/terraform-plugin-testing/statecheck" +// "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +// ) +// +// func TestExpectKnownOutputValueAtPath_CheckState_Bool(t *testing.T) { +// t.Parallel() +// +// resource.Test(t, resource.TestCase{ +// // Provider definition omitted. +// Steps: []resource.TestStep{ +// { +// // Example resource containing a computed boolean attribute named "computed_attribute" +// Config: `resource "test_resource" "one" {} +// +// // Generally, it is not necessary to use an output to test a resource attribute, +// // the resource attribute should be tested directly instead, by inspecting the +// // value of the resource attribute. For instance: +// // +// // ConfigStateChecks: []statecheck.StateCheck{ +// // statecheck.ExpectKnownValue( +// // "test_resource.one", +// // tfjsonpath.New("computed_attribute"), +// // knownvalue.Bool(true), +// // ), +// // }, +// // +// // This is only shown as an example. +// output test_resource_one_output { +// value = test_resource.one +// }`, +// ConfigStateChecks: []statecheck.StateCheck{ +// statecheck.ExpectKnownOutputValueAtPath( +// "test_resource_one_output", +// tfjsonpath.New("computed_attribute"), +// knownvalue.Bool(true), +// ), +// }, +// }, +// }, +// }) +// } +func TestCheckOutput(name, value string) TestCheckFunc { + return func(s *terraform.State) error { + ms := s.RootModule() + rs, ok := ms.Outputs[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + if rs.Value != value { + return fmt.Errorf( + "Output '%s': expected %#v, got %#v", + name, + value, + rs) + } + + return nil + } +} + +// TestMatchOutput ensures a value matching a regular expression is +// stored in state for the given name. State value checking is only +// recommended for testing Computed attributes and attribute defaults. +// +// An experimental interface exists to potentially replace the +// TestMatchOutput functionality in the future and feedback +// would be appreciated. This example performs the same check as +// TestMatchOutput with that experimental interface, by using +// [statecheck.ExpectKnownOutputValueAtPath] in combination with +// [knownvalue.StringRegexp]: +// +// package example_test +// +// import ( +// "testing" +// +// "github.com/hashicorp/terraform-plugin-testing/helper/resource" +// "github.com/hashicorp/terraform-plugin-testing/statecheck" +// "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +// ) +// +// func TestExpectKnownOutputValueAtPath_CheckState_String_Custom(t *testing.T) { +// t.Parallel() +// +// resource.Test(t, resource.TestCase{ +// // Provider definition omitted. +// Steps: []resource.TestStep{ +// { +// // Example resource containing a computed string attribute named "computed_attribute" +// Config: `resource "test_resource" "one" {} +// +// // Generally, it is not necessary to use an output to test a resource attribute, +// // the resource attribute should be tested directly instead, by inspecting the +// // value of the resource attribute. For instance: +// // +// // ConfigStateChecks: []statecheck.StateCheck{ +// // statecheck.ExpectKnownValue( +// // "test_resource.one", +// // tfjsonpath.New("computed_attribute"), +// // knownvalue.StringRegexp(regexp.MustCompile("str")), +// // ), +// // }, +// // +// // This is only shown as an example. +// output test_resource_one_output { +// value = test_resource.one +// }`, +// ConfigStateChecks: []statecheck.StateCheck{ +// statecheck.ExpectKnownOutputValueAtPath( +// "test_resource_one_output", +// tfjsonpath.New("computed_attribute"), +// knownvalue.StringRegexp(regexp.MustCompile("str"), +// ), +// }, +// }, +// }, +// }) +// } +func TestMatchOutput(name string, r *regexp.Regexp) TestCheckFunc { + return func(s *terraform.State) error { + ms := s.RootModule() + rs, ok := ms.Outputs[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + valStr, ok := rs.Value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for resource value", rs.Value) + } + + if !r.MatchString(valStr) { + return fmt.Errorf( + "Output '%s': %#v didn't match %q", + name, + rs, + r.String()) + } + + return nil + } +} + +// modulePrimaryInstanceState returns the instance state for the given resource +// name in a ModuleState +func modulePrimaryInstanceState(ms *terraform.ModuleState, name string) (*terraform.InstanceState, error) { + rs, ok := ms.Resources[name] + if !ok { + return nil, fmt.Errorf("Not found: %s in %s", name, ms.Path) + } + + is := rs.Primary + if is == nil { + return nil, fmt.Errorf("No primary instance: %s in %s", name, ms.Path) + } + + return is, nil +} + +// modulePathPrimaryInstanceState returns the primary instance state for the +// given resource name in a given module path. +func modulePathPrimaryInstanceState(s *terraform.State, mp addrs.ModuleInstance, name string) (*terraform.InstanceState, error) { + ms := s.ModuleByPath(mp) //nolint:staticcheck // legacy usage + if ms == nil { + return nil, fmt.Errorf("No module found at: %s", mp) + } + + return modulePrimaryInstanceState(ms, name) +} + +// primaryInstanceState returns the primary instance state for the given +// resource name in the root module. +func primaryInstanceState(s *terraform.State, name string) (*terraform.InstanceState, error) { + ms := s.RootModule() //nolint:staticcheck // legacy usage + return modulePrimaryInstanceState(ms, name) +} + +// indexesIntoTypeSet is a heuristic to try and identify if a flatmap style +// string address uses a precalculated TypeSet hash, which are integers and +// typically are large and obviously not a list index +func indexesIntoTypeSet(key string) bool { + for _, part := range strings.Split(key, ".") { + if i, err := strconv.Atoi(part); err == nil && i > 100 { + return true + } + } + return false +} + +func checkIfIndexesIntoTypeSet(key string, f TestCheckFunc) TestCheckFunc { + return func(s *terraform.State) error { + err := f(s) + if err != nil && indexesIntoTypeSet(key) { + return fmt.Errorf("Error in test check: %s\nTest check address %q likely indexes into TypeSet\nThis is currently not possible in the SDK", err, key) + } + return err + } +} + +func checkIfIndexesIntoTypeSetPair(keyFirst, keySecond string, f TestCheckFunc) TestCheckFunc { + return func(s *terraform.State) error { + err := f(s) + if err != nil && (indexesIntoTypeSet(keyFirst) || indexesIntoTypeSet(keySecond)) { + return fmt.Errorf("Error in test check: %s\nTest check address %q or %q likely indexes into TypeSet\nThis is currently not possible in the SDK", err, keyFirst, keySecond) + } + return err + } +} diff --git a/querycheck/testing_config.go b/querycheck/testing_config.go new file mode 100644 index 00000000..e369f349 --- /dev/null +++ b/querycheck/testing_config.go @@ -0,0 +1,29 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-testing/internal/logging" + + "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" +) + +func testStepTaint(ctx context.Context, step TestStep, wd *plugintest.WorkingDir) error { + if len(step.Taint) == 0 { + return nil + } + + logging.HelperResourceTrace(ctx, fmt.Sprintf("Using TestStep Taint: %v", step.Taint)) + + for _, p := range step.Taint { + err := wd.Taint(ctx, p) + if err != nil { + return fmt.Errorf("error tainting resource: %s", err) + } + } + return nil +} diff --git a/querycheck/testing_new.go b/querycheck/testing_new.go new file mode 100644 index 00000000..fb367a69 --- /dev/null +++ b/querycheck/testing_new.go @@ -0,0 +1,701 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "fmt" + "os" + "path/filepath" + "reflect" + "strconv" + "strings" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/go-version" + tfjson "github.com/hashicorp/terraform-json" + "github.com/mitchellh/go-testing-interface" + + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/internal/logging" + "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func runPostTestDestroy(ctx context.Context, t testing.T, c TestCase, wd *plugintest.WorkingDir, providers *providerFactories, statePreDestroy *terraform.State) error { + t.Helper() + + err := runProviderCommand(ctx, t, wd, providers, func() error { + return wd.Destroy(ctx) + }) + if err != nil { + return err + } + + if c.CheckDestroy != nil { + logging.HelperResourceTrace(ctx, "Using TestCase CheckDestroy") + logging.HelperResourceDebug(ctx, "Calling TestCase CheckDestroy") + + if err := c.CheckDestroy(statePreDestroy); err != nil { + return err + } + + logging.HelperResourceDebug(ctx, "Called TestCase CheckDestroy") + } + + return nil +} + +func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest.Helper) { + t.Helper() + + wd := helper.RequireNewWorkingDir(ctx, t, c.WorkingDir) + + ctx = logging.TestTerraformPathContext(ctx, wd.GetHelper().TerraformExecPath()) + ctx = logging.TestWorkingDirectoryContext(ctx, wd.GetHelper().WorkingDirectory()) + + providers := &providerFactories{ + legacy: c.ProviderFactories, + protov5: c.ProtoV5ProviderFactories, + protov6: c.ProtoV6ProviderFactories, + } + + defer func() { + t.Helper() + + var statePreDestroy *terraform.State + var err error + err = runProviderCommand(ctx, t, wd, providers, func() error { + _, statePreDestroy, err = getState(ctx, t, wd) + if err != nil { + return err + } + return nil + }) + if err != nil { + logging.HelperResourceError(ctx, + "Error retrieving state, there may be dangling resources", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("Error retrieving state, there may be dangling resources: %s", err.Error()) + return + } + + if !stateIsEmpty(statePreDestroy) { + err := runPostTestDestroy(ctx, t, c, wd, providers, statePreDestroy) + if err != nil { + logging.HelperResourceError(ctx, + "Error running post-test destroy, there may be dangling resources", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("Error running post-test destroy, there may be dangling resources: %s", err.Error()) + } + } + + wd.Close() + }() + + // Return value from c.ProviderConfig() is assigned to Raw as this was previously being + // passed to wd.SetConfig() when the second argument accept a configuration string. + if c.hasProviders(ctx) { + config := teststep.Configuration( + teststep.ConfigurationRequest{ + Raw: teststep.Pointer(c.providerConfig(ctx, false)), + }, + ) + + err := wd.SetConfig(ctx, config, nil) + + if err != nil { + logging.HelperResourceError(ctx, + "TestCase error setting provider configuration", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("TestCase error setting provider configuration: %s", err) + } + + err = runProviderCommand(ctx, t, wd, providers, func() error { + return wd.Init(ctx) + }) + + if err != nil { + logging.HelperResourceError(ctx, + "TestCase error running init", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("TestCase error running init: %s", err.Error()) + } + } + + // use this to track last step successfully applied + // acts as default for import tests + var appliedCfg teststep.Config + var stepNumber int + + for stepIndex, step := range c.Steps { + if stepNumber > 0 { + copyWorkingDir(ctx, t, stepNumber, wd) + } + + stepNumber = stepIndex + 1 // 1-based indexing for humans + + configRequest := teststep.PrepareConfigurationRequest{ + Directory: step.ConfigDirectory, + File: step.ConfigFile, + Raw: step.Config, + TestStepConfigRequest: config.TestStepConfigRequest{ + StepNumber: stepNumber, + TestName: t.Name(), + }, + }.Exec() + + cfg := teststep.Configuration(configRequest) + + ctx = logging.TestStepNumberContext(ctx, stepNumber) + + logging.HelperResourceDebug(ctx, "Starting TestStep") + + if step.PreConfig != nil { + logging.HelperResourceDebug(ctx, "Calling TestStep PreConfig") + step.PreConfig() + logging.HelperResourceDebug(ctx, "Called TestStep PreConfig") + } + + if step.SkipFunc != nil { + logging.HelperResourceDebug(ctx, "Calling TestStep SkipFunc") + + skip, err := step.SkipFunc() + if err != nil { + logging.HelperResourceError(ctx, + "Error calling TestStep SkipFunc", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("Error calling TestStep SkipFunc: %s", err.Error()) + } + + logging.HelperResourceDebug(ctx, "Called TestStep SkipFunc") + + if skip { + t.Logf("Skipping step %d/%d due to SkipFunc", stepNumber, len(c.Steps)) + logging.HelperResourceWarn(ctx, "Skipping TestStep due to SkipFunc") + continue + } + } + + if cfg != nil && !step.Destroy && len(step.Taint) > 0 { + err := testStepTaint(ctx, step, wd) + + if err != nil { + logging.HelperResourceError(ctx, + "TestStep error tainting resources", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("TestStep %d/%d error tainting resources: %s", stepNumber, len(c.Steps), err) + } + } + + hasProviders, err := step.hasProviders(ctx, stepIndex, t.Name()) + + if err != nil { + logging.HelperResourceError(ctx, + "TestStep error checking for providers", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("TestStep %d/%d error checking for providers: %s", stepNumber, len(c.Steps), err) + } + + if hasProviders { + providers = &providerFactories{ + legacy: sdkProviderFactories(c.ProviderFactories).merge(step.ProviderFactories), + protov5: protov5ProviderFactories(c.ProtoV5ProviderFactories).merge(step.ProtoV5ProviderFactories), + protov6: protov6ProviderFactories(c.ProtoV6ProviderFactories).merge(step.ProtoV6ProviderFactories), + } + + var hasProviderBlock bool + + if cfg != nil { + hasProviderBlock, err = cfg.HasProviderBlock(ctx) + + if err != nil { + logging.HelperResourceError(ctx, + "TestStep error determining whether configuration contains provider block", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("TestStep %d/%d error determining whether configuration contains provider block: %s", stepNumber, len(c.Steps), err) + } + } + + var testStepConfig teststep.Config + + rawCfg, err := step.providerConfig(ctx, hasProviderBlock, helper.TerraformVersion()) + + if err != nil { + logging.HelperResourceError(ctx, + "TestStep error generating provider configuration", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("TestStep %d/%d error generating provider configuration: %s", stepNumber, len(c.Steps), err) + } + + // Return value from step.providerConfig() is assigned to Raw as this was previously being + // passed to wd.SetConfig() directly when the second argument to wd.SetConfig() accepted a + // configuration string. + confRequest := teststep.PrepareConfigurationRequest{ + Directory: step.ConfigDirectory, + File: step.ConfigFile, + Raw: rawCfg, + TestStepConfigRequest: config.TestStepConfigRequest{ + StepNumber: stepNumber, + TestName: t.Name(), + }, + }.Exec() + + testStepConfig = teststep.Configuration(confRequest) + + if !step.Query { + err = wd.SetConfig(ctx, testStepConfig, step.ConfigVariables) + } + + if err != nil { + logging.HelperResourceError(ctx, + "TestStep error setting provider configuration", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("TestStep %d/%d error setting test provider configuration: %s", stepNumber, len(c.Steps), err) + } + + err = runProviderCommand(ctx, t, wd, providers, func() error { + return wd.Init(ctx) + }) + + if err != nil { + logging.HelperResourceError(ctx, + "TestStep error running init", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("TestStep %d/%d running init: %s", stepNumber, len(c.Steps), err.Error()) + return + } + } + + if step.ImportState { + logging.HelperResourceTrace(ctx, "TestStep is ImportState mode") + + err := testStepNewImportState(ctx, t, helper, wd, step, appliedCfg, providers, stepNumber) + if step.ExpectError != nil { + logging.HelperResourceDebug(ctx, "Checking TestStep ExpectError") + if err == nil { + logging.HelperResourceError(ctx, + "Error running import: expected an error but got none", + ) + t.Fatalf("Step %d/%d error running import: expected an error but got none", stepNumber, len(c.Steps)) + } + if !step.ExpectError.MatchString(err.Error()) { + logging.HelperResourceError(ctx, + fmt.Sprintf("Error running import: expected an error with pattern (%s)", step.ExpectError.String()), + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("Step %d/%d error running import, expected an error with pattern (%s), no match on: %s", stepNumber, len(c.Steps), step.ExpectError.String(), err) + } + } else { + if err != nil && c.ErrorCheck != nil { + logging.HelperResourceDebug(ctx, "Calling TestCase ErrorCheck") + err = c.ErrorCheck(err) + logging.HelperResourceDebug(ctx, "Called TestCase ErrorCheck") + } + if err != nil { + logging.HelperResourceError(ctx, + "Error running import", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("Step %d/%d error running import: %s", stepNumber, len(c.Steps), err) + } + } + + logging.HelperResourceDebug(ctx, "Finished TestStep") + + continue + } + + if step.RefreshState { + logging.HelperResourceTrace(ctx, "TestStep is RefreshState mode") + + err := testStepNewRefreshState(ctx, t, wd, step, providers) + if step.ExpectError != nil { + logging.HelperResourceDebug(ctx, "Checking TestStep ExpectError") + if err == nil { + logging.HelperResourceError(ctx, + "Error running refresh: expected an error but got none", + ) + t.Fatalf("Step %d/%d error running refresh: expected an error but got none", stepNumber, len(c.Steps)) + } + if !step.ExpectError.MatchString(err.Error()) { + logging.HelperResourceError(ctx, + fmt.Sprintf("Error running refresh: expected an error with pattern (%s)", step.ExpectError.String()), + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("Step %d/%d error running refresh, expected an error with pattern (%s), no match on: %s", stepNumber, len(c.Steps), step.ExpectError.String(), err) + } + } else { + if err != nil && c.ErrorCheck != nil { + logging.HelperResourceDebug(ctx, "Calling TestCase ErrorCheck") + err = c.ErrorCheck(err) + logging.HelperResourceDebug(ctx, "Called TestCase ErrorCheck") + } + if err != nil { + logging.HelperResourceError(ctx, + "Error running refresh", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("Step %d/%d error running refresh: %s", stepNumber, len(c.Steps), err) + } + } + + logging.HelperResourceDebug(ctx, "Finished TestStep") + + continue + } + + if step.Query { + logging.HelperResourceTrace(ctx, "TestStep is Query mode") + + queryConfigRequest := teststep.ConfigurationRequest{ + Raw: &step.Config, + } + err := wd.SetQuery(ctx, teststep.Configuration(queryConfigRequest), step.ConfigVariables) + if err != nil { + t.Fatalf("Step %d/%d error setting query: %s", stepNumber, len(c.Steps), err) + } + + err = runProviderCommand(ctx, t, wd, providers, func() error { + return wd.Init(ctx) + }) + if err != nil { + t.Fatalf("Step %d/%d error running init: %s", stepNumber, len(c.Steps), err) + } + + var queryOut []tfjson.LogMsg + err = runProviderCommand(ctx, t, wd, providers, func() error { + var err error + queryOut, err = wd.Query(ctx) + return err + }) + if err != nil { + fmt.Printf("Step %d/%d Query Output:\n%s\n", stepNumber, len(c.Steps), queryOut) + t.Fatalf("Step %d/%d error running query: %s", stepNumber, len(c.Steps), err) + } + + err = RunQueryChecks(ctx, t, queryOut, step.QueryResultChecks) + if err != nil { + t.Fatalf("Step %d/%d error running query checks: %s", stepNumber, len(c.Steps), err) + } + + fmt.Printf("Step %d/%d Query Output:\n%s\n", stepNumber, len(c.Steps), queryOut) + continue + } + + if cfg != nil { + logging.HelperResourceTrace(ctx, "TestStep is Config mode") + + err := testStepNewConfig(ctx, t, c, wd, step, providers, stepIndex, helper) + if step.ExpectError != nil { + logging.HelperResourceDebug(ctx, "Checking TestStep ExpectError") + + if err == nil { + logging.HelperResourceError(ctx, + "Expected an error but got none", + ) + t.Fatalf("Step %d/%d, expected an error but got none", stepNumber, len(c.Steps)) + } + if !step.ExpectError.MatchString(err.Error()) { + logging.HelperResourceError(ctx, + fmt.Sprintf("Expected an error with pattern (%s)", step.ExpectError.String()), + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("Step %d/%d, expected an error with pattern, no match on: %s", stepNumber, len(c.Steps), err) + } + } else { + if err != nil && c.ErrorCheck != nil { + logging.HelperResourceDebug(ctx, "Calling TestCase ErrorCheck") + + err = c.ErrorCheck(err) + + logging.HelperResourceDebug(ctx, "Called TestCase ErrorCheck") + } + if err != nil { + logging.HelperResourceError(ctx, + "Unexpected error", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("Step %d/%d error: %s", stepNumber, len(c.Steps), err) + } + } + + var hasTerraformBlock bool + var hasProviderBlock bool + + if cfg != nil { + hasTerraformBlock, err = cfg.HasTerraformBlock(ctx) + + if err != nil { + logging.HelperResourceError(ctx, + "Error determining whether configuration contains terraform block", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("Error determining whether configuration contains terraform block: %s", err) + } + + hasProviderBlock, err = cfg.HasProviderBlock(ctx) + + if err != nil { + logging.HelperResourceError(ctx, + "Error determining whether configuration contains provider block", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("Error determining whether configuration contains provider block: %s", err) + } + } + + mergedConfig, err := step.mergedConfig(ctx, c, hasTerraformBlock, hasProviderBlock, helper.TerraformVersion()) + + if err != nil { + logging.HelperResourceError(ctx, + "Error generating merged configuration", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("Error generating merged configuration: %s", err) + } + + // Preserve the step config for future test steps to use (import state) + confRequest := teststep.PrepareConfigurationRequest{ + Directory: step.ConfigDirectory, + File: step.ConfigFile, + Raw: mergedConfig, + TestStepConfigRequest: config.TestStepConfigRequest{ + StepNumber: stepNumber, + TestName: t.Name(), + }, + }.Exec() + + appliedCfg = teststep.Configuration(confRequest) + + logging.HelperResourceDebug(ctx, "Finished TestStep") + + continue + } + + t.Fatalf("Step %d/%d, unsupported test mode", stepNumber, len(c.Steps)) + } + + if stepNumber > 0 { + copyWorkingDir(ctx, t, stepNumber, wd) + } +} + +func getState(ctx context.Context, t testing.T, wd *plugintest.WorkingDir) (*tfjson.State, *terraform.State, error) { + t.Helper() + + jsonState, err := wd.State(ctx) + if err != nil { + return nil, nil, err + } + state, err := shimStateFromJson(jsonState) + if err != nil { + t.Fatal(err) + } + return jsonState, state, nil +} + +func stateIsEmpty(state *terraform.State) bool { + return state.Empty() || !state.HasResources() //nolint:staticcheck // legacy usage +} + +func planIsEmpty(plan *tfjson.Plan, tfVersion *version.Version) bool { + for _, rc := range plan.ResourceChanges { + for _, a := range rc.Change.Actions { + if a != tfjson.ActionNoop { + return false + } + } + } + + if tfVersion.LessThan(expectNonEmptyPlanOutputChangesMinTFVersion) { + return true + } + + for _, change := range plan.OutputChanges { + if !change.Actions.NoOp() { + return false + } + } + + return true +} + +func testIDRefresh(ctx context.Context, t testing.T, c TestCase, wd *plugintest.WorkingDir, step TestStep, r *terraform.ResourceState, providers *providerFactories, stepIndex int, helper *plugintest.Helper) error { + t.Helper() + + // Build the state. The state is just the resource with an ID. There + // are no attributes. We only set what is needed to perform a refresh. + state := terraform.NewState() //nolint:staticcheck // legacy usage + state.RootModule().Resources = make(map[string]*terraform.ResourceState) + state.RootModule().Resources[c.IDRefreshName] = &terraform.ResourceState{} + + configRequest := teststep.PrepareConfigurationRequest{ + Directory: step.ConfigDirectory, + File: step.ConfigFile, + Raw: step.Config, + TestStepConfigRequest: config.TestStepConfigRequest{ + StepNumber: stepIndex + 1, + TestName: t.Name(), + }, + }.Exec() + + cfg := teststep.Configuration(configRequest) + + var hasProviderBlock bool + + if cfg != nil { + var err error + + hasProviderBlock, err = cfg.HasProviderBlock(ctx) + + if err != nil { + logging.HelperResourceError(ctx, + "Error determining whether configuration contains provider block for import test config", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("Error determining whether configuration contains provider block for import test config: %s", err) + } + } + + // Return value from c.ProviderConfig() is assigned to Raw as this was previously being + // passed to wd.SetConfig() when the second argument accept a configuration string. + testStepConfig := teststep.Configuration( + teststep.ConfigurationRequest{ + Raw: teststep.Pointer(c.providerConfig(ctx, hasProviderBlock)), + }, + ) + + // Temporarily set the config to a minimal provider config for the refresh + // test. After the refresh we can reset it. + err := wd.SetConfig(ctx, testStepConfig, step.ConfigVariables) + if err != nil { + t.Fatalf("Error setting import test config: %s", err) + } + + rawCfg, err := step.providerConfig(ctx, hasProviderBlock, helper.TerraformVersion()) + + if err != nil { + t.Fatalf("Error generating import provider config: %s", err) + } + + defer func() { + t.Helper() + + confRequest := teststep.PrepareConfigurationRequest{ + Directory: step.ConfigDirectory, + File: step.ConfigFile, + Raw: rawCfg, + TestStepConfigRequest: config.TestStepConfigRequest{ + StepNumber: stepIndex + 1, + TestName: t.Name(), + }, + }.Exec() + + testStepConfigDefer := teststep.Configuration(confRequest) + + err = wd.SetConfig(ctx, testStepConfigDefer, step.ConfigVariables) + + if err != nil { + t.Fatalf("Error resetting test config: %s", err) + } + }() + + // Refresh! + err = runProviderCommand(ctx, t, wd, providers, func() error { + err = wd.Refresh(ctx) + if err != nil { + t.Fatalf("Error running terraform refresh: %s", err) + } + _, state, err = getState(ctx, t, wd) + if err != nil { + return err + } + return nil + }) + if err != nil { + return err + } + + // Verify attribute equivalence. + actualR := state.RootModule().Resources[c.IDRefreshName] + if actualR == nil { + return fmt.Errorf("Resource gone!") + } + if actualR.Primary == nil { + return fmt.Errorf("Resource has no primary instance") + } + actual := actualR.Primary.Attributes + expected := r.Primary.Attributes + + if len(c.IDRefreshIgnore) > 0 { + logging.HelperResourceTrace(ctx, fmt.Sprintf("Using TestCase IDRefreshIgnore: %v", c.IDRefreshIgnore)) + } + + // Remove fields we're ignoring + for _, v := range c.IDRefreshIgnore { + for k := range actual { + if strings.HasPrefix(k, v) { + delete(actual, k) + } + } + for k := range expected { + if strings.HasPrefix(k, v) { + delete(expected, k) + } + } + } + + if !reflect.DeepEqual(actual, expected) { + // Determine only the different attributes + for k, v := range expected { + if av, ok := actual[k]; ok && v == av { + delete(expected, k) + delete(actual, k) + } + } + + if diff := cmp.Diff(expected, actual); diff != "" { + return fmt.Errorf("IDRefreshName attributes not equivalent. Difference is shown below. The - symbol indicates attributes missing after refresh.\n\n%s", diff) + } + } + + return nil +} + +func copyWorkingDir(ctx context.Context, t testing.T, stepNumber int, wd *plugintest.WorkingDir) { + if os.Getenv(plugintest.EnvTfAccPersistWorkingDir) == "" { + return + } + + workingDir := wd.GetHelper().WorkingDirectory() + + dest := filepath.Join(workingDir, fmt.Sprintf("%s%s", "step_", strconv.Itoa(stepNumber))) + + baseDir := wd.BaseDir() + rootBaseDir := strings.TrimPrefix(baseDir, workingDir) + + err := plugintest.CopyDir(workingDir, dest, rootBaseDir) + if err != nil { + logging.HelperResourceError(ctx, + "Unexpected error copying working directory files", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("TestStep %d/%d error copying working directory files: %s", stepNumber, err) + } + + t.Logf("Working directory and files have been copied to: %s", dest) +} diff --git a/querycheck/testing_new_config.go b/querycheck/testing_new_config.go new file mode 100644 index 00000000..38114cd1 --- /dev/null +++ b/querycheck/testing_new_config.go @@ -0,0 +1,469 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "errors" + "fmt" + "os" + + "github.com/hashicorp/terraform-exec/tfexec" + tfjson "github.com/hashicorp/terraform-json" + "github.com/mitchellh/go-testing-interface" + + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + + "github.com/hashicorp/terraform-plugin-testing/internal/logging" + "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" +) + +// expectNonEmptyPlanOutputChangesMinTFVersion is used to keep compatibility for +// Terraform 0.12 and 0.13 after enabling ExpectNonEmptyPlan to check output +// changes. Those older versions will always show outputs being created. +var expectNonEmptyPlanOutputChangesMinTFVersion = tfversion.Version0_14_0 + +func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugintest.WorkingDir, step TestStep, providers *providerFactories, stepIndex int, helper *plugintest.Helper) error { + t.Helper() + + // When `refreshAfterApply` is true, a `Config`-mode test step will invoke + // a refresh before successful completion. This is a compatibility measure + // for test cases that have different -- but semantically-equal -- state + // representations in their test steps. When comparing two states, the + // testing framework is not aware of semantic equality or set equality. + _, refreshAfterApply := os.LookupEnv(EnvTfAccRefreshAfterApply) + + configRequest := teststep.PrepareConfigurationRequest{ + Directory: step.ConfigDirectory, + File: step.ConfigFile, + Raw: step.Config, + TestStepConfigRequest: config.TestStepConfigRequest{ + StepNumber: stepIndex + 1, + TestName: t.Name(), + }, + }.Exec() + + cfg := teststep.Configuration(configRequest) + + var hasTerraformBlock bool + var hasProviderBlock bool + + if cfg != nil { + var err error + + hasTerraformBlock, err = cfg.HasTerraformBlock(ctx) + + if err != nil { + logging.HelperResourceError(ctx, + "Error determining whether configuration contains terraform block", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("Error determining whether configuration contains terraform block: %s", err) + } + + hasProviderBlock, err = cfg.HasProviderBlock(ctx) + + if err != nil { + logging.HelperResourceError(ctx, + "Error determining whether configuration contains provider block", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("Error determining whether configuration contains provider block: %s", err) + } + } + + mergedConfig, err := step.mergedConfig(ctx, c, hasTerraformBlock, hasProviderBlock, helper.TerraformVersion()) + + if err != nil { + logging.HelperResourceError(ctx, + "Error generating merged configuration", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("Error generating merged configuration: %s", err) + } + + confRequest := teststep.PrepareConfigurationRequest{ + Directory: step.ConfigDirectory, + File: step.ConfigFile, + Raw: mergedConfig, + TestStepConfigRequest: config.TestStepConfigRequest{ + StepNumber: stepIndex + 1, + TestName: t.Name(), + }, + }.Exec() + + testStepConfig := teststep.Configuration(confRequest) + + err = wd.SetConfig(ctx, testStepConfig, step.ConfigVariables) + if err != nil { + return fmt.Errorf("Error setting config: %w", err) + } + + // If this step is a PlanOnly step, skip over this first Plan and + // subsequent Apply, and use the follow-up Plan that checks for + // permadiffs + if !step.PlanOnly { + logging.HelperResourceDebug(ctx, "Running Terraform CLI plan and apply") + + // Plan! + err := runProviderCommand(ctx, t, wd, providers, func() error { + var opts []tfexec.PlanOption + if step.Destroy { + opts = append(opts, tfexec.Destroy(true)) + } + + if c.AdditionalCLIOptions != nil { + if c.AdditionalCLIOptions.Plan.AllowDeferral { + opts = append(opts, tfexec.AllowDeferral(true)) + } + if c.AdditionalCLIOptions.Plan.NoRefresh { + opts = append(opts, tfexec.Refresh(false)) + } + } + + return wd.CreatePlan(ctx, opts...) + }) + if err != nil { + return fmt.Errorf("Error running pre-apply plan: %w", err) + } + + // Run pre-apply plan checks + if len(step.ConfigPlanChecks.PreApply) > 0 { + var plan *tfjson.Plan + err = runProviderCommand(ctx, t, wd, providers, func() error { + var err error + plan, err = wd.SavedPlan(ctx) + return err + }) + if err != nil { + return fmt.Errorf("Error retrieving pre-apply plan: %w", err) + } + + err = runPlanChecks(ctx, t, plan, step.ConfigPlanChecks.PreApply) + if err != nil { + return fmt.Errorf("Pre-apply plan check(s) failed:\n%w", err) + } + } + + // We need to keep a copy of the state prior to destroying such + // that the destroy steps can verify their behavior in the + // check function + var stateBeforeApplication *terraform.State + + if step.Check != nil && step.Destroy { + // Refresh the state before shimming it for destroy checks later. + // This re-implements previously existing test step logic for the + // specific situation that a provider developer has applied a + // resource with a previous schema version and is destroying it with + // a provider that has a newer schema version. Without this refresh + // the shim logic will return an error such as: + // + // Failed to marshal state to json: schema version 0 for null_resource.test in state does not match version 1 from the provider + err := runProviderCommand(ctx, t, wd, providers, func() error { + return wd.Refresh(ctx) + }) + + if err != nil { + return fmt.Errorf("Error running pre-apply refresh: %w", err) + } + + err = runProviderCommand(ctx, t, wd, providers, func() error { + _, stateBeforeApplication, err = getState(ctx, t, wd) + if err != nil { + return err + } + return nil + }) + + if err != nil { + return fmt.Errorf("Error retrieving pre-apply state: %w", err) + } + } + + // Apply the diff, creating real resources + err = runProviderCommand(ctx, t, wd, providers, func() error { + var opts []tfexec.ApplyOption + + if c.AdditionalCLIOptions != nil && c.AdditionalCLIOptions.Apply.AllowDeferral { + opts = append(opts, tfexec.AllowDeferral(true)) + } + + return wd.Apply(ctx, opts...) + }) + if err != nil { + if step.Destroy { + return fmt.Errorf("Error running destroy: %w", err) + } + return fmt.Errorf("Error running apply: %w", err) + } + + // Run any configured checks + if step.Check != nil { + logging.HelperResourceTrace(ctx, "Using TestStep Check") + + if step.Destroy { + if err := step.Check(stateBeforeApplication); err != nil { + return fmt.Errorf("Check failed: %w", err) + } + } else { + var state *terraform.State + + err := runProviderCommand(ctx, t, wd, providers, func() error { + _, state, err = getState(ctx, t, wd) + if err != nil { + return err + } + return nil + }) + + if err != nil { + return fmt.Errorf("Error retrieving state after apply: %w", err) + } + + if err := step.Check(state); err != nil { + return fmt.Errorf("Check failed: %w", err) + } + } + } + + // Run state checks + if len(step.ConfigStateChecks) > 0 { + var state *tfjson.State + + err = runProviderCommand(ctx, t, wd, providers, func() error { + var err error + state, err = wd.State(ctx) + return err + }) + + if err != nil { + return fmt.Errorf("Error retrieving post-apply, post-refresh state: %w", err) + } + + err = runStateChecks(ctx, t, state, step.ConfigStateChecks) + if err != nil { + return fmt.Errorf("Post-apply refresh state check(s) failed:\n%w", err) + } + } + } + + // Test for perpetual diffs by performing a plan, a refresh, and another plan + logging.HelperResourceDebug(ctx, "Running Terraform CLI plan to check for perpetual differences") + + // do a plan + err = runProviderCommand(ctx, t, wd, providers, func() error { + opts := []tfexec.PlanOption{ + tfexec.Refresh(false), + } + if step.Destroy { + opts = append(opts, tfexec.Destroy(true)) + } + + if c.AdditionalCLIOptions != nil { + if c.AdditionalCLIOptions.Plan.AllowDeferral { + opts = append(opts, tfexec.AllowDeferral(true)) + } + if c.AdditionalCLIOptions.Plan.NoRefresh { + opts = append(opts, tfexec.Refresh(false)) + } + } + + return wd.CreatePlan(ctx, opts...) + }) + if err != nil { + if step.PlanOnly { + return fmt.Errorf("Error running non-refresh plan: %w", err) + } + + return fmt.Errorf("Error running post-apply non-refresh plan: %w", err) + } + + var plan *tfjson.Plan + err = runProviderCommand(ctx, t, wd, providers, func() error { + var err error + plan, err = wd.SavedPlan(ctx) + return err + }) + if err != nil { + if step.PlanOnly { + return fmt.Errorf("Error reading saved non-refresh plan: %w", err) + } + + return fmt.Errorf("Error reading saved post-apply non-refresh plan: %w", err) + } + + // Run post-apply, pre-refresh plan checks + if len(step.ConfigPlanChecks.PostApplyPreRefresh) > 0 { + err = runPlanChecks(ctx, t, plan, step.ConfigPlanChecks.PostApplyPreRefresh) + if err != nil { + if step.PlanOnly { + return fmt.Errorf("Non-refresh plan checks(s) failed:\n%w", err) + } + + return fmt.Errorf("Post-apply, pre-refresh plan check(s) failed:\n%w", err) + } + } + + if !planIsEmpty(plan, helper.TerraformVersion()) && !step.ExpectNonEmptyPlan { + var stdout string + err = runProviderCommand(ctx, t, wd, providers, func() error { + var err error + stdout, err = wd.SavedPlanRawStdout(ctx) + return err + }) + if err != nil { + return fmt.Errorf("Error reading saved human-readable non-refresh plan output: %w", err) + } + + if step.PlanOnly { + return fmt.Errorf("The non-refresh plan was not empty.\nstdout:\n\n%s", stdout) + } + + return fmt.Errorf("After applying this test step, the non-refresh plan was not empty.\nstdout:\n\n%s", stdout) + } + + // do another plan + err = runProviderCommand(ctx, t, wd, providers, func() error { + var opts []tfexec.PlanOption + if step.Destroy { + opts = append(opts, tfexec.Destroy(true)) + + if step.PreventPostDestroyRefresh { + opts = append(opts, tfexec.Refresh(false)) + } + } + + if c.AdditionalCLIOptions != nil { + if c.AdditionalCLIOptions.Plan.AllowDeferral { + opts = append(opts, tfexec.AllowDeferral(true)) + } + if c.AdditionalCLIOptions.Plan.NoRefresh { + opts = append(opts, tfexec.Refresh(false)) + } + } + + return wd.CreatePlan(ctx, opts...) + }) + if err != nil { + if step.PlanOnly { + return fmt.Errorf("Error running refresh plan: %w", err) + } + + return fmt.Errorf("Error running post-apply refresh plan: %w", err) + } + + err = runProviderCommand(ctx, t, wd, providers, func() error { + var err error + plan, err = wd.SavedPlan(ctx) + return err + }) + if err != nil { + if step.PlanOnly { + return fmt.Errorf("Error reading refresh plan: %w", err) + } + + return fmt.Errorf("Error reading post-apply refresh plan: %w", err) + } + + // Run post-apply, post-refresh plan checks + if len(step.ConfigPlanChecks.PostApplyPostRefresh) > 0 { + err = runPlanChecks(ctx, t, plan, step.ConfigPlanChecks.PostApplyPostRefresh) + if err != nil { + return fmt.Errorf("Post-apply refresh plan check(s) failed:\n%w", err) + } + } + + // check if plan is empty + if !planIsEmpty(plan, helper.TerraformVersion()) && !step.ExpectNonEmptyPlan { + var stdout string + err = runProviderCommand(ctx, t, wd, providers, func() error { + var err error + stdout, err = wd.SavedPlanRawStdout(ctx) + return err + }) + if err != nil { + return fmt.Errorf("Error reading human-readable refresh plan output: %w", err) + } + + if step.PlanOnly { + return fmt.Errorf("The refresh plan was not empty.\nstdout\n\n%s", stdout) + } + + return fmt.Errorf("After applying this test step, the refresh plan was not empty.\nstdout\n\n%s", stdout) + } else if step.ExpectNonEmptyPlan && planIsEmpty(plan, helper.TerraformVersion()) { + return errors.New("Expected a non-empty plan, but got an empty refresh plan") + } + + // ID-ONLY REFRESH + // If we've never checked an id-only refresh and our state isn't + // empty, find the first resource and test it. + if c.IDRefreshName != "" { + logging.HelperResourceTrace(ctx, "Using TestCase IDRefreshName") + + var state *terraform.State + + err = runProviderCommand(ctx, t, wd, providers, func() error { + _, state, err = getState(ctx, t, wd) + if err != nil { + return err + } + return nil + }) + + if err != nil { + return err + } + + //nolint:staticcheck // legacy usage + if state.Empty() { + return nil + } + + var idRefreshCheck *terraform.ResourceState + + // Find the first non-nil resource in the state + for _, m := range state.Modules { + if len(m.Resources) > 0 { + if v, ok := m.Resources[c.IDRefreshName]; ok { + idRefreshCheck = v + } + + break + } + } + + // If we have an instance to check for refreshes, do it + // immediately. We do it in the middle of another test + // because it shouldn't affect the overall state (refresh + // is read-only semantically) and we want to fail early if + // this fails. If refresh isn't read-only, then this will have + // caught a different bug. + if idRefreshCheck != nil { + fmt.Println("Not Writing by testing ID Refresh") + if err := testIDRefresh(ctx, t, c, wd, step, idRefreshCheck, providers, stepIndex, helper); err != nil { + return fmt.Errorf( + "[ERROR] Test: ID-only test failed: %s", err) + } + } + } + + if refreshAfterApply && !step.Destroy && !step.PlanOnly { + if len(c.Steps) > stepIndex+1 { + // If the next step is a refresh, then we have no need to refresh here + if !c.Steps[stepIndex+1].RefreshState { + // Log a searchable message to easily determine when this is no longer being used + logging.HelperResourceDebug(ctx, EnvTfAccRefreshAfterApply+": running apply -refresh-only -refresh=true") + err := runProviderCommandApplyRefreshOnly(ctx, t, wd, providers) + if err != nil { + return fmt.Errorf("Error running apply refresh-only: %w", err) + } + } + } + } + + return nil +} diff --git a/querycheck/testing_new_import_state.go b/querycheck/testing_new_import_state.go new file mode 100644 index 00000000..59639333 --- /dev/null +++ b/querycheck/testing_new_import_state.go @@ -0,0 +1,571 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "encoding/json" + "fmt" + "reflect" + "strings" + + "github.com/hashicorp/go-version" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/google/go-cmp/cmp" + "github.com/mitchellh/go-testing-interface" + + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/internal/logging" + "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest.Helper, testCaseWorkingDir *plugintest.WorkingDir, step TestStep, priorStepCfg teststep.Config, providers *providerFactories, stepNumber int) error { + t.Helper() + + // step.ImportStateKind implicitly defaults to the zero-value (ImportCommandWithID) for backward compatibility + kind := step.ImportStateKind + importStatePersist := step.ImportStatePersist + + if err := importStatePreconditions(t, helper, step); err != nil { + return err + } + + resourceName := step.ResourceName + if resourceName == "" { + t.Fatal("ResourceName is required for an import state test") + } + + // get state from check sequence + var state *terraform.State + var stateJSON *tfjson.State + var err error + + err = runProviderCommand(ctx, t, testCaseWorkingDir, providers, func() error { + stateJSON, state, err = getState(ctx, t, testCaseWorkingDir) + if err != nil { + return err + } + return nil + }) + if err != nil { + t.Fatalf("Error getting state: %s", err) + } + + // Determine the ID to import + var importId string + switch { + case step.ImportStateIdFunc != nil: + logging.HelperResourceTrace(ctx, "Using TestStep ImportStateIdFunc for import identifier") + + var err error + + logging.HelperResourceDebug(ctx, "Calling TestStep ImportStateIdFunc") + + importId, err = step.ImportStateIdFunc(state) + + if err != nil { + t.Fatal(err) + } + + logging.HelperResourceDebug(ctx, "Called TestStep ImportStateIdFunc") + case step.ImportStateId != "": + logging.HelperResourceTrace(ctx, "Using TestStep ImportStateId for import identifier") + + importId = step.ImportStateId + default: + logging.HelperResourceTrace(ctx, "Using resource identifier for import identifier") + + resource, err := testResource(resourceName, state) + if err != nil { + t.Fatal(err) + } + importId = resource.Primary.ID + } + + if step.ImportStateIdPrefix != "" { + logging.HelperResourceTrace(ctx, "Prepending TestStep ImportStateIdPrefix for import identifier") + + importId = step.ImportStateIdPrefix + importId + } + + logging.HelperResourceTrace(ctx, fmt.Sprintf("Using import identifier: %s", importId)) + + var priorIdentityValues map[string]any + + if kind.plannable() && kind.resourceIdentity() { + 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) + } + } + + testStepConfigRequest := config.TestStepConfigRequest{ + StepNumber: stepNumber, + TestName: t.Name(), + } + testStepConfig := teststep.Configuration(teststep.PrepareConfigurationRequest{ + Directory: step.ConfigDirectory, + File: step.ConfigFile, + Raw: step.Config, + TestStepConfigRequest: testStepConfigRequest, + }.Exec()) + + // If the current import state test step doesn't have configuration, use the prior test step config + if testStepConfig == nil { + if priorStepCfg == nil { + t.Fatal("Cannot import state with no specified config") + } + + logging.HelperResourceTrace(ctx, "Using prior TestStep Config for import") + + testStepConfig = priorStepCfg + } + + switch { + case step.ImportStateConfigExact: + break + + case kind.plannable() && kind.resourceIdentity(): + testStepConfig = appendImportBlockWithIdentity(testStepConfig, resourceName, priorIdentityValues) + + case kind.plannable(): + testStepConfig = appendImportBlock(testStepConfig, resourceName, importId) + } + + var workingDir *plugintest.WorkingDir + if importStatePersist { + workingDir = testCaseWorkingDir + } else { + workingDir = helper.RequireNewWorkingDir(ctx, t, "") + defer workingDir.Close() + } + + err = workingDir.SetConfig(ctx, testStepConfig, step.ConfigVariables) + if err != nil { + t.Fatalf("Error setting test config: %s", err) + } + + if kind.plannable() { + if stepNumber > 1 { + err = workingDir.CopyState(ctx, testCaseWorkingDir.StateFilePath()) + if err != nil { + t.Fatalf("copying state: %s", err) + } + + err = runProviderCommand(ctx, t, workingDir, providers, func() error { + return workingDir.RemoveResource(ctx, resourceName) + }) + if err != nil { + t.Fatalf("removing resource %s from copied state: %s", resourceName, err) + } + } + } + + if !importStatePersist { + err = runProviderCommand(ctx, t, workingDir, providers, func() error { + return workingDir.Init(ctx) + }) + if err != nil { + t.Fatalf("Error running init: %s", err) + } + } + + if kind.plannable() { + return testImportBlock(ctx, t, workingDir, providers, resourceName, step, priorIdentityValues) + } else { + return testImportCommand(ctx, t, workingDir, providers, resourceName, importId, step, state) + } +} + +func testImportBlock(ctx context.Context, t testing.T, workingDir *plugintest.WorkingDir, providers *providerFactories, resourceName string, step TestStep, priorIdentityValues map[string]any) error { + kind := step.ImportStateKind + + err := runProviderCommandCreatePlan(ctx, t, workingDir, providers) + if err != nil { + return fmt.Errorf("generating plan with import config: %s", err) + } + + plan, err := runProviderCommandSavedPlan(ctx, t, workingDir, providers) + if err != nil { + return fmt.Errorf("reading generated plan with import config: %s", err) + } + + logging.HelperResourceDebug(ctx, fmt.Sprintf("ImportBlockWithId: %d resource changes", len(plan.ResourceChanges))) + + // Verify reasonable things about the plan + var resourceChangeUnderTest *tfjson.ResourceChange + + if len(plan.ResourceChanges) == 0 { + return fmt.Errorf("importing resource %s: expected a resource change, got no changes", resourceName) + } + + for _, change := range plan.ResourceChanges { + if change.Address == resourceName { + resourceChangeUnderTest = change + } + } + + if resourceChangeUnderTest == nil || resourceChangeUnderTest.Change == nil || resourceChangeUnderTest.Change.Actions == nil { + return fmt.Errorf("importing resource %s: expected a resource change, got no changes", resourceName) + } + + change := resourceChangeUnderTest.Change + actions := change.Actions + importing := change.Importing + + switch { + case importing == nil: + return fmt.Errorf("importing resource %s: expected an import operation, got %q action with plan \nstdout:\n\n%s", resourceChangeUnderTest.Address, actions, savedPlanRawStdout(ctx, t, workingDir, providers)) + // By default we want to ensure there isn't a proposed plan after importing, but for some resources this is unavoidable. + // An example would be importing a resource that cannot read it's entire value back from the remote API. + case !step.ExpectNonEmptyPlan && !actions.NoOp(): + return fmt.Errorf("importing resource %s: expected a no-op import operation, got %q action with plan \nstdout:\n\n%s", resourceChangeUnderTest.Address, actions, savedPlanRawStdout(ctx, t, workingDir, providers)) + } + + if err := runPlanChecks(ctx, t, plan, step.ImportPlanChecks.PreApply); err != nil { + return err + } + + if kind.resourceIdentity() { + 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 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) + }) + if err != nil { + return err + } + + var importState *terraform.State + err = runProviderCommand(ctx, t, workingDir, providers, func() error { + _, importState, err = getState(ctx, t, workingDir) + if err != nil { + return err + } + return nil + }) + if err != nil { + t.Fatalf("Error getting state: %s", err) + } + + logging.HelperResourceDebug(ctx, fmt.Sprintf("State after import: %d resources in the root module", len(importState.RootModule().Resources))) + + // Go through the imported state and verify + if step.ImportStateCheck != nil { + logging.HelperResourceTrace(ctx, "Using TestStep ImportStateCheck") + runImportStateCheckFunction(ctx, t, importState, step) + } + + // Verify that all the states match + if step.ImportStateVerify { + logging.HelperResourceTrace(ctx, "Using TestStep ImportStateVerify") + + // Ensure that we do not match against data sources as they + // cannot be imported and are not what we want to verify. + // Mode is not present in ResourceState so we use the + // stringified ResourceStateKey for comparison. + newResources := make(map[string]*terraform.ResourceState) + for k, v := range importState.RootModule().Resources { + if !strings.HasPrefix(k, "data.") { + newResources[k] = v + } + } + oldResources := make(map[string]*terraform.ResourceState) + for k, v := range state.RootModule().Resources { + if !strings.HasPrefix(k, "data.") { + oldResources[k] = v + } + } + + identifierAttribute := step.ImportStateVerifyIdentifierAttribute + + if identifierAttribute == "" { + identifierAttribute = "id" + } + + for _, r := range newResources { + rIdentifier, ok := r.Primary.Attributes[identifierAttribute] + + if !ok { + t.Fatalf("ImportStateVerify: New resource missing identifier attribute %q, ensure attribute value is properly set or use ImportStateVerifyIdentifierAttribute to choose different attribute", identifierAttribute) + } + + // Find the existing resource + var oldR *terraform.ResourceState + for _, r2 := range oldResources { + if r2.Primary == nil || r2.Type != r.Type || r2.Provider != r.Provider { + continue + } + + r2Identifier, ok := r2.Primary.Attributes[identifierAttribute] + + if !ok { + t.Fatalf("ImportStateVerify: Old resource missing identifier attribute %q, ensure attribute value is properly set or use ImportStateVerifyIdentifierAttribute to choose different attribute", identifierAttribute) + } + + if r2Identifier == rIdentifier { + oldR = r2 + break + } + } + if oldR == nil || oldR.Primary == nil { + t.Fatalf( + "Failed state verification, resource with ID %s not found", + rIdentifier) + } + + // don't add empty flatmapped containers, so we can more easily + // compare the attributes + skipEmpty := func(k, v string) bool { + if strings.HasSuffix(k, ".#") || strings.HasSuffix(k, ".%") { + if v == "0" { + return true + } + } + return false + } + + // Compare their attributes + actual := make(map[string]string) + for k, v := range r.Primary.Attributes { + if skipEmpty(k, v) { + continue + } + actual[k] = v + } + + expected := make(map[string]string) + for k, v := range oldR.Primary.Attributes { + if skipEmpty(k, v) { + continue + } + expected[k] = v + } + + // Remove fields we're ignoring + for _, v := range step.ImportStateVerifyIgnore { + for k := range actual { + if strings.HasPrefix(k, v) { + delete(actual, k) + } + } + for k := range expected { + if strings.HasPrefix(k, v) { + delete(expected, k) + } + } + } + + // timeouts are only _sometimes_ added to state. To + // account for this, just don't compare timeouts at + // all. + for k := range actual { + if strings.HasPrefix(k, "timeouts.") { + delete(actual, k) + } + if k == "timeouts" { + delete(actual, k) + } + } + for k := range expected { + if strings.HasPrefix(k, "timeouts.") { + delete(expected, k) + } + if k == "timeouts" { + delete(expected, k) + } + } + + if !reflect.DeepEqual(actual, expected) { + // Determine only the different attributes + // go-cmp tries to show surrounding identical map key/value for + // context of differences, which may be confusing. + for k, v := range expected { + if av, ok := actual[k]; ok && v == av { + delete(expected, k) + delete(actual, k) + } + } + + if diff := cmp.Diff(expected, actual); diff != "" { + return fmt.Errorf("ImportStateVerify attributes not equivalent. Difference is shown below. The - symbol indicates attributes missing after import.\n\n%s", diff) + } + } + } + } + + return nil +} + +func appendImportBlock(config teststep.Config, resourceName string, importID string) teststep.Config { + return config.Append( + fmt.Sprintf(``+"\n"+ + `import {`+"\n"+ + ` to = %s`+"\n"+ + ` id = %q`+"\n"+ + `}`, + resourceName, importID)) +} + +func appendImportBlockWithIdentity(config teststep.Config, resourceName string, identityValues map[string]any) teststep.Config { + configBuilder := strings.Builder{} + configBuilder.WriteString(fmt.Sprintf(``+"\n"+ + `import {`+"\n"+ + ` to = %s`+"\n"+ + ` identity = {`+"\n", + resourceName)) + + for k, v := range identityValues { + // It's valid for identity attributes to be null, we can just omit it from config + if v == nil { + continue + } + + switch v := v.(type) { + case bool: + configBuilder.WriteString(fmt.Sprintf(` %q = %t`+"\n", k, v)) + + case []any: + var quotedV []string + for _, v := range v { + quotedV = append(quotedV, fmt.Sprintf(`%q`, v)) + } + configBuilder.WriteString(fmt.Sprintf(` %q = [%s]`+"\n", k, strings.Join(quotedV, ", "))) + + case json.Number: + configBuilder.WriteString(fmt.Sprintf(` %q = %s`+"\n", k, v)) + + case string: + configBuilder.WriteString(fmt.Sprintf(` %q = %q`+"\n", k, v)) + + default: + panic(fmt.Sprintf("unexpected type %T for identity value %q", v, k)) + } + } + + configBuilder.WriteString(` }` + "\n") + configBuilder.WriteString(`}` + "\n") + + return config.Append(configBuilder.String()) +} + +func importStatePreconditions(t testing.T, helper *plugintest.Helper, step TestStep) error { + t.Helper() + + kind := step.ImportStateKind + versionUnderTest := *helper.TerraformVersion().Core() + resourceIdentityMinimumVersion := version.Must(version.NewVersion("1.12.0")) + + // Instead of calling [t.Fatal], we return an error. This package's unit tests can use [TestStep.ExpectError] to match + // on the error message. An alternative, [plugintest.TestExpectTFatal], does not have access to logged error messages, + // so it is open to false positives on this complex code path. + // + // Multiple cases may match, so check the most specific cases first + switch { + case kind.resourceIdentity() && versionUnderTest.LessThan(resourceIdentityMinimumVersion): + return fmt.Errorf( + `ImportState steps using resource identity require Terraform 1.12.0 or later. Either ` + + `upgrade the Terraform version running the test or add a ` + "`TerraformVersionChecks`" + ` to ` + + `the test case to skip this test.` + "\n\n" + + `https://developer.hashicorp.com/terraform/plugin/testing/acceptance-tests/tfversion-checks#skip-version-checks`) + + case kind.plannable() && versionUnderTest.LessThan(tfversion.Version1_5_0): + return fmt.Errorf( + `ImportState steps using plannable import blocks require Terraform 1.5.0 or later. Either ` + + `upgrade the Terraform version running the test or add a ` + "`TerraformVersionChecks`" + ` to ` + + `the test case to skip this test.` + "\n\n" + + `https://developer.hashicorp.com/terraform/plugin/testing/acceptance-tests/tfversion-checks#skip-version-checks`) + + case kind.plannable() && step.ImportStatePersist: + return fmt.Errorf(`ImportStatePersist is not supported with plannable import blocks`) + + case kind.plannable() && step.ImportStateVerify: + return fmt.Errorf(`ImportStateVerify is not supported with plannable import blocks`) + } + + return nil +} + +func resourcesFromState(stateValues *tfjson.StateValues) []*tfjson.StateResource { + if stateValues == nil || stateValues.RootModule == nil { + return []*tfjson.StateResource{} + } + + return stateValues.RootModule.Resources +} + +func identityValuesFromStateValues(stateValues *tfjson.StateValues, resourceName string) map[string]any { + var resource *tfjson.StateResource + resources := resourcesFromState(stateValues) + + for _, r := range resources { + if r.Address == resourceName { + resource = r + break + } + } + + if resource == nil || len(resource.IdentityValues) == 0 { + return map[string]any{} + } + + return resource.IdentityValues +} + +func runImportStateCheckFunction(ctx context.Context, t testing.T, importState *terraform.State, step TestStep) { + t.Helper() + + var states []*terraform.InstanceState + for address, r := range importState.RootModule().Resources { + if strings.HasPrefix(address, "data.") { + continue + } + + if r.Primary == nil { + continue + } + + is := r.Primary.DeepCopy() //nolint:staticcheck // legacy usage + is.Ephemeral.Type = r.Type // otherwise the check function cannot see the type + states = append(states, is) + } + + logging.HelperResourceTrace(ctx, "Calling TestStep ImportStateCheck") + + if err := step.ImportStateCheck(states); err != nil { + t.Fatal(err) + } + + logging.HelperResourceTrace(ctx, "Called TestStep ImportStateCheck") +} + +func savedPlanRawStdout(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, providers *providerFactories) string { + t.Helper() + + var stdout string + + err := runProviderCommand(ctx, t, wd, providers, func() error { + var err error + stdout, err = wd.SavedPlanRawStdout(ctx) + return err + }) + + if err != nil { + return fmt.Sprintf("error retrieving formatted plan output: %s", err) + } + return stdout +} diff --git a/querycheck/testing_new_refresh_state.go b/querycheck/testing_new_refresh_state.go new file mode 100644 index 00000000..62aaaad4 --- /dev/null +++ b/querycheck/testing_new_refresh_state.go @@ -0,0 +1,105 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "fmt" + + tfjson "github.com/hashicorp/terraform-json" + "github.com/mitchellh/go-testing-interface" + + "github.com/hashicorp/terraform-plugin-testing/terraform" + + "github.com/hashicorp/terraform-plugin-testing/internal/logging" + "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" +) + +func testStepNewRefreshState(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, step TestStep, providers *providerFactories) error { + t.Helper() + + var err error + // Explicitly ensure prior state exists before refresh. + err = runProviderCommand(ctx, t, wd, providers, func() error { + _, _, err = getState(ctx, t, wd) + if err != nil { + return err + } + return nil + }) + if err != nil { + t.Fatalf("Error getting state: %s", err) + } + + err = runProviderCommand(ctx, t, wd, providers, func() error { + return wd.Refresh(ctx) + }) + if err != nil { + return err + } + + var refreshState *terraform.State + err = runProviderCommand(ctx, t, wd, providers, func() error { + _, refreshState, err = getState(ctx, t, wd) + if err != nil { + return err + } + return nil + }) + if err != nil { + t.Fatalf("Error getting state: %s", err) + } + + // Go through the refreshed state and verify + if step.Check != nil { + logging.HelperResourceDebug(ctx, "Calling TestStep Check for RefreshState") + + if err := step.Check(refreshState); err != nil { + t.Fatal(err) + } + + logging.HelperResourceDebug(ctx, "Called TestStep Check for RefreshState") + } + + // do a plan + err = runProviderCommand(ctx, t, wd, providers, func() error { + return wd.CreatePlan(ctx) + }) + if err != nil { + return fmt.Errorf("Error running post-refresh plan: %w", err) + } + + var plan *tfjson.Plan + err = runProviderCommand(ctx, t, wd, providers, func() error { + var err error + plan, err = wd.SavedPlan(ctx) + return err + }) + if err != nil { + return fmt.Errorf("Error retrieving post-refresh plan: %w", err) + } + + // Run post-refresh plan checks + if len(step.RefreshPlanChecks.PostRefresh) > 0 { + err = runPlanChecks(ctx, t, plan, step.RefreshPlanChecks.PostRefresh) + if err != nil { + return fmt.Errorf("Post-refresh plan check(s) failed:\n%w", err) + } + } + + if !planIsEmpty(plan, wd.GetHelper().TerraformVersion()) && !step.ExpectNonEmptyPlan { + var stdout string + err = runProviderCommand(ctx, t, wd, providers, func() error { + var err error + stdout, err = wd.SavedPlanRawStdout(ctx) + return err + }) + if err != nil { + return fmt.Errorf("Error retrieving formatted plan output: %w", err) + } + return fmt.Errorf("After refreshing state during this test step, a followup plan was not empty.\nstdout:\n\n%s", stdout) + } + + return nil +} diff --git a/querycheck/teststep_providers.go b/querycheck/teststep_providers.go new file mode 100644 index 00000000..11eb6fba --- /dev/null +++ b/querycheck/teststep_providers.go @@ -0,0 +1,258 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/hashicorp/go-version" +) + +// tfBlockMinReqTFVersion is used to prevent errors arising from +// adding required providers to the terraform block when Terraform +// is any version prior to v1.0.0 +const tfBlockMinReqTFVersion = "1.0.0" + +// mergedConfig prepends any necessary terraform configuration blocks to the +// TestStep Config. +// +// If there are ExternalProviders configurations in either the TestCase or +// TestStep, the terraform configuration block should be included with the +// step configuration to prevent errors with providers outside the +// registry.terraform.io hostname or outside the hashicorp namespace. +// This is only necessary when using TestStep.Config. +// +// When TestStep.ConfigDirectory is used, the expectation is that the +// Terraform configuration files will specify a terraform configuration +// block and/or provider blocks as necessary. +func (s TestStep) mergedConfig(ctx context.Context, testCase TestCase, configHasTerraformBlock, configHasProviderBlock bool, tfVersion *version.Version) (string, error) { + var config strings.Builder + + // Prevent issues with existing configurations containing the terraform + // configuration block. + if configHasTerraformBlock { + config.WriteString(s.Config) + + return config.String(), nil + } + + if testCase.hasProviders(ctx) { + cfg, err := s.providerConfigTestCase(ctx, configHasProviderBlock, testCase, tfVersion) + + if err != nil { + return "", err + } + + config.WriteString(cfg) + } else { + cfg, err := s.providerConfig(ctx, configHasProviderBlock, tfVersion) + + if err != nil { + return "", err + } + + config.WriteString(cfg) + } + + config.WriteString(s.Config) + + return config.String(), nil +} + +// providerConfig takes the list of providers in a TestStep and returns a +// config with only empty provider blocks. This is useful for Import, where no +// config is provided, but the providers must be defined. +func (s TestStep) providerConfig(_ context.Context, skipProviderBlock bool, tfVersion *version.Version) (string, error) { + var providerBlocks, requiredProviderBlocks strings.Builder + + for name, externalProvider := range s.ExternalProviders { + if !skipProviderBlock { + providerBlocks.WriteString(fmt.Sprintf("provider %q {}\n", name)) + } + + if externalProvider.Source == "" && externalProvider.VersionConstraint == "" { + continue + } + + requiredProviderBlocks.WriteString(fmt.Sprintf(" %s = {\n", name)) + + if externalProvider.Source != "" { + requiredProviderBlocks.WriteString(fmt.Sprintf(" source = %q\n", externalProvider.Source)) + } + + if externalProvider.VersionConstraint != "" { + requiredProviderBlocks.WriteString(fmt.Sprintf(" version = %q\n", externalProvider.VersionConstraint)) + } + + requiredProviderBlocks.WriteString(" }\n") + } + + minReqVersion, err := version.NewVersion(tfBlockMinReqTFVersion) + + if err != nil { + return "", err + } + + for name := range s.ProviderFactories { + if tfVersion.LessThan(minReqVersion) { + break + } + + requiredProviderBlocks.WriteString(addTerraformBlockSource(name, s.Config)) + } + + for name := range s.ProtoV5ProviderFactories { + if tfVersion.LessThan(minReqVersion) { + break + } + + requiredProviderBlocks.WriteString(addTerraformBlockSource(name, s.Config)) + } + + for name := range s.ProtoV6ProviderFactories { + if tfVersion.LessThan(minReqVersion) { + break + } + + requiredProviderBlocks.WriteString(addTerraformBlockSource(name, s.Config)) + } + + if requiredProviderBlocks.Len() > 0 { + return fmt.Sprintf(` +terraform { + required_providers { +%[1]s + } +} + +%[2]s +`, strings.TrimSuffix(requiredProviderBlocks.String(), "\n"), providerBlocks.String()), nil + } + + return providerBlocks.String(), nil +} + +func (s TestStep) providerConfigTestCase(_ context.Context, skipProviderBlock bool, testCase TestCase, tfVersion *version.Version) (string, error) { + var providerBlocks, requiredProviderBlocks strings.Builder + + providerNames := make(map[string]struct{}, len(testCase.Providers)) + + for name := range testCase.Providers { + providerNames[name] = struct{}{} + } + + for name := range testCase.ProviderFactories { + delete(providerNames, name) + } + + // [BF] The Providers field handling predates the logic being moved to this + // method. It's not entirely clear to me at this time why this field + // is being used and not the others, but leaving it here just in case + // it does have a special purpose that wasn't being unit tested prior. + for name := range providerNames { + providerBlocks.WriteString(fmt.Sprintf("provider %q {}\n", name)) + + requiredProviderBlocks.WriteString(fmt.Sprintf(" %s = {\n", name)) + + requiredProviderBlocks.WriteString(" }\n") + } + + for name, externalProvider := range testCase.ExternalProviders { + if !skipProviderBlock { + providerBlocks.WriteString(fmt.Sprintf("provider %q {}\n", name)) + } + + if externalProvider.Source == "" && externalProvider.VersionConstraint == "" { + continue + } + + requiredProviderBlocks.WriteString(fmt.Sprintf(" %s = {\n", name)) + + if externalProvider.Source != "" { + requiredProviderBlocks.WriteString(fmt.Sprintf(" source = %q\n", externalProvider.Source)) + } + + if externalProvider.VersionConstraint != "" { + requiredProviderBlocks.WriteString(fmt.Sprintf(" version = %q\n", externalProvider.VersionConstraint)) + } + + requiredProviderBlocks.WriteString(" }\n") + } + + minReqVersion, err := version.NewVersion(tfBlockMinReqTFVersion) + + if err != nil { + return "", err + } + + for name := range testCase.ProviderFactories { + if tfVersion.LessThan(minReqVersion) { + break + } + + providerFactoryBlocks := addTerraformBlockSource(name, s.Config) + + if len(providerFactoryBlocks) > 0 { + requiredProviderBlocks.WriteString(providerFactoryBlocks) + } + } + + for name := range testCase.ProtoV5ProviderFactories { + if tfVersion.LessThan(minReqVersion) { + break + } + + protov5ProviderFactoryBlocks := addTerraformBlockSource(name, s.Config) + + if len(protov5ProviderFactoryBlocks) > 0 { + requiredProviderBlocks.WriteString(protov5ProviderFactoryBlocks) + } + } + + for name := range testCase.ProtoV6ProviderFactories { + if tfVersion.LessThan(minReqVersion) { + break + } + + protov6ProviderFactoryBlocks := addTerraformBlockSource(name, s.Config) + + if len(protov6ProviderFactoryBlocks) > 0 { + requiredProviderBlocks.WriteString(addTerraformBlockSource(name, s.Config)) + } + } + + if requiredProviderBlocks.Len() > 0 { + return fmt.Sprintf(` +terraform { + required_providers { +%[1]s + } +} + +%[2]s +`, strings.TrimSuffix(requiredProviderBlocks.String(), "\n"), providerBlocks.String()), nil + } + + return providerBlocks.String(), nil +} + +func addTerraformBlockSource(name, config string) string { + var js json.RawMessage + + // Do not process JSON. + if err := json.Unmarshal([]byte(config), &js); err == nil { + return "" + } + + var providerBlocks strings.Builder + + providerBlocks.WriteString(fmt.Sprintf(" %s = {\n", name)) + providerBlocks.WriteString(fmt.Sprintf(" source = %q\n", getProviderAddr(name))) + providerBlocks.WriteString(" }\n") + + return providerBlocks.String() +} diff --git a/querycheck/teststep_validate.go b/querycheck/teststep_validate.go new file mode 100644 index 00000000..bc8d90dc --- /dev/null +++ b/querycheck/teststep_validate.go @@ -0,0 +1,245 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/internal/logging" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" +) + +// testStepValidateRequest contains data for the (TestStep).validate() method. +type testStepValidateRequest struct { + // StepConfiguration contains the TestStep configuration derived from + // TestStep.Config, TestStep.ConfigDirectory, or TestStep.ConfigFile. + StepConfiguration teststep.Config + + // StepNumber is the index of the TestStep in the TestCase.Steps. + StepNumber int + + // TestCaseHasExternalProviders is enabled if the TestCase has + // ExternalProviders. + TestCaseHasExternalProviders bool + + // TestCaseHasProviders is enabled if the TestCase has set any of + // ExternalProviders, ProtoV5ProviderFactories, ProtoV6ProviderFactories, + // or ProviderFactories. + TestCaseHasProviders bool + + // TestName is the name of the test. + TestName string +} + +// hasExternalProviders returns true if the TestStep has +// ExternalProviders set. +func (s TestStep) hasExternalProviders() bool { + return len(s.ExternalProviders) > 0 +} + +// hasProviders returns true if the TestStep has set any of the +// ExternalProviders, ProtoV5ProviderFactories, ProtoV6ProviderFactories, or +// ProviderFactories fields. It will also return true if ConfigDirectory or +// Config contain terraform configuration which specify a provider block. +func (s TestStep) hasProviders(ctx context.Context, stepIndex int, testName string) (bool, error) { + if len(s.ExternalProviders) > 0 { + return true, nil + } + + if len(s.ProtoV5ProviderFactories) > 0 { + return true, nil + } + + if len(s.ProtoV6ProviderFactories) > 0 { + return true, nil + } + + if len(s.ProviderFactories) > 0 { + return true, nil + } + + configRequest := teststep.PrepareConfigurationRequest{ + Directory: s.ConfigDirectory, + File: s.ConfigFile, + TestStepConfigRequest: config.TestStepConfigRequest{ + StepNumber: stepIndex + 1, + TestName: testName, + }, + }.Exec() + + cfg := teststep.Configuration(configRequest) + + var cfgHasProviders bool + + if cfg != nil { + var err error + + cfgHasProviders, err = cfg.HasProviderBlock(ctx) + + if err != nil { + return false, err + } + } + + if cfgHasProviders { + return true, nil + } + + return false, nil +} + +// validate ensures the TestStep is valid based on the following criteria: +// +// - Config or ImportState or RefreshState is set. +// - Config and RefreshState are not both set. +// - RefreshState and Destroy are not both set. +// - RefreshState is not the first TestStep. +// - Providers are not specified (ExternalProviders, +// ProtoV5ProviderFactories, ProtoV6ProviderFactories, ProviderFactories) +// if specified at the TestCase level. +// - Providers are specified (ExternalProviders, ProtoV5ProviderFactories, +// ProtoV6ProviderFactories, ProviderFactories) if not specified at the +// TestCase level. +// - No overlapping ExternalProviders and ProviderFactories entries +// - ResourceName is not empty when ImportState is true, ImportStateIdFunc +// is not set, and ImportStateId is not set. +// - ConfigPlanChecks (PreApply, PostApplyPreRefresh, PostApplyPostRefresh) are only set when Config is set. +// - ConfigPlanChecks.PreApply are only set when PlanOnly is false. +// - RefreshPlanChecks (PostRefresh) are only set when RefreshState is set. +func (s TestStep) validate(ctx context.Context, req testStepValidateRequest) error { + ctx = logging.TestStepNumberContext(ctx, req.StepNumber) + + logging.HelperResourceTrace(ctx, "Validating TestStep") + + if req.StepConfiguration == nil && !s.ImportState && !s.RefreshState { + err := fmt.Errorf("TestStep missing Config or ConfigDirectory or ConfigFile or ImportState or RefreshState") + logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) + return err + } + + if req.StepConfiguration != nil && s.RefreshState { + err := fmt.Errorf("TestStep cannot have Config or ConfigDirectory or ConfigFile and RefreshState") + logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) + return err + } + + if s.RefreshState && s.Destroy { + err := fmt.Errorf("TestStep cannot have RefreshState and Destroy") + logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) + return err + } + + if s.RefreshState && req.StepNumber == 1 { + err := fmt.Errorf("TestStep cannot have RefreshState as first step") + logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) + return err + } + + if s.ImportState && s.RefreshState { + err := fmt.Errorf("TestStep cannot have ImportState and RefreshState in same step") + logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) + return err + } + + for name := range s.ExternalProviders { + if _, ok := s.ProviderFactories[name]; ok { + err := fmt.Errorf("TestStep provider %q set in both ExternalProviders and ProviderFactories", name) + logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) + return err + } + } + + if req.TestCaseHasExternalProviders && req.StepConfiguration != nil && req.StepConfiguration.HasConfigurationFiles() { + err := fmt.Errorf("Providers must only be specified within the terraform configuration files when using TestStep.Config") + logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) + return err + } + + if s.hasExternalProviders() && req.StepConfiguration != nil && req.StepConfiguration.HasConfigurationFiles() { + err := fmt.Errorf("Providers must only be specified within the terraform configuration files when using TestStep.Config") + logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) + return err + } + + // We need a 0-based step index for consistency + hasProviders, err := s.hasProviders(ctx, req.StepNumber-1, req.TestName) + + if err != nil { + logging.HelperResourceError(ctx, "TestStep error checking for providers", map[string]interface{}{logging.KeyError: err}) + return err + } + + if req.TestCaseHasProviders && hasProviders { + err := fmt.Errorf("Providers must only be specified either at the TestCase or TestStep level") + logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) + return err + } + + var cfgHasProviderBlock bool + + if req.StepConfiguration != nil { + cfgHasProviderBlock, err = req.StepConfiguration.HasProviderBlock(ctx) + + if err != nil { + logging.HelperResourceError(ctx, "TestStep error checking for if configuration has provider block", map[string]interface{}{logging.KeyError: err}) + return err + } + } + + if !req.TestCaseHasProviders && !hasProviders && !cfgHasProviderBlock { + err := fmt.Errorf("Providers must be specified at the TestCase level, or in all TestStep, or in TestStep.ConfigDirectory or TestStep.ConfigFile") + logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) + return err + } + + if s.ImportState { + if s.ImportStateId == "" && s.ImportStateIdFunc == nil && s.ResourceName == "" { + err := fmt.Errorf("TestStep ImportState must be specified with ImportStateId, ImportStateIdFunc, or ResourceName") + logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) + return err + } + } + + if len(s.ConfigPlanChecks.PreApply) > 0 { + if req.StepConfiguration == nil { + err := fmt.Errorf("TestStep ConfigPlanChecks.PreApply must only be specified with Config, ConfigDirectory or ConfigFile") + logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) + return err + } + + if s.PlanOnly { + err := fmt.Errorf("TestStep ConfigPlanChecks.PreApply cannot be run with PlanOnly") + logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) + return err + } + } + + if len(s.ConfigPlanChecks.PostApplyPreRefresh) > 0 && req.StepConfiguration == nil { + err := fmt.Errorf("TestStep ConfigPlanChecks.PostApplyPreRefresh must only be specified with Config, ConfigDirectory or ConfigFile") + logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) + return err + } + + if len(s.ConfigPlanChecks.PostApplyPostRefresh) > 0 && req.StepConfiguration == nil { + err := fmt.Errorf("TestStep ConfigPlanChecks.PostApplyPostRefresh must only be specified with Config, ConfigDirectory or ConfigFile") + logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) + return err + } + + if len(s.RefreshPlanChecks.PostRefresh) > 0 && !s.RefreshState { + err := fmt.Errorf("TestStep RefreshPlanChecks.PostRefresh must only be specified with RefreshState") + logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) + return err + } + + if len(s.ConfigStateChecks) > 0 && req.StepConfiguration == nil { + err := fmt.Errorf("TestStep ConfigStateChecks must only be specified with Config, ConfigDirectory or ConfigFile") + logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) + return err + } + + return nil +} diff --git a/querycheck/tfversion_checks.go b/querycheck/tfversion_checks.go new file mode 100644 index 00000000..50941749 --- /dev/null +++ b/querycheck/tfversion_checks.go @@ -0,0 +1,31 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + + "github.com/hashicorp/go-version" + "github.com/mitchellh/go-testing-interface" + + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func runTFVersionChecks(ctx context.Context, t testing.T, terraformVersion *version.Version, terraformVersionChecks []tfversion.TerraformVersionCheck) { + t.Helper() + + for _, tfVersionCheck := range terraformVersionChecks { + resp := tfversion.CheckTerraformVersionResponse{} + tfVersionCheck.CheckTerraformVersion(ctx, tfversion.CheckTerraformVersionRequest{TerraformVersion: terraformVersion}, &resp) + + if resp.Error != nil { + t.Fatalf(resp.Error.Error()) + } + + if resp.Skip != "" { + t.Skip(resp.Skip) + } + } + +} diff --git a/querycheck/types_test.go b/querycheck/types_test.go new file mode 100644 index 00000000..808f2478 --- /dev/null +++ b/querycheck/types_test.go @@ -0,0 +1,74 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func RequiredBoolAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.Bool, + Required: true, + } +} + +func OptionalComputedListAttribute(name string, elementType tftypes.Type) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.List{ElementType: elementType}, + Optional: true, + Computed: true, + } +} + +func RequiredListAttribute(name string, elementType tftypes.Type) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.List{ElementType: elementType}, + Required: true, + } +} + +func RequiredNumberAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.Number, + Required: true, + } +} + +func OptionalNumberAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.Number, + Optional: true, + } +} + +func ComputedStringAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.String, + Computed: true, + } +} + +func OptionalStringAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.String, + Optional: true, + } +} + +func RequiredStringAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.String, + Required: true, + } +} From 1b91d14a50dea48dedc2aecd7f70254c464c1165 Mon Sep 17 00:00:00 2001 From: Rain Date: Thu, 25 Sep 2025 14:32:14 -0400 Subject: [PATCH 4/9] Revert "Saving my place" This reverts commit ccc4965d1ad46a9d41e7585019f3acec4a4e57d8. --- querycheck/additional_cli_options.go | 29 - querycheck/contains_name_test.go | 49 +- querycheck/environment_variables.go | 45 - querycheck/examplecloud_list_test.go | 233 -- querycheck/examplecloud_test.go | 143 -- querycheck/expect_identity_test.go | 24 +- querycheck/expect_known_value_test.go | 24 +- .../expect_result_length_atleast_test.go | 24 +- querycheck/expect_result_length_exact_test.go | 24 +- querycheck/plan_checks.go | 28 - querycheck/plugin.go | 528 ---- querycheck/query_checks.go | 50 - querycheck/state_checks.go | 29 - querycheck/state_shim.go | 344 --- querycheck/testcase_providers.go | 61 - querycheck/testcase_test.go | 122 - querycheck/testcase_validate.go | 113 - querycheck/testing.go | 2161 ----------------- querycheck/testing_config.go | 29 - querycheck/testing_new.go | 701 ------ querycheck/testing_new_config.go | 469 ---- querycheck/testing_new_import_state.go | 571 ----- querycheck/testing_new_refresh_state.go | 105 - querycheck/teststep_providers.go | 258 -- querycheck/teststep_validate.go | 245 -- querycheck/tfversion_checks.go | 31 - querycheck/types_test.go | 74 - 27 files changed, 83 insertions(+), 6431 deletions(-) delete mode 100644 querycheck/additional_cli_options.go delete mode 100644 querycheck/environment_variables.go delete mode 100644 querycheck/examplecloud_list_test.go delete mode 100644 querycheck/examplecloud_test.go delete mode 100644 querycheck/plan_checks.go delete mode 100644 querycheck/plugin.go delete mode 100644 querycheck/query_checks.go delete mode 100644 querycheck/state_checks.go delete mode 100644 querycheck/state_shim.go delete mode 100644 querycheck/testcase_providers.go delete mode 100644 querycheck/testcase_test.go delete mode 100644 querycheck/testcase_validate.go delete mode 100644 querycheck/testing.go delete mode 100644 querycheck/testing_config.go delete mode 100644 querycheck/testing_new.go delete mode 100644 querycheck/testing_new_config.go delete mode 100644 querycheck/testing_new_import_state.go delete mode 100644 querycheck/testing_new_refresh_state.go delete mode 100644 querycheck/teststep_providers.go delete mode 100644 querycheck/teststep_validate.go delete mode 100644 querycheck/tfversion_checks.go delete mode 100644 querycheck/types_test.go diff --git a/querycheck/additional_cli_options.go b/querycheck/additional_cli_options.go deleted file mode 100644 index e1b9b5bf..00000000 --- a/querycheck/additional_cli_options.go +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -// AdditionalCLIOptions allows an intentionally limited set of options to be passed -// to the Terraform CLI when executing test steps. -type AdditionalCLIOptions struct { - // Apply represents options to be passed to the `terraform apply` command. - Apply ApplyOptions - - // Plan represents options to be passed to the `terraform plan` command. - Plan PlanOptions -} - -// ApplyOptions represents options to be passed to the `terraform apply` command. -type ApplyOptions struct { - // AllowDeferral will pass the experimental `-allow-deferral` flag to the apply command. - AllowDeferral bool -} - -// PlanOptions represents options to be passed to the `terraform plan` command. -type PlanOptions struct { - // AllowDeferral will pass the experimental `-allow-deferral` flag to the plan command. - AllowDeferral bool - - // NoRefresh will pass the `-refresh=false` flag to the plan command. - NoRefresh bool -} diff --git a/querycheck/contains_name_test.go b/querycheck/contains_name_test.go index 1c40873e..6b9cdb73 100644 --- a/querycheck/contains_name_test.go +++ b/querycheck/contains_name_test.go @@ -1,34 +1,37 @@ -package querycheck +package querycheck_test import ( - "regexp" - "testing" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" "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/querycheck" "github.com/hashicorp/terraform-plugin-testing/tfversion" + "regexp" + "testing" ) func TestContainsResourceWithName(t *testing.T) { t.Parallel() - UnitTest(t, TestCase{ + r.UnitTest(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ ListResources: map[string]testprovider.ListResource{ + // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use - "examplecloud_containerette": examplecloudListResource(), + + //"examplecloud_containerette": examplecloudListResource(), }, Resources: map[string]testprovider.Resource{ - "examplecloud_containerette": examplecloudResource(), + //"examplecloud_containerette": examplecloudResource(), }, }), }, - Steps: []TestStep{ + Steps: []r.TestStep{ // We'll skip the first test step where we simulate creating the resource that will be returned when we query for it for simplicity. { Query: true, @@ -51,13 +54,13 @@ func TestContainsResourceWithName(t *testing.T) { } } `, - QueryResultChecks: []QueryResultCheck{ - ContainsResourceWithName("examplecloud_containerette.test", "banane"), - ContainsResourceWithName("examplecloud_containerette.test", "ananas"), - ContainsResourceWithName("examplecloud_containerette.test", "kiwi"), - ContainsResourceWithName("examplecloud_containerette.test2", "papaya"), - ContainsResourceWithName("examplecloud_containerette.test2", "birne"), - ContainsResourceWithName("examplecloud_containerette.test2", "kirsche"), + QueryResultChecks: []querycheck.QueryResultCheck{ + querycheck.ContainsResourceWithName("examplecloud_containerette.test", "banane"), + querycheck.ContainsResourceWithName("examplecloud_containerette.test", "ananas"), + querycheck.ContainsResourceWithName("examplecloud_containerette.test", "kiwi"), + querycheck.ContainsResourceWithName("examplecloud_containerette.test2", "papaya"), + querycheck.ContainsResourceWithName("examplecloud_containerette.test2", "birne"), + querycheck.ContainsResourceWithName("examplecloud_containerette.test2", "kirsche"), }, }, }, @@ -68,22 +71,24 @@ func TestContainsResourceWithName(t *testing.T) { func TestContainsResourceWithName_NotFound(t *testing.T) { t.Parallel() - UnitTest(t, TestCase{ + r.UnitTest(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ ListResources: map[string]testprovider.ListResource{ - // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use - "examplecloud_containerette": examplecloudListResource(), + + // TODO: define a resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use + + //"examplecloud_containerette": examplecloudListResource(), }, Resources: map[string]testprovider.Resource{ - "examplecloud_containerette": examplecloudResource(), + //"examplecloud_containerette": examplecloudResource(), }, }), }, - Steps: []TestStep{ + Steps: []r.TestStep{ { Query: true, Config: ` @@ -105,8 +110,8 @@ func TestContainsResourceWithName_NotFound(t *testing.T) { } } `, - QueryResultChecks: []QueryResultCheck{ - ContainsResourceWithName("examplecloud_containerette.test", "pflaume"), + QueryResultChecks: []querycheck.QueryResultCheck{ + querycheck.ContainsResourceWithName("examplecloud_containerette.test", "pflaume"), }, // TODO update expected error message to match what we output ExpectError: regexp.MustCompile("examplecloud_containerette.test - there are no pflaumen here!"), diff --git a/querycheck/environment_variables.go b/querycheck/environment_variables.go deleted file mode 100644 index 35bea66f..00000000 --- a/querycheck/environment_variables.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -// Environment variables for acceptance testing. Additional environment -// variable constants can be found in the internal/plugintest package. -const ( - // Environment variable to enable acceptance tests using this package's - // ParallelTest and Test functions whose TestCase does not enable the - // IsUnitTest field. Defaults to disabled, in which each test will call - // (*testing.T).Skip(). Can be set to any value to enable acceptance tests, - // however "1" is conventional. - EnvTfAcc = "TF_ACC" - - // Environment variable with hostname for the provider under acceptance - // test. The hostname is the first portion of the full provider source - // address, such as "example.com" in example.com/myorg/myprovider. Defaults - // to "registry.terraform.io". - // - // Only required if any Terraform configuration set via the TestStep - // type Config field includes a provider source, such as the terraform - // configuration block required_providers attribute. - EnvTfAccProviderHost = "TF_ACC_PROVIDER_HOST" - - // Environment variable with namespace for the provider under acceptance - // test. The namespace is the second portion of the full provider source - // address, such as "myorg" in registry.terraform.io/myorg/myprovider. - // Defaults to "-" for Terraform 0.12-0.13 compatibility and "hashicorp". - // - // Only required if any Terraform configuration set via the TestStep - // type Config field includes a provider source, such as the terraform - // configuration block required_providers attribute. - EnvTfAccProviderNamespace = "TF_ACC_PROVIDER_NAMESPACE" - - // This is an undocumented compatibility flag. When this is set, a - // `Config`-mode test step will invoke a refresh before successful - // completion. - // - // This is a compatibility measure for test cases that have different -- - // but semantically-equal -- state representations in their test steps. - // When comparing two states, the testing framework is not aware of - // semantic equality or set equality. - EnvTfAccRefreshAfterApply = "TF_ACC_REFRESH_AFTER_APPLY" -) diff --git a/querycheck/examplecloud_list_test.go b/querycheck/examplecloud_list_test.go deleted file mode 100644 index 0e6302b6..00000000 --- a/querycheck/examplecloud_list_test.go +++ /dev/null @@ -1,233 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -import ( - "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/list" - "github.com/hashicorp/terraform-plugin-testing/internal/teststep" -) - -func examplecloudListResource() testprovider.ListResource { - return testprovider.ListResource{ - IncludeResource: true, - SchemaResponse: &list.SchemaResponse{ - Schema: &tfprotov6.Schema{ - Block: &tfprotov6.SchemaBlock{ - Attributes: []*tfprotov6.SchemaAttribute{ - { - Name: "resource_group_name", - Type: tftypes.String, - Required: true, - }, - }, - }, - }, - }, - ListResultsStream: &list.ListResultsStream{ - Results: func(push func(list.ListResult) bool) { - push(list.ListResult{ - Resource: teststep.Pointer(tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - "location": tftypes.String, - "name": tftypes.String, - "resource_group_name": tftypes.String, - "instances": tftypes.Number, - }, - }, - map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "foo/banane"), - "location": tftypes.NewValue(tftypes.String, "westeurope"), - "name": tftypes.NewValue(tftypes.String, "banane"), - "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), - "instances": tftypes.NewValue(tftypes.Number, 5), - }, - )), - Identity: teststep.Pointer(tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "resource_group_name": tftypes.String, - "name": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), - "name": tftypes.NewValue(tftypes.String, "banane"), - }, - )), - DisplayName: "banane", - }) - push(list.ListResult{ - Resource: teststep.Pointer(tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - "location": tftypes.String, - "name": tftypes.String, - "resource_group_name": tftypes.String, - "instances": tftypes.Number, - }, - }, - map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "foo/ananas"), - "location": tftypes.NewValue(tftypes.String, "westeurope"), - "name": tftypes.NewValue(tftypes.String, "ananas"), - "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), - "instances": tftypes.NewValue(tftypes.Number, 9000), - }, - )), - Identity: teststep.Pointer(tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "resource_group_name": tftypes.String, - "name": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), - "name": tftypes.NewValue(tftypes.String, "ananas"), - }, - )), - DisplayName: "ananas", - }) - push(list.ListResult{ - Resource: teststep.Pointer(tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - "location": tftypes.String, - "name": tftypes.String, - "resource_group_name": tftypes.String, - "instances": tftypes.Number, - }, - }, - map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "foo/kiwi"), - "location": tftypes.NewValue(tftypes.String, "westeurope"), - "name": tftypes.NewValue(tftypes.String, "kiwi"), - "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), - "instances": tftypes.NewValue(tftypes.Number, 88), - }, - )), - Identity: teststep.Pointer(tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "resource_group_name": tftypes.String, - "name": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), - "name": tftypes.NewValue(tftypes.String, "kiwi"), - }, - )), - DisplayName: "kiwi", - }) - push(list.ListResult{ - Resource: teststep.Pointer(tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - "location": tftypes.String, - "name": tftypes.String, - "resource_group_name": tftypes.String, - "instances": tftypes.Number, - }, - }, - map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "bar/papaya"), - "location": tftypes.NewValue(tftypes.String, "westeurope"), - "name": tftypes.NewValue(tftypes.String, "banane"), - "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), - "instances": tftypes.NewValue(tftypes.Number, 3), - }, - )), - Identity: teststep.Pointer(tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "resource_group_name": tftypes.String, - "name": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "resource_group_name": tftypes.NewValue(tftypes.String, "bar"), - "name": tftypes.NewValue(tftypes.String, "papaya"), - }, - )), - DisplayName: "papaya", - }) - push(list.ListResult{ - Resource: teststep.Pointer(tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - "location": tftypes.String, - "name": tftypes.String, - "resource_group_name": tftypes.String, - "instances": tftypes.Number, - }, - }, - map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "bar/birne"), - "location": tftypes.NewValue(tftypes.String, "westeurope"), - "name": tftypes.NewValue(tftypes.String, "birne"), - "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), - "instances": tftypes.NewValue(tftypes.Number, 8564), - }, - )), - Identity: teststep.Pointer(tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "resource_group_name": tftypes.String, - "name": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "resource_group_name": tftypes.NewValue(tftypes.String, "bar"), - "name": tftypes.NewValue(tftypes.String, "birne"), - }, - )), - DisplayName: "birne", - }) - push(list.ListResult{ - Resource: teststep.Pointer(tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - "location": tftypes.String, - "name": tftypes.String, - "resource_group_name": tftypes.String, - "instances": tftypes.Number, - }, - }, - map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "bar/kirsche"), - "location": tftypes.NewValue(tftypes.String, "westeurope"), - "name": tftypes.NewValue(tftypes.String, "kirsche"), - "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), - "instances": tftypes.NewValue(tftypes.Number, 500), - }, - )), - Identity: teststep.Pointer(tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "resource_group_name": tftypes.String, - "name": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "resource_group_name": tftypes.NewValue(tftypes.String, "bar"), - "name": tftypes.NewValue(tftypes.String, "kirsche"), - }, - )), - DisplayName: "kirsche", - }) - }, - }, - } -} diff --git a/querycheck/examplecloud_test.go b/querycheck/examplecloud_test.go deleted file mode 100644 index cdd4ce1d..00000000 --- a/querycheck/examplecloud_test.go +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -import ( - "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/resource" - "github.com/hashicorp/terraform-plugin-testing/internal/teststep" -) - -func examplecloudResource() testprovider.Resource { - return testprovider.Resource{ - CreateResponse: &resource.CreateResponse{ - NewState: tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - "location": tftypes.String, - "name": tftypes.String, - "resource_group_name": tftypes.String, - "instances": tftypes.Number, - }, - }, - map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "foo/banana"), - "location": tftypes.NewValue(tftypes.String, "westeurope"), - "name": tftypes.NewValue(tftypes.String, "banana"), - "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), - "instances": tftypes.NewValue(tftypes.Number, int64(5)), - }, - ), - NewIdentity: teststep.Pointer(tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "resource_group_name": tftypes.String, - "name": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), - "name": tftypes.NewValue(tftypes.String, "banana"), - }, - )), - }, - ReadResponse: &resource.ReadResponse{ - NewState: tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - "location": tftypes.String, - "name": tftypes.String, - "resource_group_name": tftypes.String, - "instances": tftypes.Number, - }, - }, - map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "foo/banana"), - "location": tftypes.NewValue(tftypes.String, "westeurope"), - "name": tftypes.NewValue(tftypes.String, "banana"), - "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), - "instances": tftypes.NewValue(tftypes.Number, int64(5)), - }, - ), - NewIdentity: teststep.Pointer(tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "resource_group_name": tftypes.String, - "name": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), - "name": tftypes.NewValue(tftypes.String, "banana"), - }, - )), - }, - ImportStateResponse: &resource.ImportStateResponse{ - State: tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - "location": tftypes.String, - "name": tftypes.String, - "resource_group_name": tftypes.String, - "instances": tftypes.Number, - }, - }, - map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "foo/banana"), - "location": tftypes.NewValue(tftypes.String, "westeurope"), - "name": tftypes.NewValue(tftypes.String, "banana"), - "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), - "instances": tftypes.NewValue(tftypes.Number, int64(5)), - }, - ), - Identity: teststep.Pointer(tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "resource_group_name": tftypes.String, - "name": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), - "name": tftypes.NewValue(tftypes.String, "banana"), - }, - )), - }, - SchemaResponse: &resource.SchemaResponse{ - Schema: &tfprotov6.Schema{ - Block: &tfprotov6.SchemaBlock{ - Attributes: []*tfprotov6.SchemaAttribute{ - ComputedStringAttribute("id"), - RequiredStringAttribute("location"), - RequiredStringAttribute("name"), - RequiredStringAttribute("resource_group_name"), - OptionalNumberAttribute("instances"), - }, - }, - }, - }, - IdentitySchemaResponse: &resource.IdentitySchemaResponse{ - Schema: &tfprotov6.ResourceIdentitySchema{ - Version: 1, - IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ - { - Name: "resource_group_name", - Type: tftypes.String, - RequiredForImport: true, - }, - { - Name: "name", - Type: tftypes.String, - RequiredForImport: true, - }, - }, - }, - }, - } -} diff --git a/querycheck/expect_identity_test.go b/querycheck/expect_identity_test.go index a2288921..d296949a 100644 --- a/querycheck/expect_identity_test.go +++ b/querycheck/expect_identity_test.go @@ -1,33 +1,35 @@ package querycheck import ( - "testing" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" "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/tfversion" + "testing" ) func TestExpectIdentity(t *testing.T) { t.Parallel() - UnitTest(t, TestCase{ + r.UnitTest(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ ListResources: map[string]testprovider.ListResource{ + // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use - "examplecloud_containerette": examplecloudListResource(), + + //"examplecloud_containerette": examplecloudListResource(), }, Resources: map[string]testprovider.Resource{ - "examplecloud_containerette": examplecloudResource(), + //"examplecloud_containerette": examplecloudResource(), }, }), }, - Steps: []TestStep{ + Steps: []r.TestStep{ // TODO }, }) @@ -37,22 +39,24 @@ func TestExpectIdentity(t *testing.T) { func TestExpectIdentity_NotFound(t *testing.T) { t.Parallel() - UnitTest(t, TestCase{ + r.UnitTest(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ ListResources: map[string]testprovider.ListResource{ + // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use - "examplecloud_containerette": examplecloudListResource(), + + //"examplecloud_containerette": examplecloudListResource(), }, Resources: map[string]testprovider.Resource{ - "examplecloud_containerette": examplecloudResource(), + //"examplecloud_containerette": examplecloudResource(), }, }), }, - Steps: []TestStep{ + Steps: []r.TestStep{ // TODO }, }) diff --git a/querycheck/expect_known_value_test.go b/querycheck/expect_known_value_test.go index 6c26bcf9..d0121e39 100644 --- a/querycheck/expect_known_value_test.go +++ b/querycheck/expect_known_value_test.go @@ -1,33 +1,35 @@ package querycheck import ( - "testing" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" "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/tfversion" + "testing" ) func TestExpectKnownValue(t *testing.T) { t.Parallel() - UnitTest(t, TestCase{ + r.UnitTest(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ ListResources: map[string]testprovider.ListResource{ + // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use - "examplecloud_containerette": examplecloudListResource(), + + //"examplecloud_containerette": examplecloudListResource(), }, Resources: map[string]testprovider.Resource{ - "examplecloud_containerette": examplecloudResource(), + //"examplecloud_containerette": examplecloudResource(), }, }), }, - Steps: []TestStep{ + Steps: []r.TestStep{ // TODO }, }) @@ -37,22 +39,24 @@ func TestExpectKnownValue(t *testing.T) { func TestExpectKnownValue_ValueIncorrect(t *testing.T) { t.Parallel() - UnitTest(t, TestCase{ + r.UnitTest(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ ListResources: map[string]testprovider.ListResource{ + // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use - "examplecloud_containerette": examplecloudListResource(), + + //"examplecloud_containerette": examplecloudListResource(), }, Resources: map[string]testprovider.Resource{ - "examplecloud_containerette": examplecloudResource(), + //"examplecloud_containerette": examplecloudResource(), }, }), }, - Steps: []TestStep{ + Steps: []r.TestStep{ // TODO }, }) diff --git a/querycheck/expect_result_length_atleast_test.go b/querycheck/expect_result_length_atleast_test.go index 7b770f9a..afb351bb 100644 --- a/querycheck/expect_result_length_atleast_test.go +++ b/querycheck/expect_result_length_atleast_test.go @@ -1,33 +1,35 @@ package querycheck import ( - "testing" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" "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/tfversion" + "testing" ) func TestResultLengthAtLeast(t *testing.T) { t.Parallel() - UnitTest(t, TestCase{ + r.UnitTest(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ ListResources: map[string]testprovider.ListResource{ + // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use - "examplecloud_containerette": examplecloudListResource(), + + //"examplecloud_containerette": examplecloudListResource(), }, Resources: map[string]testprovider.Resource{ - "examplecloud_containerette": examplecloudResource(), + //"examplecloud_containerette": examplecloudResource(), }, }), }, - Steps: []TestStep{ + Steps: []r.TestStep{ // TODO }, }) @@ -37,22 +39,24 @@ func TestResultLengthAtLeast(t *testing.T) { func TestResultLengthAtLeast_TooFewResults(t *testing.T) { t.Parallel() - UnitTest(t, TestCase{ + r.UnitTest(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ ListResources: map[string]testprovider.ListResource{ + // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use - "examplecloud_containerette": examplecloudListResource(), + + //"examplecloud_containerette": examplecloudListResource(), }, Resources: map[string]testprovider.Resource{ - "examplecloud_containerette": examplecloudResource(), + //"examplecloud_containerette": examplecloudResource(), }, }), }, - Steps: []TestStep{ + Steps: []r.TestStep{ // TODO }, }) diff --git a/querycheck/expect_result_length_exact_test.go b/querycheck/expect_result_length_exact_test.go index cd1811dc..e3346838 100644 --- a/querycheck/expect_result_length_exact_test.go +++ b/querycheck/expect_result_length_exact_test.go @@ -1,33 +1,35 @@ package querycheck import ( - "testing" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" "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/tfversion" + "testing" ) func TestResultLengthExact(t *testing.T) { t.Parallel() - UnitTest(t, TestCase{ + r.UnitTest(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ ListResources: map[string]testprovider.ListResource{ + // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use - "examplecloud_containerette": examplecloudListResource(), + + //"examplecloud_containerette": examplecloudListResource(), }, Resources: map[string]testprovider.Resource{ - "examplecloud_containerette": examplecloudResource(), + //"examplecloud_containerette": examplecloudResource(), }, }), }, - Steps: []TestStep{ + Steps: []r.TestStep{ // TODO }, }) @@ -37,22 +39,24 @@ func TestResultLengthExact(t *testing.T) { func TestResultLengthExact_WrongAmount(t *testing.T) { t.Parallel() - UnitTest(t, TestCase{ + r.UnitTest(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ ListResources: map[string]testprovider.ListResource{ + // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use - "examplecloud_containerette": examplecloudListResource(), + + //"examplecloud_containerette": examplecloudListResource(), }, Resources: map[string]testprovider.Resource{ - "examplecloud_containerette": examplecloudResource(), + //"examplecloud_containerette": examplecloudResource(), }, }), }, - Steps: []TestStep{ + Steps: []r.TestStep{ // TODO }, }) diff --git a/querycheck/plan_checks.go b/querycheck/plan_checks.go deleted file mode 100644 index 61754453..00000000 --- a/querycheck/plan_checks.go +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -import ( - "context" - "errors" - - tfjson "github.com/hashicorp/terraform-json" - "github.com/hashicorp/terraform-plugin-testing/plancheck" - "github.com/mitchellh/go-testing-interface" -) - -func runPlanChecks(ctx context.Context, t testing.T, plan *tfjson.Plan, planChecks []plancheck.PlanCheck) error { - t.Helper() - - var result []error - - for _, planCheck := range planChecks { - resp := plancheck.CheckPlanResponse{} - planCheck.CheckPlan(ctx, plancheck.CheckPlanRequest{Plan: plan}, &resp) - - result = append(result, resp.Error) - } - - return errors.Join(result...) -} diff --git a/querycheck/plugin.go b/querycheck/plugin.go deleted file mode 100644 index 2583d5be..00000000 --- a/querycheck/plugin.go +++ /dev/null @@ -1,528 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -import ( - "context" - "fmt" - "io" - "os" - "strings" - "sync" - - "github.com/hashicorp/go-hclog" - "github.com/hashicorp/terraform-exec/tfexec" - tfjson "github.com/hashicorp/terraform-json" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/plugin" - "github.com/mitchellh/go-testing-interface" - - "github.com/hashicorp/terraform-plugin-testing/internal/logging" - "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" -) - -// protov5ProviderFactory is a function which is called to start a protocol -// version 5 provider server. -type protov5ProviderFactory func() (tfprotov5.ProviderServer, error) - -// protov5ProviderFactories is a mapping of provider addresses to provider -// factory for protocol version 5 provider servers. -type protov5ProviderFactories map[string]func() (tfprotov5.ProviderServer, error) - -// merge combines provider factories. -// -// In case of an overlapping entry, the later entry will overwrite the previous -// value. -func (pf protov5ProviderFactories) merge(otherPfs ...protov5ProviderFactories) protov5ProviderFactories { - result := make(protov5ProviderFactories) - - for name, providerFactory := range pf { - result[name] = providerFactory - } - - for _, otherPf := range otherPfs { - for name, providerFactory := range otherPf { - result[name] = providerFactory - } - } - - return result -} - -// protov6ProviderFactory is a function which is called to start a protocol -// version 6 provider server. -type protov6ProviderFactory func() (tfprotov6.ProviderServer, error) - -// protov6ProviderFactories is a mapping of provider addresses to provider -// factory for protocol version 6 provider servers. -type protov6ProviderFactories map[string]func() (tfprotov6.ProviderServer, error) - -// merge combines provider factories. -// -// In case of an overlapping entry, the later entry will overwrite the previous -// value. -func (pf protov6ProviderFactories) merge(otherPfs ...protov6ProviderFactories) protov6ProviderFactories { - result := make(protov6ProviderFactories) - - for name, providerFactory := range pf { - result[name] = providerFactory - } - - for _, otherPf := range otherPfs { - for name, providerFactory := range otherPf { - result[name] = providerFactory - } - } - - return result -} - -// sdkProviderFactory is a function which is called to start a SDK provider -// server. -type sdkProviderFactory func() (*schema.Provider, error) - -// protov6ProviderFactories is a mapping of provider addresses to provider -// factory for protocol version 6 provider servers. -type sdkProviderFactories map[string]func() (*schema.Provider, error) - -// merge combines provider factories. -// -// In case of an overlapping entry, the later entry will overwrite the previous -// value. -func (pf sdkProviderFactories) merge(otherPfs ...sdkProviderFactories) sdkProviderFactories { - result := make(sdkProviderFactories) - - for name, providerFactory := range pf { - result[name] = providerFactory - } - - for _, otherPf := range otherPfs { - for name, providerFactory := range otherPf { - result[name] = providerFactory - } - } - - return result -} - -type providerFactories struct { - legacy sdkProviderFactories - protov5 protov5ProviderFactories - protov6 protov6ProviderFactories -} - -func runProviderCommandApplyRefreshOnly(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, factories *providerFactories) error { - t.Helper() - - fn := func() error { - return wd.Apply(ctx, tfexec.Refresh(true), tfexec.RefreshOnly(true)) - } - return runProviderCommand(ctx, t, wd, factories, fn) -} - -func runProviderCommandCreatePlan(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, factories *providerFactories) error { - t.Helper() - - fn := func() error { - return wd.CreatePlan(ctx) - } - return runProviderCommand(ctx, t, wd, factories, fn) -} - -func runProviderCommandSavedPlan(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, factories *providerFactories) (*tfjson.Plan, error) { - t.Helper() - - var plan *tfjson.Plan - fn := func() error { - var err error - plan, err = wd.SavedPlan(ctx) - return err - } - err := runProviderCommand(ctx, t, wd, factories, fn) - if err != nil { - return nil, err - } - - return plan, nil -} - -func runProviderCommand(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, factories *providerFactories, f func() error) error { - // don't point to this as a test failure location - // point to whatever called it - t.Helper() - - // This should not happen, but prevent panics just in case. - if factories == nil { - err := fmt.Errorf("Provider factories are missing to run Terraform command. Please report this bug in the testing framework.") - logging.HelperResourceError(ctx, err.Error()) - return err - } - - // Run the providers in the same process as the test runner using the - // reattach behavior in Terraform. This ensures we get test coverage - // and enables the use of delve as a debugger. - - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - // this is needed so Terraform doesn't default to expecting protocol 4; - // we're skipping the handshake because Terraform didn't launch the - // plugins. - os.Setenv("PLUGIN_PROTOCOL_VERSIONS", "5") - - // Acceptance testing does not need to call checkpoint as the output - // is not accessible, nor desirable if explicitly using - // TF_ACC_TERRAFORM_PATH or TF_ACC_TERRAFORM_VERSION environment variables. - // - // Avoid calling (tfexec.Terraform).SetEnv() as it will stop copying - // os.Environ() and prevents TF_VAR_ environment variable usage. - os.Setenv("CHECKPOINT_DISABLE", "1") - - // Terraform 0.12.X and 0.13.X+ treat namespaceless providers - // differently in terms of what namespace they default to. So we're - // going to set both variations, as we don't know which version of - // Terraform we're talking to. We're also going to allow overriding - // the host or namespace using environment variables. - var namespaces []string - host := "registry.terraform.io" - if v := os.Getenv(EnvTfAccProviderNamespace); v != "" { - namespaces = append(namespaces, v) - } else { - namespaces = append(namespaces, "-", "hashicorp") - } - if v := os.Getenv(EnvTfAccProviderHost); v != "" { - host = v - } - - // schema.Provider have a global stop context that is created outside - // the server context and have their own associated goroutine. Since - // Terraform does not call the StopProvider RPC to stop the server in - // reattach mode, ensure that we save these servers to later call that - // RPC and end those goroutines. - legacyProviderServers := make([]*schema.GRPCProviderServer, 0, len(factories.legacy)) - - // Spin up gRPC servers for every provider factory, start a - // WaitGroup to listen for all of the close channels. - var wg sync.WaitGroup - reattachInfo := map[string]tfexec.ReattachConfig{} - for providerName, factory := range factories.legacy { - // providerName may be returned as terraform-provider-foo, and - // we need just foo. So let's fix that. - providerName = strings.TrimPrefix(providerName, "terraform-provider-") - providerAddress := getProviderAddr(providerName) - - logging.HelperResourceTrace(ctx, "Creating sdkv2 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) - - provider, err := factory() - if err != nil { - return fmt.Errorf("unable to create provider %q from factory: %w", providerName, err) - } - - logging.HelperResourceTrace(ctx, "Created sdkv2 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) - - // keep track of the running factory, so we can make sure it's - // shut down. - wg.Add(1) - - grpcProviderServer := schema.NewGRPCProviderServer(provider) - legacyProviderServers = append(legacyProviderServers, grpcProviderServer) - - // Ensure StopProvider is always called when returning early. - defer grpcProviderServer.StopProvider(ctx, nil) //nolint:errcheck // does not return errors - - // configure the settings our plugin will be served with - // the GRPCProviderFunc wraps a non-gRPC provider server - // into a gRPC interface, and the logger just discards logs - // from go-plugin. - opts := &plugin.ServeOpts{ - GRPCProviderFunc: func() tfprotov5.ProviderServer { - return grpcProviderServer - }, - Logger: hclog.New(&hclog.LoggerOptions{ - Name: "plugintest", - Level: hclog.Trace, - Output: io.Discard, - }), - NoLogOutputOverride: true, - UseTFLogSink: t, - ProviderAddr: providerAddress, - } - - logging.HelperResourceTrace(ctx, "Starting sdkv2 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) - - config, closeCh, err := plugin.DebugServe(ctx, opts) - if err != nil { - return fmt.Errorf("unable to serve provider %q: %w", providerName, err) - } - - logging.HelperResourceTrace(ctx, "Started sdkv2 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) - - tfexecConfig := tfexec.ReattachConfig{ - Protocol: config.Protocol, - ProtocolVersion: config.ProtocolVersion, - Pid: config.Pid, - Test: config.Test, - Addr: tfexec.ReattachConfigAddr{ - Network: config.Addr.Network, - String: config.Addr.String, - }, - } - - // when the provider exits, remove one from the waitgroup - // so we can track when everything is done - go func(c <-chan struct{}) { - <-c - wg.Done() - }(closeCh) - - // set our provider's reattachinfo in our map, once - // for every namespace that different Terraform versions - // may expect. - for _, ns := range namespaces { - reattachInfo[strings.TrimSuffix(host, "/")+"/"+ - strings.TrimSuffix(ns, "/")+"/"+ - providerName] = tfexecConfig - } - } - - // Now spin up gRPC servers for every protov5 provider factory - // in the same way. - for providerName, factory := range factories.protov5 { - // providerName may be returned as terraform-provider-foo, and - // we need just foo. So let's fix that. - providerName = strings.TrimPrefix(providerName, "terraform-provider-") - providerAddress := getProviderAddr(providerName) - - // If the user has supplied the same provider in both - // ProviderFactories and ProtoV5ProviderFactories, they made a - // mistake and we should exit early. - for _, ns := range namespaces { - reattachString := strings.TrimSuffix(host, "/") + "/" + - strings.TrimSuffix(ns, "/") + "/" + - providerName - if _, ok := reattachInfo[reattachString]; ok { - return fmt.Errorf("Provider %s registered in both TestCase.ProviderFactories and TestCase.ProtoV5ProviderFactories: please use one or the other, or supply a muxed provider to TestCase.ProtoV5ProviderFactories.", providerName) - } - } - - logging.HelperResourceTrace(ctx, "Creating tfprotov5 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) - - provider, err := factory() - if err != nil { - return fmt.Errorf("unable to create provider %q from factory: %w", providerName, err) - } - - logging.HelperResourceTrace(ctx, "Created tfprotov5 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) - - // keep track of the running factory, so we can make sure it's - // shut down. - wg.Add(1) - - // configure the settings our plugin will be served with - // the GRPCProviderFunc wraps a non-gRPC provider server - // into a gRPC interface, and the logger just discards logs - // from go-plugin. - opts := &plugin.ServeOpts{ - GRPCProviderFunc: func() tfprotov5.ProviderServer { - return provider - }, - Logger: hclog.New(&hclog.LoggerOptions{ - Name: "plugintest", - Level: hclog.Trace, - Output: io.Discard, - }), - NoLogOutputOverride: true, - UseTFLogSink: t, - ProviderAddr: providerAddress, - } - - logging.HelperResourceTrace(ctx, "Starting tfprotov5 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) - - config, closeCh, err := plugin.DebugServe(ctx, opts) - if err != nil { - return fmt.Errorf("unable to serve provider %q: %w", providerName, err) - } - - logging.HelperResourceTrace(ctx, "Started tfprotov5 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) - - tfexecConfig := tfexec.ReattachConfig{ - Protocol: config.Protocol, - ProtocolVersion: config.ProtocolVersion, - Pid: config.Pid, - Test: config.Test, - Addr: tfexec.ReattachConfigAddr{ - Network: config.Addr.Network, - String: config.Addr.String, - }, - } - - // when the provider exits, remove one from the waitgroup - // so we can track when everything is done - go func(c <-chan struct{}) { - <-c - wg.Done() - }(closeCh) - - // set our provider's reattachinfo in our map, once - // for every namespace that different Terraform versions - // may expect. - for _, ns := range namespaces { - reattachString := strings.TrimSuffix(host, "/") + "/" + - strings.TrimSuffix(ns, "/") + "/" + - providerName - reattachInfo[reattachString] = tfexecConfig - } - } - - // Now spin up gRPC servers for every protov6 provider factory - // in the same way. - for providerName, factory := range factories.protov6 { - // providerName may be returned as terraform-provider-foo, and - // we need just foo. So let's fix that. - providerName = strings.TrimPrefix(providerName, "terraform-provider-") - providerAddress := getProviderAddr(providerName) - - // If the user has already registered this provider in - // ProviderFactories or ProtoV5ProviderFactories, they made a - // mistake and we should exit early. - for _, ns := range namespaces { - reattachString := strings.TrimSuffix(host, "/") + "/" + - strings.TrimSuffix(ns, "/") + "/" + - providerName - if _, ok := reattachInfo[reattachString]; ok { - return fmt.Errorf("Provider %s registered in both TestCase.ProtoV6ProviderFactories and either TestCase.ProviderFactories or TestCase.ProtoV5ProviderFactories: please use one of the three, or supply a muxed provider to TestCase.ProtoV5ProviderFactories.", providerName) - } - } - - logging.HelperResourceTrace(ctx, "Creating tfprotov6 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) - - provider, err := factory() - if err != nil { - return fmt.Errorf("unable to create provider %q from factory: %w", providerName, err) - } - - logging.HelperResourceTrace(ctx, "Created tfprotov6 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) - - // keep track of the running factory, so we can make sure it's - // shut down. - wg.Add(1) - - opts := &plugin.ServeOpts{ - GRPCProviderV6Func: func() tfprotov6.ProviderServer { - return provider - }, - Logger: hclog.New(&hclog.LoggerOptions{ - Name: "plugintest", - Level: hclog.Trace, - Output: io.Discard, - }), - NoLogOutputOverride: true, - UseTFLogSink: t, - ProviderAddr: providerAddress, - } - - logging.HelperResourceTrace(ctx, "Starting tfprotov6 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) - - config, closeCh, err := plugin.DebugServe(ctx, opts) - if err != nil { - return fmt.Errorf("unable to serve provider %q: %w", providerName, err) - } - - logging.HelperResourceTrace(ctx, "Started tfprotov6 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) - - tfexecConfig := tfexec.ReattachConfig{ - Protocol: config.Protocol, - ProtocolVersion: config.ProtocolVersion, - Pid: config.Pid, - Test: config.Test, - Addr: tfexec.ReattachConfigAddr{ - Network: config.Addr.Network, - String: config.Addr.String, - }, - } - - // when the provider exits, remove one from the waitgroup - // so we can track when everything is done - go func(c <-chan struct{}) { - <-c - wg.Done() - }(closeCh) - - // set our provider's reattachinfo in our map, once - // for every namespace that different Terraform versions - // may expect. - for _, ns := range namespaces { - reattachString := strings.TrimSuffix(host, "/") + "/" + - strings.TrimSuffix(ns, "/") + "/" + - providerName - reattachInfo[reattachString] = tfexecConfig - } - } - - // set the working directory reattach info that will tell Terraform how to - // connect to our various running servers. - wd.SetReattachInfo(ctx, reattachInfo) - - logging.HelperResourceTrace(ctx, "Calling wrapped Terraform CLI command") - - // ok, let's call whatever Terraform command the test was trying to - // call, now that we know it'll attach back to those servers we just - // started. - err := f() - if err != nil { - logging.HelperResourceWarn(ctx, "Error running Terraform CLI command", map[string]interface{}{logging.KeyError: err}) - } - - logging.HelperResourceTrace(ctx, "Called wrapped Terraform CLI command") - logging.HelperResourceTrace(ctx, "Stopping providers") - - // cancel the servers so they'll return. Otherwise, this closeCh won't - // get closed, and we'll hang here. - cancel() - - // For legacy providers, call the StopProvider RPC so the StopContext - // goroutine is cleaned up properly. - for _, legacyProviderServer := range legacyProviderServers { - legacyProviderServer.StopProvider(ctx, nil) //nolint:errcheck // does not return errors - } - - logging.HelperResourceTrace(ctx, "Waiting for providers to stop") - - // wait for the servers to actually shut down; it may take a moment for - // them to clean up, or whatever. - // TODO: add a timeout here? - // PC: do we need one? The test will time out automatically... - wg.Wait() - - logging.HelperResourceTrace(ctx, "Providers have successfully stopped") - - // once we've run the Terraform command, let's remove the reattach - // information from the WorkingDir's environment. The WorkingDir will - // persist until the next call, but the server in the reattach info - // doesn't exist anymore at this point, so the reattach info is no - // longer valid. In theory it should be overwritten in the next call, - // but just to avoid any confusing bug reports, let's just unset the - // environment variable altogether. - wd.UnsetReattachInfo() - - // return any error returned from the orchestration code running - // Terraform commands - return err -} - -func getProviderAddr(name string) string { - host := "registry.terraform.io" - namespace := "hashicorp" - if v := os.Getenv(EnvTfAccProviderNamespace); v != "" { - namespace = v - } - if v := os.Getenv(EnvTfAccProviderHost); v != "" { - host = v - } - return strings.TrimSuffix(host, "/") + "/" + - strings.TrimSuffix(namespace, "/") + "/" + - name -} diff --git a/querycheck/query_checks.go b/querycheck/query_checks.go deleted file mode 100644 index dcf18cb9..00000000 --- a/querycheck/query_checks.go +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -import ( - "context" - "errors" - "fmt" - - tfjson "github.com/hashicorp/terraform-json" - "github.com/mitchellh/go-testing-interface" -) - -func RunQueryChecks(ctx context.Context, t testing.T, query []tfjson.LogMsg, queryChecks []QueryResultCheck) error { - t.Helper() - - var result []error - - if query == nil { - result = append(result, fmt.Errorf("no query results found")) - } - - found := make([]tfjson.ListResourceFoundData, 0) - summary := tfjson.ListCompleteData{} - - for _, msg := range query { - switch v := msg.(type) { - case tfjson.ListResourceFoundMessage: - found = append(found, v.ListResourceFound) - case tfjson.ListCompleteMessage: - summary = v.ListComplete - // TODO diagnostics and errors? - default: - continue - } - } - - for _, queryCheck := range queryChecks { - resp := CheckQueryResponse{} - queryCheck.CheckQuery(ctx, CheckQueryRequest{ - Query: found, - QuerySummary: &summary, - }, &resp) - - result = append(result, resp.Error) - } - - return errors.Join(result...) -} diff --git a/querycheck/state_checks.go b/querycheck/state_checks.go deleted file mode 100644 index 91095473..00000000 --- a/querycheck/state_checks.go +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -import ( - "context" - "errors" - - tfjson "github.com/hashicorp/terraform-json" - "github.com/mitchellh/go-testing-interface" - - "github.com/hashicorp/terraform-plugin-testing/statecheck" -) - -func runStateChecks(ctx context.Context, t testing.T, state *tfjson.State, stateChecks []statecheck.StateCheck) error { - t.Helper() - - var result []error - - for _, stateCheck := range stateChecks { - resp := statecheck.CheckStateResponse{} - stateCheck.CheckState(ctx, statecheck.CheckStateRequest{State: state}, &resp) - - result = append(result, resp.Error) - } - - return errors.Join(result...) -} diff --git a/querycheck/state_shim.go b/querycheck/state_shim.go deleted file mode 100644 index b0f1b067..00000000 --- a/querycheck/state_shim.go +++ /dev/null @@ -1,344 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -import ( - "encoding/json" - "fmt" - "strconv" - - tfjson "github.com/hashicorp/terraform-json" - - "github.com/hashicorp/terraform-plugin-testing/terraform" - - "github.com/hashicorp/terraform-plugin-testing/internal/addrs" - "github.com/hashicorp/terraform-plugin-testing/internal/tfdiags" -) - -type shimmedState struct { - state *terraform.State -} - -func shimStateFromJson(jsonState *tfjson.State) (*terraform.State, error) { - state := terraform.NewState() //nolint:staticcheck // legacy usage - state.TFVersion = jsonState.TerraformVersion - - if jsonState.Values == nil { - // the state is empty - return state, nil - } - - for key, output := range jsonState.Values.Outputs { - os, err := shimOutputState(output) - if err != nil { - return nil, err - } - state.RootModule().Outputs[key] = os - } - - ss := &shimmedState{state} - err := ss.shimStateModule(jsonState.Values.RootModule) - if err != nil { - return nil, err - } - - return state, nil -} - -func shimOutputState(so *tfjson.StateOutput) (*terraform.OutputState, error) { - os := &terraform.OutputState{ - Sensitive: so.Sensitive, - } - - switch v := so.Value.(type) { - case string: - os.Type = "string" - os.Value = v - return os, nil - case []interface{}: - os.Type = "list" - if len(v) == 0 { - os.Value = v - return os, nil - } - - switch firstElem := v[0].(type) { - case string: - elements := make([]interface{}, len(v)) - for i, el := range v { - strElement, ok := el.(string) - // If the type of the element doesn't match the first elem, it's a tuple, return the original value - if !ok { - os.Value = v - return os, nil - } - elements[i] = strElement - } - os.Value = elements - case bool: - elements := make([]interface{}, len(v)) - for i, el := range v { - boolElement, ok := el.(bool) - // If the type of the element doesn't match the first elem, it's a tuple, return the original value - if !ok { - os.Value = v - return os, nil - } - - elements[i] = boolElement - } - os.Value = elements - // unmarshalled number from JSON will always be json.Number - case json.Number: - elements := make([]interface{}, len(v)) - for i, el := range v { - numberElement, ok := el.(json.Number) - // If the type of the element doesn't match the first elem, it's a tuple, return the original value - if !ok { - os.Value = v - return os, nil - } - - elements[i] = numberElement - } - os.Value = elements - case []interface{}: - os.Value = v - case map[string]interface{}: - os.Value = v - default: - return nil, fmt.Errorf("unexpected output list element type: %T", firstElem) - } - return os, nil - case map[string]interface{}: - os.Type = "map" - os.Value = v - return os, nil - case bool: - os.Type = "string" - os.Value = strconv.FormatBool(v) - return os, nil - // unmarshalled number from JSON will always be json.Number - case json.Number: - os.Type = "string" - os.Value = v.String() - return os, nil - } - - return nil, fmt.Errorf("unexpected output type: %T", so.Value) -} - -func (ss *shimmedState) shimStateModule(sm *tfjson.StateModule) error { - var path addrs.ModuleInstance - - if sm.Address == "" { - path = addrs.RootModuleInstance - } else { - var diags tfdiags.Diagnostics - path, diags = addrs.ParseModuleInstanceStr(sm.Address) - if diags.HasErrors() { - return diags.Err() - } - } - - mod := ss.state.AddModule(path) //nolint:staticcheck // legacy usage - for _, res := range sm.Resources { - resourceState, err := shimResourceState(res) - if err != nil { - return err - } - - key, err := shimResourceStateKey(res) - if err != nil { - return err - } - - mod.Resources[key] = resourceState - } - - if len(sm.ChildModules) > 0 { - return fmt.Errorf("Modules are not supported. Found %d modules.", - len(sm.ChildModules)) - } - return nil -} - -func shimResourceStateKey(res *tfjson.StateResource) (string, error) { - if res.Index == nil { - return res.Address, nil - } - - var mode terraform.ResourceMode - switch res.Mode { - case tfjson.DataResourceMode: - mode = terraform.DataResourceMode - case tfjson.ManagedResourceMode: - mode = terraform.ManagedResourceMode - default: - return "", fmt.Errorf("unexpected resource mode for %q", res.Address) - } - - var index int - switch idx := res.Index.(type) { - case json.Number: - i, err := idx.Int64() - if err != nil { - return "", fmt.Errorf("unexpected index value (%q) for %q, ", - idx, res.Address) - } - index = int(i) - default: - return "", fmt.Errorf("unexpected index type (%T) for %q, "+ - "for_each is not supported", res.Index, res.Address) - } - - rsk := &terraform.ResourceStateKey{ - Mode: mode, - Type: res.Type, - Name: res.Name, - Index: index, - } - - return rsk.String(), nil -} - -func shimResourceState(res *tfjson.StateResource) (*terraform.ResourceState, error) { - sf := &shimmedFlatmap{} - err := sf.FromMap(res.AttributeValues) - if err != nil { - return nil, err - } - attributes := sf.Flatmap() - - // The instance state identifier was a Terraform versions 0.11 and earlier - // concept which helped core and the then SDK determine if the resource - // should be removed and as an identifier value in the human readable - // output. This concept unfortunately carried over to the testing logic when - // the testing logic was mostly changed to use the public, machine-readable - // JSON interface with Terraform, rather than reusing prior internal logic - // from Terraform. Using the "id" attribute value for this identifier was - // the default implementation and therefore those older versions of - // Terraform required the attribute. This is no longer necessary after - // Terraform versions 0.12 and later. - // - // If the "id" attribute is not found, set the instance state identifier to - // a synthetic value that can hopefully lead someone encountering the value - // to these comments. The prior logic used to raise an error if the - // attribute was not present, but this value should now only be present in - // legacy logic of this Go module, such as unintentionally exported logic in - // the terraform package, and not encountered during normal testing usage. - // - // Reference: https://github.com/hashicorp/terraform-plugin-testing/issues/84 - instanceStateID, ok := attributes["id"] - - if !ok { - instanceStateID = "id-attribute-not-set" - } - - return &terraform.ResourceState{ - Provider: res.ProviderName, - Type: res.Type, - Primary: &terraform.InstanceState{ - ID: instanceStateID, - Attributes: attributes, - Meta: map[string]interface{}{ - "schema_version": int(res.SchemaVersion), - }, - Tainted: res.Tainted, - }, - Dependencies: res.DependsOn, - }, nil -} - -type shimmedFlatmap struct { - m map[string]string -} - -func (sf *shimmedFlatmap) FromMap(attributes map[string]interface{}) error { - if sf.m == nil { - sf.m = make(map[string]string, len(attributes)) - } - - return sf.AddMap("", attributes) -} - -func (sf *shimmedFlatmap) AddMap(prefix string, m map[string]interface{}) error { - for key, value := range m { - k := key - if prefix != "" { - k = fmt.Sprintf("%s.%s", prefix, key) - } - - err := sf.AddEntry(k, value) - if err != nil { - return fmt.Errorf("unable to add map key %q entry: %w", k, err) - } - } - - mapLength := "%" - if prefix != "" { - mapLength = fmt.Sprintf("%s.%s", prefix, "%") - } - - if err := sf.AddEntry(mapLength, strconv.Itoa(len(m))); err != nil { - return fmt.Errorf("unable to add map length %q entry: %w", mapLength, err) - } - - return nil -} - -func (sf *shimmedFlatmap) AddSlice(name string, elements []interface{}) error { - for i, elem := range elements { - key := fmt.Sprintf("%s.%d", name, i) - err := sf.AddEntry(key, elem) - if err != nil { - return fmt.Errorf("unable to add slice key %q entry: %w", key, err) - } - } - - sliceLength := fmt.Sprintf("%s.#", name) - if err := sf.AddEntry(sliceLength, strconv.Itoa(len(elements))); err != nil { - return fmt.Errorf("unable to add slice length %q entry: %w", sliceLength, err) - } - - return nil -} - -func (sf *shimmedFlatmap) AddEntry(key string, value interface{}) error { - switch el := value.(type) { - case nil: - // omit the entry - return nil - case bool: - sf.m[key] = strconv.FormatBool(el) - case json.Number: - sf.m[key] = el.String() - case string: - sf.m[key] = el - case map[string]interface{}: - err := sf.AddMap(key, el) - if err != nil { - return err - } - case []interface{}: - err := sf.AddSlice(key, el) - if err != nil { - return err - } - default: - // This should never happen unless terraform-json - // changes how attributes (types) are represented. - // - // We handle all types which the JSON unmarshaler - // can possibly produce - // https://golang.org/pkg/encoding/json/#Unmarshal - - return fmt.Errorf("%q: unexpected type (%T)", key, el) - } - return nil -} - -func (sf *shimmedFlatmap) Flatmap() map[string]string { - return sf.m -} diff --git a/querycheck/testcase_providers.go b/querycheck/testcase_providers.go deleted file mode 100644 index 1ea783cc..00000000 --- a/querycheck/testcase_providers.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -import ( - "context" - "fmt" - "strings" -) - -// providerConfig takes the list of providers in a TestCase and returns a -// config with only empty provider blocks. This is useful for Import, where no -// config is provided, but the providers must be defined. -func (c TestCase) providerConfig(_ context.Context, skipProviderBlock bool) string { - var providerBlocks, requiredProviderBlocks strings.Builder - - // [BF] The Providers field handling predates the logic being moved to this - // method. It's not entirely clear to me at this time why this field - // is being used and not the others, but leaving it here just in case - // it does have a special purpose that wasn't being unit tested prior. - for name := range c.Providers { - providerBlocks.WriteString(fmt.Sprintf("provider %q {}\n", name)) - } - - for name, externalProvider := range c.ExternalProviders { - if !skipProviderBlock { - providerBlocks.WriteString(fmt.Sprintf("provider %q {}\n", name)) - } - - if externalProvider.Source == "" && externalProvider.VersionConstraint == "" { - continue - } - - requiredProviderBlocks.WriteString(fmt.Sprintf(" %s = {\n", name)) - - if externalProvider.Source != "" { - requiredProviderBlocks.WriteString(fmt.Sprintf(" source = %q\n", externalProvider.Source)) - } - - if externalProvider.VersionConstraint != "" { - requiredProviderBlocks.WriteString(fmt.Sprintf(" version = %q\n", externalProvider.VersionConstraint)) - } - - requiredProviderBlocks.WriteString(" }\n") - } - - if requiredProviderBlocks.Len() > 0 { - return fmt.Sprintf(` -terraform { - required_providers { -%[1]s - } -} - -%[2]s -`, strings.TrimSuffix(requiredProviderBlocks.String(), "\n"), providerBlocks.String()) - } - - return providerBlocks.String() -} diff --git a/querycheck/testcase_test.go b/querycheck/testcase_test.go deleted file mode 100644 index 5a7d70e2..00000000 --- a/querycheck/testcase_test.go +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -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/datasource" - "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" -) - -// Reference: https://github.com/hashicorp/terraform-plugin-testing/issues/84 -func TestTestCase_NoDataSourceIdRequirement(t *testing.T) { - t.Parallel() - - UnitTest(t, TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories - }, - Steps: []TestStep{ - { - Check: ComposeAggregateTestCheckFunc( - TestCheckNoResourceAttr("data.test_datasource.test", "id"), - TestCheckResourceAttr("data.test_datasource.test", "not_id", "test"), - ), - Config: `data "test_datasource" "test" {}`, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "test": providerserver.NewProviderServer(testprovider.Provider{ - DataSources: map[string]testprovider.DataSource{ - "test_datasource": { - ReadResponse: &datasource.ReadResponse{ - State: tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "not_id": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "not_id": tftypes.NewValue(tftypes.String, "test"), - }, - ), - }, - SchemaResponse: &datasource.SchemaResponse{ - Schema: &tfprotov6.Schema{ - Block: &tfprotov6.SchemaBlock{ - Attributes: []*tfprotov6.SchemaAttribute{ - { - Name: "not_id", - Type: tftypes.String, - Computed: true, - }, - }, - }, - }, - }, - }, - }, - }), - }, - }, - }, - }) -} - -// Reference: https://github.com/hashicorp/terraform-plugin-testing/issues/84 -func TestTestCase_NoResourceIdRequirement(t *testing.T) { - t.Parallel() - - UnitTest(t, TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories - }, - Steps: []TestStep{ - { - Check: ComposeAggregateTestCheckFunc( - TestCheckNoResourceAttr("test_resource.test", "id"), - TestCheckResourceAttr("test_resource.test", "not_id", "test"), - ), - Config: `resource "test_resource" "test" {}`, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "test": providerserver.NewProviderServer(testprovider.Provider{ - Resources: map[string]testprovider.Resource{ - "test_resource": { - CreateResponse: &resource.CreateResponse{ - NewState: tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "not_id": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "not_id": tftypes.NewValue(tftypes.String, "test"), - }, - ), - }, - SchemaResponse: &resource.SchemaResponse{ - Schema: &tfprotov6.Schema{ - Block: &tfprotov6.SchemaBlock{ - Attributes: []*tfprotov6.SchemaAttribute{ - { - Name: "not_id", - Type: tftypes.String, - Computed: true, - }, - }, - }, - }, - }, - }, - }, - }), - }, - }, - }, - }) -} diff --git a/querycheck/testcase_validate.go b/querycheck/testcase_validate.go deleted file mode 100644 index c6f908da..00000000 --- a/querycheck/testcase_validate.go +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -import ( - "context" - "fmt" - - "github.com/mitchellh/go-testing-interface" - - "github.com/hashicorp/terraform-plugin-testing/config" - "github.com/hashicorp/terraform-plugin-testing/internal/logging" - "github.com/hashicorp/terraform-plugin-testing/internal/teststep" -) - -// hasProviders returns true if the TestCase has ExternalProviders set. -func (c TestCase) hasExternalProviders(_ context.Context) bool { - return len(c.ExternalProviders) > 0 -} - -// hasProviders returns true if the TestCase has set any of the -// ExternalProviders, ProtoV5ProviderFactories, ProtoV6ProviderFactories, -// ProviderFactories, or Providers fields. -func (c TestCase) hasProviders(_ context.Context) bool { - if len(c.ExternalProviders) > 0 { - return true - } - - if len(c.ProtoV5ProviderFactories) > 0 { - return true - } - - if len(c.ProtoV6ProviderFactories) > 0 { - return true - } - - if len(c.ProviderFactories) > 0 { - return true - } - - if len(c.Providers) > 0 { - return true - } - - return false -} - -// validate ensures the TestCase is valid based on the following criteria: -// -// - No overlapping ExternalProviders and Providers entries -// - No overlapping ExternalProviders and ProviderFactories entries -// - TestStep validations performed by the (TestStep).validate() method. -func (c TestCase) validate(ctx context.Context, t testing.T) error { - logging.HelperResourceTrace(ctx, "Validating TestCase") - - if len(c.Steps) == 0 { - err := fmt.Errorf("TestCase missing Steps") - logging.HelperResourceError(ctx, "TestCase validation error", map[string]interface{}{logging.KeyError: err}) - return err - } - - for name := range c.ExternalProviders { - if _, ok := c.Providers[name]; ok { - err := fmt.Errorf("TestCase provider %q set in both ExternalProviders and Providers", name) - logging.HelperResourceError(ctx, "TestCase validation error", map[string]interface{}{logging.KeyError: err}) - return err - } - - if _, ok := c.ProviderFactories[name]; ok { - err := fmt.Errorf("TestCase provider %q set in both ExternalProviders and ProviderFactories", name) - logging.HelperResourceError(ctx, "TestCase validation error", map[string]interface{}{logging.KeyError: err}) - return err - } - } - - testCaseHasExternalProviders := c.hasExternalProviders(ctx) - testCaseHasProviders := c.hasProviders(ctx) - - for stepIndex, step := range c.Steps { - stepNumber := stepIndex + 1 // Use 1-based index for humans - - configRequest := teststep.PrepareConfigurationRequest{ - Directory: step.ConfigDirectory, - File: step.ConfigFile, - Raw: step.Config, - TestStepConfigRequest: config.TestStepConfigRequest{ - StepNumber: stepNumber, - TestName: t.Name(), - }, - }.Exec() - - stepConfiguration := teststep.Configuration(configRequest) - - stepValidateReq := testStepValidateRequest{ - StepConfiguration: stepConfiguration, - StepNumber: stepNumber, - TestCaseHasExternalProviders: testCaseHasExternalProviders, - TestCaseHasProviders: testCaseHasProviders, - TestName: t.Name(), - } - - err := step.validate(ctx, stepValidateReq) - - if err != nil { - err := fmt.Errorf("TestStep %d/%d validation error: %w", stepNumber, len(c.Steps), err) - logging.HelperResourceError(ctx, "TestCase validation error", map[string]interface{}{logging.KeyError: err}) - return err - } - } - - return nil -} diff --git a/querycheck/testing.go b/querycheck/testing.go deleted file mode 100644 index a9d1b9ef..00000000 --- a/querycheck/testing.go +++ /dev/null @@ -1,2161 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -import ( - "context" - "errors" - "flag" - "fmt" - "log" - "os" - "regexp" - "strconv" - "strings" - "time" - - "github.com/mitchellh/go-testing-interface" - - "github.com/hashicorp/terraform-plugin-go/tfprotov5" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - - "github.com/hashicorp/terraform-plugin-testing/config" - "github.com/hashicorp/terraform-plugin-testing/plancheck" - "github.com/hashicorp/terraform-plugin-testing/statecheck" - "github.com/hashicorp/terraform-plugin-testing/terraform" - "github.com/hashicorp/terraform-plugin-testing/tfversion" - - "github.com/hashicorp/terraform-plugin-testing/internal/addrs" - "github.com/hashicorp/terraform-plugin-testing/internal/logging" - "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" -) - -// flagSweep is a flag available when running tests on the command line. It -// contains a comma separated list of regions to for the sweeper functions to -// run in. This flag bypasses the normal Test path and instead runs functions designed to -// clean up any leaked resources a testing environment could have created. It is -// a best effort attempt, and relies on Provider authors to implement "Sweeper" -// methods for resources. - -// Adding Sweeper methods with AddTestSweepers will -// construct a list of sweeper funcs to be called here. We iterate through -// regions provided by the sweep flag, and for each region we iterate through the -// tests, and exit on any errors. At time of writing, sweepers are ran -// sequentially, however they can list dependencies to be ran first. We track -// the sweepers that have been ran, so as to not run a sweeper twice for a given -// region. -// -// WARNING: -// Sweepers are designed to be destructive. You should not use the -sweep flag -// in any environment that is not strictly a test environment. Resources will be -// destroyed. - -var flagSweep = flag.String("sweep", "", "List of Regions to run available Sweepers") -var flagSweepAllowFailures = flag.Bool("sweep-allow-failures", false, "Enable to allow Sweeper Tests to continue after failures") -var flagSweepRun = flag.String("sweep-run", "", "Comma separated list of Sweeper Tests to run") -var sweeperFuncs map[string]*Sweeper - -// SweeperFunc is a signature for a function that acts as a sweeper. It -// accepts a string for the region that the sweeper is to be ran in. This -// function must be able to construct a valid client for that region. -type SweeperFunc func(r string) error - -type Sweeper struct { - // Name for sweeper. Must be unique to be ran by the Sweeper Runner - Name string - - // Dependencies list the const names of other Sweeper functions that must be ran - // prior to running this Sweeper. This is an ordered list that will be invoked - // recursively at the helper/resource level - Dependencies []string - - // Sweeper function that when invoked sweeps the Provider of specific - // resources - F SweeperFunc -} - -func init() { - sweeperFuncs = make(map[string]*Sweeper) -} - -// AddTestSweepers function adds a given name and Sweeper configuration -// pair to the internal sweeperFuncs map. Invoke this function to register a -// resource sweeper to be available for running when the -sweep flag is used -// with `go test`. Sweeper names must be unique to help ensure a given sweeper -// is only ran once per run. -func AddTestSweepers(name string, s *Sweeper) { - if _, ok := sweeperFuncs[name]; ok { - log.Fatalf("[ERR] Error adding (%s) to sweeperFuncs: function already exists in map", name) - } - - sweeperFuncs[name] = s -} - -// TestMain adds sweeper functionality to the "go test" command, otherwise -// tests are executed as normal. Most provider acceptance tests are written -// using the Test() function of this package, which imposes its own -// requirements and Terraform CLI behavior. Refer to that function's -// documentation for additional details. -// -// Sweepers enable infrastructure cleanup functions to be included with -// resource definitions, typically so developers can remove all resources of -// that resource type from testing infrastructure in case of failures that -// prevented the normal resource destruction behavior of acceptance tests. -// Use the AddTestSweepers() function to configure available sweepers. -// -// Sweeper flags added to the "go test" command: -// -// -sweep: Comma-separated list of locations/regions to run available sweepers. -// -sweep-allow-failures: Enable to allow other sweepers to run after failures. -// -sweep-run: Comma-separated list of resource type sweepers to run. Defaults -// to all sweepers. -// -// Refer to the Env prefixed constants for environment variables that further -// control testing functionality. -func TestMain(m interface { - Run() int -}) { - flag.Parse() - if *flagSweep != "" { - // parse flagSweep contents for regions to run - regions := strings.Split(*flagSweep, ",") - - // get filtered list of sweepers to run based on sweep-run flag - sweepers := filterSweepers(*flagSweepRun, sweeperFuncs) - - if _, err := runSweepers(regions, sweepers, *flagSweepAllowFailures); err != nil { - os.Exit(1) - } - } else { - exitCode := m.Run() - os.Exit(exitCode) - } -} - -func runSweepers(regions []string, sweepers map[string]*Sweeper, allowFailures bool) (map[string]map[string]error, error) { - var sweeperErrorFound bool - sweeperRunList := make(map[string]map[string]error) - - for _, region := range regions { - region = strings.TrimSpace(region) - - var regionSweeperErrorFound bool - regionSweeperRunList := make(map[string]error) - - start := time.Now() - log.Printf("[DEBUG] Running Sweepers for region (%s):\n", region) - for _, sweeper := range sweepers { - if err := runSweeperWithRegion(region, sweeper, sweepers, regionSweeperRunList, allowFailures); err != nil { - if allowFailures { - continue - } - - sweeperRunList[region] = regionSweeperRunList - return sweeperRunList, fmt.Errorf("sweeper (%s) for region (%s) failed: %s", sweeper.Name, region, err) - } - } - elapsed := time.Since(start) - log.Printf("Completed Sweepers for region (%s) in %s", region, elapsed) - - log.Printf("Sweeper Tests for region (%s) ran successfully:\n", region) - for sweeper, sweeperErr := range regionSweeperRunList { - if sweeperErr == nil { - log.Printf("\t- %s\n", sweeper) - } else { - regionSweeperErrorFound = true - } - } - - if regionSweeperErrorFound { - sweeperErrorFound = true - log.Printf("Sweeper Tests for region (%s) ran unsuccessfully:\n", region) - for sweeper, sweeperErr := range regionSweeperRunList { - if sweeperErr != nil { - log.Printf("\t- %s: %s\n", sweeper, sweeperErr) - } - } - } - - sweeperRunList[region] = regionSweeperRunList - } - - if sweeperErrorFound { - return sweeperRunList, errors.New("at least one sweeper failed") - } - - return sweeperRunList, nil -} - -// filterSweepers takes a comma separated string listing the names of sweepers -// to be ran, and returns a filtered set from the list of all of sweepers to -// run based on the names given. -func filterSweepers(f string, source map[string]*Sweeper) map[string]*Sweeper { - filterSlice := strings.Split(strings.ToLower(f), ",") - if len(filterSlice) == 1 && filterSlice[0] == "" { - // if the filter slice is a single element of "" then no sweeper list was - // given, so just return the full list - return source - } - - sweepers := make(map[string]*Sweeper) - for name := range source { - for _, s := range filterSlice { - if strings.Contains(strings.ToLower(name), s) { - for foundName, foundSweeper := range filterSweeperWithDependencies(name, source) { - sweepers[foundName] = foundSweeper - } - } - } - } - return sweepers -} - -// filterSweeperWithDependencies recursively returns sweeper and all dependencies. -// Since filterSweepers performs fuzzy matching, this function is used -// to perform exact sweeper and dependency lookup. -func filterSweeperWithDependencies(name string, source map[string]*Sweeper) map[string]*Sweeper { - result := make(map[string]*Sweeper) - - currentSweeper, ok := source[name] - if !ok { - log.Printf("[WARN] Sweeper has dependency (%s), but that sweeper was not found", name) - return result - } - - result[name] = currentSweeper - - for _, dependency := range currentSweeper.Dependencies { - for foundName, foundSweeper := range filterSweeperWithDependencies(dependency, source) { - result[foundName] = foundSweeper - } - } - - return result -} - -// runSweeperWithRegion receives a sweeper and a region, and recursively calls -// itself with that region for every dependency found for that sweeper. If there -// are no dependencies, invoke the contained sweeper fun with the region, and -// add the success/fail status to the sweeperRunList. -func runSweeperWithRegion(region string, s *Sweeper, sweepers map[string]*Sweeper, sweeperRunList map[string]error, allowFailures bool) error { - for _, dep := range s.Dependencies { - depSweeper, ok := sweepers[dep] - - if !ok { - log.Printf("[ERROR] Sweeper (%s) has dependency (%s), but that sweeper was not found", s.Name, dep) - return fmt.Errorf("sweeper (%s) has dependency (%s), but that sweeper was not found", s.Name, dep) - } - - log.Printf("[DEBUG] Sweeper (%s) has dependency (%s), running..", s.Name, dep) - err := runSweeperWithRegion(region, depSweeper, sweepers, sweeperRunList, allowFailures) - - if err != nil { - if allowFailures { - log.Printf("[ERROR] Error running Sweeper (%s) in region (%s): %s", depSweeper.Name, region, err) - continue - } - - return err - } - } - - if _, ok := sweeperRunList[s.Name]; ok { - log.Printf("[DEBUG] Sweeper (%s) already ran in region (%s)", s.Name, region) - return nil - } - - log.Printf("[DEBUG] Running Sweeper (%s) in region (%s)", s.Name, region) - - start := time.Now() - runE := s.F(region) - elapsed := time.Since(start) - - log.Printf("[DEBUG] Completed Sweeper (%s) in region (%s) in %s", s.Name, region, elapsed) - - sweeperRunList[s.Name] = runE - - if runE != nil { - log.Printf("[ERROR] Error running Sweeper (%s) in region (%s): %s", s.Name, region, runE) - } - - return runE -} - -// Deprecated: Use EnvTfAcc instead. -const TestEnvVar = EnvTfAcc - -// TestCheckFunc is the callback type used with acceptance tests to check -// the state of a resource. The state passed in is the latest state known, -// or in the case of being after a destroy, it is the last known state when -// it was created. -type TestCheckFunc func(*terraform.State) error - -// ImportStateCheckFunc is the check function for ImportState tests -type ImportStateCheckFunc func([]*terraform.InstanceState) error - -// ImportStateIdFunc is an ID generation function to help with complex ID -// generation for ImportState tests. -type ImportStateIdFunc func(*terraform.State) (string, error) - -// ErrorCheckFunc is a function providers can use to handle errors. -type ErrorCheckFunc func(error) error - -// TestCase is a single acceptance test case used to test the apply/destroy -// lifecycle of a resource in a specific configuration. -// -// When the destroy plan is executed, the config from the last TestStep -// is used to plan it. -// -// Refer to the Env prefixed constants for environment variables that further -// control testing functionality. -type TestCase struct { - // IsUnitTest allows a test to run regardless of the TF_ACC - // environment variable. This should be used with care - only for - // fast tests on local resources (e.g. remote state with a local - // backend) but can be used to increase confidence in correct - // operation of Terraform without waiting for a full acctest run. - IsUnitTest bool - - // PreCheck, if non-nil, will be called before any test steps are - // executed. It will only be executed in the case that the steps - // would run, so it can be used for some validation before running - // acceptance tests, such as verifying that keys are setup. - PreCheck func() - - // TerraformVersionChecks is a list of checks to run against - // the Terraform CLI version which is running the testing. - // Each check is executed in order, respecting the first skip - // or fail response, unless the Any() meta check is also used. - TerraformVersionChecks []tfversion.TerraformVersionCheck - - // ProviderFactories can be specified for the providers that are valid. - // - // This can also be specified at the TestStep level to enable per-step - // differences in providers, however all provider specifications must - // be done either at the TestCase level or TestStep level, otherwise the - // testing framework will raise an error and fail the test. - // - // These are the providers that can be referenced within the test. Each key - // is an individually addressable provider. Typically you will only pass a - // single value here for the provider you are testing. Aliases are not - // supported by the test framework, so to use multiple provider instances, - // you should add additional copies to this map with unique names. To set - // their configuration, you would reference them similar to the following: - // - // provider "my_factory_key" { - // # ... - // } - // - // resource "my_resource" "mr" { - // provider = my_factory_key - // - // # ... - // } - ProviderFactories map[string]func() (*schema.Provider, error) - - // ProtoV5ProviderFactories serves the same purpose as ProviderFactories, - // but for protocol v5 providers defined using the terraform-plugin-go - // ProviderServer interface. - // - // This can also be specified at the TestStep level to enable per-step - // differences in providers, however all provider specifications must - // be done either at the TestCase level or TestStep level, otherwise the - // testing framework will raise an error and fail the test. - ProtoV5ProviderFactories map[string]func() (tfprotov5.ProviderServer, error) - - // ProtoV6ProviderFactories serves the same purpose as ProviderFactories, - // but for protocol v6 providers defined using the terraform-plugin-go - // ProviderServer interface. - // The version of Terraform used in acceptance testing must be greater - // than or equal to v0.15.4 to use ProtoV6ProviderFactories. - // - // This can also be specified at the TestStep level to enable per-step - // differences in providers, however all provider specifications must - // be done either at the TestCase level or TestStep level, otherwise the - // testing framework will raise an error and fail the test. - ProtoV6ProviderFactories map[string]func() (tfprotov6.ProviderServer, error) - - // Providers is the ResourceProvider that will be under test. - // - // Deprecated: Providers is deprecated, please use ProviderFactories - Providers map[string]*schema.Provider - - // ExternalProviders are providers the TestCase relies on that should - // be downloaded from the registry during init. - // - // This can also be specified at the TestStep level to enable per-step - // differences in providers, however all provider specifications must - // be done either at the TestCase level or TestStep level, otherwise the - // testing framework will raise an error and fail the test. - // - // This is generally unnecessary to set at the TestCase level, however - // it has existing in the testing framework prior to the introduction of - // TestStep level specification and was only necessary for performing - // import testing where the configuration contained a provider outside the - // one under test. - ExternalProviders map[string]ExternalProvider - - // PreventPostDestroyRefresh can be set to true for cases where data sources - // are tested alongside real resources - PreventPostDestroyRefresh bool - - // CheckDestroy is called after the resource is finally destroyed - // to allow the tester to test that the resource is truly gone. - CheckDestroy TestCheckFunc - - // ErrorCheck allows providers the option to handle errors such as skipping - // tests based on certain errors. - // - // This functionality is only intended for provider-controlled error - // messaging. While in certain scenarios this can also catch testing logic - // error messages, those messages are not protected by compatibility - // promises. - ErrorCheck ErrorCheckFunc - - // Steps are the apply sequences done within the context of the - // same state. Each step can have its own check to verify correctness. - Steps []TestStep - - // IDRefreshName is the name of the resource to check during ID-only - // refresh testing, which ensures that a resource can be refreshed solely - // by its identifier. This will default to the first non-nil primary - // resource in the state. It runs every TestStep. - // - // While not deprecated, most resource tests should instead prefer using - // TestStep.ImportState based testing as it works with multiple attribute - // identifiers and also verifies resource import functionality. - IDRefreshName string - - // IDRefreshIgnore is a list of configuration keys that will be ignored - // during ID-only refresh testing. - IDRefreshIgnore []string - - // WorkingDir sets the base directory where testing files used by the testing - // module are generated. If WorkingDir is unset, a randomized, temporary - // directory is used. - // - // Use the TF_ACC_PERSIST_WORKING_DIR environment variable, conventionally - // set to "1", to persist any working directory files. Otherwise, this directory is - // automatically cleaned up at the end of the TestCase. - WorkingDir string - - // AdditionalCLIOptions allows an intentionally limited set of options to be passed - // to the Terraform CLI when executing test steps. - AdditionalCLIOptions *AdditionalCLIOptions -} - -// ExternalProvider holds information about third-party providers that should -// be downloaded by Terraform as part of running the test step. -type ExternalProvider struct { - VersionConstraint string // the version constraint for the provider - Source string // the provider source -} - -type ImportStateKind byte - -const ( - // ImportCommandWithID tests import by using the ID string with the `terraform import` command - ImportCommandWithID ImportStateKind = iota - - // ImportBlockWithID tests import by using the ID string in an import configuration block with the `terraform plan` command - ImportBlockWithID - - // ImportBlockWithResourceIdentity imports the state using an import block with a resource identity - ImportBlockWithResourceIdentity -) - -// plannable reports whether this kind indicates the use of plannable import blocks -func (kind ImportStateKind) plannable() bool { - return kind == ImportBlockWithID || kind == ImportBlockWithResourceIdentity -} - -// resourceIdentity reports whether this kind indicates the use of resource identity in import blocks -func (kind ImportStateKind) resourceIdentity() bool { - return kind == ImportBlockWithResourceIdentity -} - -func (kind ImportStateKind) String() string { - return map[ImportStateKind]string{ - ImportCommandWithID: "ImportCommandWithID", - ImportBlockWithID: "ImportBlockWithID", - ImportBlockWithResourceIdentity: "ImportBlockWithResourceIdentity", - }[kind] -} - -// TestStep is a single apply sequence of a test, done within the -// context of a state. -// -// Multiple TestSteps can be sequenced in a Test to allow testing -// potentially complex update logic. In general, simply create/destroy -// tests will only need one step. -// -// Refer to the Env prefixed constants for environment variables that further -// control testing functionality. -type TestStep struct { - // ResourceName should be set to the name of the resource - // that is being tested. Example: "aws_instance.foo". Various test - // modes use this to auto-detect state information. - // - // This is only required if the test mode settings below say it is - // for the mode you're using. - ResourceName string - - // PreConfig is called before the Config is applied to perform any per-step - // setup that needs to happen. This is called regardless of "test mode" - // below. - PreConfig func() - - // Taint is a list of resource addresses to taint prior to the execution of - // the step. Be sure to only include this at a step where the referenced - // address will be present in state, as it will fail the test if the resource - // is missing. - // - // This option is ignored on ImportState tests, and currently only works for - // resources in the root module path. - Taint []string - - //--------------------------------------------------------------- - // Test modes. One of the following groups of settings must be - // set to determine what the test step will do. Ideally we would've - // used Go interfaces here but there are now hundreds of tests we don't - // want to re-type so instead we just determine which step logic - // to run based on what settings below are set. - //--------------------------------------------------------------- - - //--------------------------------------------------------------- - // Plan, Apply testing - //--------------------------------------------------------------- - - // Config a string of the configuration to give to Terraform. If this - // is set, then the TestCase will execute this step with the same logic - // as a `terraform apply`. If both Config and ConfigDirectory are set - // an error will be returned. - // - // JSON Configuration Syntax can be used and is assumed whenever Config - // contains valid JSON. - // - // Only one of Config, ConfigDirectory or ConfigFile can be set - // otherwise an error will be returned. - Config string - - // ConfigDirectory is a function which returns a function that - // accepts config.TestStepProviderConfig and returns a string - // representing a directory that contains Terraform - // configuration files. - // - // There are helper functions in the [config] package that can be used, - // such as: - // - // - [config.StaticDirectory] - // - [config.TestNameDirectory] - // - [config.TestStepDirectory] - // - // When running Terraform operations for the test, Terraform will - // be executed with copies of the files of this directory as its - // working directory. Only one of Config, ConfigDirectory or - // ConfigFile can be set otherwise an error will be returned. - ConfigDirectory config.TestStepConfigFunc - - // ConfigFile is a function which returns a function that - // accepts config.TestStepProviderConfig and returns a string - // representing a file that contains Terraform configuration. - // - // There are helper functions in the [config] package that can be used, - // such as: - // - // - [config.StaticFile] - // - [config.TestNameFile] - // - [config.TestStepFile] - // - // When running Terraform operations for the test, Terraform will - // be executed with a copy of the file as its working directory. - // Only one of Config, ConfigDirectory or ConfigFile can be set - // otherwise an error will be returned. - ConfigFile config.TestStepConfigFunc - - // ImportStateConfigExact indicates that the test framework should use the exact - // content of the Config, ConfigFile, or ConfigDirectory inputs and should - // not modify it at test run time. - // - // The default is false. At test run time, the test framework will generate - // specific kinds of configuration, such as import blocks, and append them - // to the given Config, ConfigFile, or ConfigDirectory inputs. Using this - // default improves test readability and removes duplication of setup. - ImportStateConfigExact bool - - // ConfigVariables is a map defining variables for use in conjunction - // with Terraform configuration. If this map is populated then it - // will be used to assemble an *.auto.tfvars.json which will be - // written into the working directory. Any variables that are - // defined within the Terraform configuration that have a matching - // variable definition in *.auto.tfvars.json will have their value - // substituted when the acceptance test is executed. - ConfigVariables config.Variables - - // Check is called after the Config is applied. Use this step to - // make your own API calls to check the status of things, and to - // inspect the format of the ResourceState itself. - // - // If an error is returned, the test will fail. In this case, a - // destroy plan will still be attempted. - // - // If this is nil, no check is done on this step. - Check TestCheckFunc - - // Destroy will create a destroy plan if set to true. - Destroy bool - - // ExpectNonEmptyPlan can be set to true for specific types of tests that are - // looking to verify that a diff occurs - ExpectNonEmptyPlan bool - - // ExpectError allows the construction of test cases that we expect to fail - // with an error. The specified regexp must match against the error for the - // test to pass. - // - // This functionality is only intended for provider-controlled error - // messaging. While in certain scenarios this can also catch testing logic - // error messages, those messages are not protected by compatibility - // promises. - ExpectError *regexp.Regexp - - // ConfigPlanChecks allows assertions to be made against the plan file at different points of a Config (apply) test using a plan check. - // Custom plan checks can be created by implementing the [PlanCheck] interface, or by using a PlanCheck implementation from the provided [plancheck] package - // - // [PlanCheck]: https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck#PlanCheck - // [plancheck]: https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck - ConfigPlanChecks ConfigPlanChecks - - // RefreshPlanChecks allows assertions to be made against the plan file at different points of a Refresh test using a plan check. - // Custom plan checks can be created by implementing the [PlanCheck] interface, or by using a PlanCheck implementation from the provided [plancheck] package - // - // [PlanCheck]: https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck#PlanCheck - // [plancheck]: https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck - RefreshPlanChecks RefreshPlanChecks - - // ConfigStateChecks allow assertions to be made against the state file during a Config (apply) test using a state check. - // Custom state checks can be created by implementing the [statecheck.StateCheck] interface, or by using a StateCheck implementation from the provided [statecheck] package. - ConfigStateChecks []statecheck.StateCheck - - // QueryResultChecks allow assertions to be made against a collection of found resources that were returned by a query using a query check. - // Custom query checks can be created by implementing the [querycheck.QueryResultCheck] interface, or by using a QueryResultCheck implementation from the provided [querycheck] package. - QueryResultChecks []QueryResultCheck - - // PlanOnly can be set to only run `plan` with this configuration, and not - // actually apply it. This is useful for ensuring config changes result in - // no-op plans - PlanOnly bool - - // PreventDiskCleanup can be set to true for testing terraform modules which - // require access to disk at runtime. Note that this will leave files in the - // temp folder - PreventDiskCleanup bool - - // PreventPostDestroyRefresh can be set to true for cases where data sources - // are tested alongside real resources - PreventPostDestroyRefresh bool - - // SkipFunc enables skipping the TestStep, based on environment criteria. - // For example, this can prevent running certain steps that may be runtime - // platform or API configuration dependent. - // - // Return true with no error to skip the test step. The error return - // should be used to signify issues that prevented the function from - // completing as expected. - // - // SkipFunc is called after PreConfig but before applying the Config. - SkipFunc func() (bool, error) - - //--------------------------------------------------------------- - // ImportState testing - //--------------------------------------------------------------- - - // ImportState, if true, will test the functionality of ImportState - // by importing the resource with ResourceName (must be set) and the - // ID of that resource. - ImportState bool - - // ImportStateKind controls the method of import that is used in combination with the other import-related fields on the TestStep struct. - // - // - By default, ImportCommandWithID is used, which tests import by using the ID string with the `terraform import` command. This was the original behavior prior to introducing the ImportStateKind field. - // - ImportBlockWithID tests import by using the ID string in an import configuration block with the `terraform plan` command. - // - ImportBlockWithResourceIdentity imports the state using an import configuration block with a resource identity. - ImportStateKind ImportStateKind - - // 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. - ImportStateId string - - // ImportStateIdPrefix is the prefix added in front of ImportStateId. - // This can be useful in complex import cases, where more than one - // attribute needs to be passed on as the Import ID. Mainly in cases - // where the ID is not known, and a known prefix needs to be added to - // the unset ImportStateId field. - ImportStateIdPrefix string - - // ImportStateIdFunc is a function that can be used to dynamically generate - // the ID for the ImportState tests. It is sent the state, which can be - // checked to derive the attributes necessary and generate the string in the - // desired format. - ImportStateIdFunc ImportStateIdFunc - - // ImportStateCheck checks the results of ImportState. It should be - // used to verify that the resulting value of ImportState has the - // proper resources, IDs, and attributes. - // - // Prefer ImportStateVerify over ImportStateCheck, unless the resource - // import explicitly is expected to create multiple resources (not a - // recommended resource implementation) or if attributes are imported with - // syntactically different but semantically/functionally equivalent values - // where special logic is needed. - // - // Terraform versions 1.3 and later can include data source states during - // import, which the testing framework will skip to prevent the need for - // Terraform version specific logic in provider testing. - ImportStateCheck ImportStateCheckFunc - - // ImportPlanChecks allows assertions to be made against the plan file at different points of a plannable import test using a plan check. - // Custom plan checks can be created by implementing the [PlanCheck] interface, or by using a PlanCheck implementation from the provided [plancheck] package - // - // [PlanCheck]: https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck#PlanCheck - // [plancheck]: https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck - ImportPlanChecks ImportPlanChecks - - // ImportStateVerify, if true, will also check that the state values - // that are finally put into the state after import match for all the - // IDs returned by the Import. Note that this checks for strict equality - // and does not respect DiffSuppressFunc or CustomizeDiff. - // - // By default, the prior resource state and import resource state are - // matched by the "id" attribute. If the "id" attribute is not implemented - // or another attribute more uniquely identifies the resource, set the - // ImportStateVerifyIdentifierAttribute field to adjust the attribute for - // matching. - // - // If certain attributes cannot be correctly imported, set the - // ImportStateVerifyIgnore field. - ImportStateVerify bool - - // ImportStateVerifyIdentifierAttribute is the resource attribute for - // matching the prior resource state and import resource state during import - // verification. By default, the "id" attribute is used. - ImportStateVerifyIdentifierAttribute string - - // ImportStateVerifyIgnore is a list of prefixes of fields that should - // not be verified to be equal. These can be set to ephemeral fields or - // fields that can't be refreshed and don't matter. - ImportStateVerifyIgnore []string - - // ImportStatePersist, if true, will update the persisted state with the - // state generated by the import operation (i.e., terraform import). When - // false (default) the state generated by the import operation is discarded - // at the end of the test step that is verifying import behavior. - ImportStatePersist bool - - //--------------------------------------------------------------- - // RefreshState testing - //--------------------------------------------------------------- - - // RefreshState, if true, will test the functionality of `terraform - // refresh` by refreshing the state, running any checks against the - // refreshed state, and running a plan to verify against unexpected plan - // differences. - // - // If the refresh is expected to result in a non-empty plan - // ExpectNonEmptyPlan should be set to true in the same TestStep. - // - // RefreshState cannot be the first TestStep and, it is mutually exclusive - // with ImportState. - RefreshState bool - - // ProviderFactories can be specified for the providers that are valid for - // this TestStep. When providers are specified at the TestStep level, all - // TestStep within a TestCase must declare providers. - // - // This can also be specified at the TestCase level for all TestStep, - // however all provider specifications must be done either at the TestCase - // level or TestStep level, otherwise the testing framework will raise an - // error and fail the test. - // - // These are the providers that can be referenced within the test. Each key - // is an individually addressable provider. Typically you will only pass a - // single value here for the provider you are testing. Aliases are not - // supported by the test framework, so to use multiple provider instances, - // you should add additional copies to this map with unique names. To set - // their configuration, you would reference them similar to the following: - // - // provider "my_factory_key" { - // # ... - // } - // - // resource "my_resource" "mr" { - // provider = my_factory_key - // - // # ... - // } - ProviderFactories map[string]func() (*schema.Provider, error) - - // ProtoV5ProviderFactories serves the same purpose as ProviderFactories, - // but for protocol v5 providers defined using the terraform-plugin-go - // ProviderServer interface. When providers are specified at the TestStep - // level, all TestStep within a TestCase must declare providers. - // - // This can also be specified at the TestCase level for all TestStep, - // however all provider specifications must be done either at the TestCase - // level or TestStep level, otherwise the testing framework will raise an - // error and fail the test. - ProtoV5ProviderFactories map[string]func() (tfprotov5.ProviderServer, error) - - // ProtoV6ProviderFactories serves the same purpose as ProviderFactories, - // but for protocol v6 providers defined using the terraform-plugin-go - // ProviderServer interface. - // The version of Terraform used in acceptance testing must be greater - // than or equal to v0.15.4 to use ProtoV6ProviderFactories. When providers - // are specified at the TestStep level, all TestStep within a TestCase must - // declare providers. - // - // This can also be specified at the TestCase level for all TestStep, - // however all provider specifications must be done either at the TestCase - // level or TestStep level, otherwise the testing framework will raise an - // error and fail the test. - ProtoV6ProviderFactories map[string]func() (tfprotov6.ProviderServer, error) - - // ExternalProviders are providers the TestStep relies on that should - // be downloaded from the registry during init. When providers are - // specified at the TestStep level, all TestStep within a TestCase must - // declare providers. - // - // This can also be specified at the TestCase level for all TestStep, - // however all provider specifications must be done either at the TestCase - // level or TestStep level, otherwise the testing framework will raise an - // error and fail the test. - // - // Outside specifying an earlier version of the provider under test, - // typically for state upgrader testing, this is generally only necessary - // for performing import testing where the prior TestStep configuration - // contained a provider outside the one under test. - ExternalProviders map[string]ExternalProvider - - // If true, the test step will run the query command - Query bool -} - -// ConfigPlanChecks defines the different points in a Config TestStep when plan checks can be run. -type ConfigPlanChecks struct { - // PreApply runs all plan checks in the slice. This occurs before the apply of a Config test is run. This slice cannot be populated - // with TestStep.PlanOnly, as there is no PreApply plan run with that flag set. All errors by plan checks in this slice are aggregated, reported, and will result in a test failure. - PreApply []plancheck.PlanCheck - - // PostApplyPreRefresh runs all plan checks in the slice. This occurs after the apply and before the refresh of a Config test is run. - // All errors by plan checks in this slice are aggregated, reported, and will result in a test failure. - PostApplyPreRefresh []plancheck.PlanCheck - - // PostApplyPostRefresh runs all plan checks in the slice. This occurs after the apply and refresh of a Config test are run. - // All errors by plan checks in this slice are aggregated, reported, and will result in a test failure. - PostApplyPostRefresh []plancheck.PlanCheck -} - -// ImportPlanChecks defines the different points in an Import TestStep when plan checks can be run. -type ImportPlanChecks struct { - // PreApply runs all plan checks in the slice. This occurs after the plan of an Import test is computed. This slice cannot be populated - // with TestStep.PlanOnly, as there is no PreApply plan run with that flag set. All errors by plan checks in this slice are aggregated, reported, and will result in a test failure. - PreApply []plancheck.PlanCheck -} - -// RefreshPlanChecks defines the different points in a Refresh TestStep when plan checks can be run. -type RefreshPlanChecks struct { - // PostRefresh runs all plan checks in the slice. This occurs after the refresh of the Refresh test is run. - // All errors by plan checks in this slice are aggregated, reported, and will result in a test failure. - PostRefresh []plancheck.PlanCheck -} - -// ParallelTest performs an acceptance test on a resource, allowing concurrency -// with other ParallelTest. The number of concurrent tests is controlled by the -// "go test" command -parallel flag. -// -// Tests will fail if they do not properly handle conditions to allow multiple -// tests to occur against the same resource or service (e.g. random naming). -// -// Test() function requirements and documentation also apply to this function. -func ParallelTest(t testing.T, c TestCase) { - t.Helper() - t.Parallel() - Test(t, c) -} - -// Test performs an acceptance test on a resource. -// -// Tests are not run unless an environmental variable "TF_ACC" is -// set to some non-empty value. This is to avoid test cases surprising -// a user by creating real resources. -// -// Use the ParallelTest() function to automatically set (*testing.T).Parallel() -// to enable testing concurrency. Use the UnitTest() function to automatically -// set the TestCase type IsUnitTest field. -// -// This function will automatically find or install Terraform CLI into a -// temporary directory, based on the following behavior: -// -// - If the TF_ACC_TERRAFORM_PATH environment variable is set, that -// Terraform CLI binary is used if found and executable. If not found or -// executable, an error will be returned unless the -// TF_ACC_TERRAFORM_VERSION environment variable is also set. -// - If the TF_ACC_TERRAFORM_VERSION environment variable is set, install -// and use that Terraform CLI version. -// - If both the TF_ACC_TERRAFORM_PATH and TF_ACC_TERRAFORM_VERSION -// environment variables are unset, perform a lookup for the Terraform -// CLI binary based on the operating system PATH. If not found, the -// latest available Terraform CLI binary is installed. -// -// Refer to the Env prefixed constants for additional details about these -// environment variables, and others, that control testing functionality. -func Test(t testing.T, c TestCase) { - t.Helper() - - ctx := context.Background() - ctx = logging.InitTestContext(ctx, t) - - err := c.validate(ctx, t) - - if err != nil { - logging.HelperResourceError(ctx, - "Test validation error", - map[string]interface{}{logging.KeyError: err}, - ) - t.Fatalf("Test validation error: %s", err) - } - - // We only run acceptance tests if an env var is set because they're - // slow and generally require some outside configuration. You can opt out - // of this with OverrideEnvVar on individual TestCases. - if os.Getenv(EnvTfAcc) == "" && !c.IsUnitTest { - t.Skip(fmt.Sprintf( - "Acceptance tests skipped unless env '%s' set", - EnvTfAcc)) - return - } - - // Copy any explicitly passed providers to factories, this is for backwards compatibility. - if len(c.Providers) > 0 { - c.ProviderFactories = map[string]func() (*schema.Provider, error){} - - for name, p := range c.Providers { - prov := p - c.ProviderFactories[name] = func() (*schema.Provider, error) { //nolint:unparam // required signature - return prov, nil - } - } - } - - logging.HelperResourceDebug(ctx, "Starting TestCase") - - // Run the PreCheck if we have it. - // This is done after the auto-configure to allow providers - // to override the default auto-configure parameters. - if c.PreCheck != nil { - logging.HelperResourceDebug(ctx, "Calling TestCase PreCheck") - - c.PreCheck() - - logging.HelperResourceDebug(ctx, "Called TestCase PreCheck") - } - - sourceDir, err := os.Getwd() - if err != nil { - t.Fatalf("Error getting working dir: %s", err) - } - helper := plugintest.AutoInitProviderHelper(ctx, sourceDir) - defer func(helper *plugintest.Helper) { - err := helper.Close() - if err != nil { - logging.HelperResourceError(ctx, "Unable to clean up temporary test files", map[string]interface{}{logging.KeyError: err}) - } - }(helper) - - // Run the TerraformVersionChecks if we have it. - // This is done after creating the helper because a working directory is required - // to retrieve the Terraform version. - if c.TerraformVersionChecks != nil { - runTFVersionChecks(ctx, t, helper.TerraformVersion(), c.TerraformVersionChecks) - } - - runNewTest(ctx, t, c, helper) - - logging.HelperResourceDebug(ctx, "Finished TestCase") -} - -// UnitTest is a helper to force the acceptance testing harness to run in the -// normal unit test suite. This should only be used for resource that don't -// have any external dependencies. -// -// Test() function requirements and documentation also apply to this function. -func UnitTest(t testing.T, c TestCase) { - t.Helper() - - c.IsUnitTest = true - Test(t, c) -} - -func testResource(name string, state *terraform.State) (*terraform.ResourceState, error) { - for _, m := range state.Modules { - if len(m.Resources) > 0 { - if v, ok := m.Resources[name]; ok { - return v, nil - } - } - } - - return nil, fmt.Errorf( - "Resource specified by ResourceName couldn't be found: %s", name) -} - -// ComposeTestCheckFunc lets you compose multiple TestCheckFuncs into -// a single TestCheckFunc. -// -// As a user testing their provider, this lets you decompose your checks -// into smaller pieces more easily. -// -// ComposeTestCheckFunc returns immediately on the first TestCheckFunc error. -// To aggregrate all errors, use ComposeAggregateTestCheckFunc instead. -func ComposeTestCheckFunc(fs ...TestCheckFunc) TestCheckFunc { - return func(s *terraform.State) error { - for i, f := range fs { - if err := f(s); err != nil { - return fmt.Errorf("Check %d/%d error: %w", i+1, len(fs), err) - } - } - - return nil - } -} - -// ComposeAggregateTestCheckFunc lets you compose multiple TestCheckFuncs into -// a single TestCheckFunc. -// -// As a user testing their provider, this lets you decompose your checks -// into smaller pieces more easily. -// -// Unlike ComposeTestCheckFunc, ComposeAggergateTestCheckFunc runs _all_ of the -// TestCheckFuncs and aggregates failures. -func ComposeAggregateTestCheckFunc(fs ...TestCheckFunc) TestCheckFunc { - return func(s *terraform.State) error { - var result []error - - for i, f := range fs { - if err := f(s); err != nil { - result = append(result, fmt.Errorf("Check %d/%d error: %w", i+1, len(fs), err)) - } - } - - return errors.Join(result...) - } -} - -// TestCheckResourceAttrSet ensures any value exists in the state for the -// given name and key combination. The opposite of this TestCheckFunc is -// TestCheckNoResourceAttr. State value checking is only recommended for -// testing Computed attributes and attribute defaults. -// -// Use this as a last resort when a more specific TestCheckFunc cannot be -// implemented, such as: -// -// - TestCheckResourceAttr: Equality checking of non-TypeSet state value. -// - TestCheckResourceAttrPair: Equality checking of non-TypeSet state -// value, based on another state value. -// - TestCheckTypeSet*: Equality checking of TypeSet state values. -// - TestMatchResourceAttr: Regular expression checking of non-TypeSet -// state value. -// - TestMatchTypeSet*: Regular expression checking on TypeSet state values. -// -// For managed resources, the name parameter is combination of the resource -// type, a period (.), and the name label. The name for the below example -// configuration would be "myprovider_thing.example". -// -// resource "myprovider_thing" "example" { ... } -// -// For data sources, the name parameter is a combination of the keyword "data", -// a period (.), the data source type, a period (.), and the name label. The -// name for the below example configuration would be -// "data.myprovider_thing.example". -// -// data "myprovider_thing" "example" { ... } -// -// The key parameter is an attribute path in Terraform CLI 0.11 and earlier -// "flatmap" syntax. Keys start with the attribute name of a top-level -// attribute. Use the following special key syntax to inspect underlying -// values of a list or map attribute: -// -// - .{NUMBER}: List value at index, e.g. .0 to inspect the first element -// - .{KEY}: Map value at key, e.g. .example to inspect the example key -// value -// -// While it is possible to check nested attributes under list and map -// attributes using the special key syntax, checking a list, map, or set -// attribute directly is not supported. Use TestCheckResourceAttr with -// the special .# or .% key syntax for those situations instead. -// -// An experimental interface exists to potentially replace the -// TestCheckResourceAttrSet functionality in the future and feedback -// would be appreciated. This example performs the same check as -// TestCheckResourceAttrSet with that experimental interface, by -// using [ExpectKnownValue] with [knownvalue.NotNull]: -// -// package example_test -// -// import ( -// "testing" -// -// "github.com/hashicorp/terraform-plugin-testing/helper/resource" -// "github.com/hashicorp/terraform-plugin-testing/knownvalue" -// "github.com/hashicorp/terraform-plugin-testing/statecheck" -// "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" -// ) -// -// func TestExpectKnownValue_CheckState_AttributeFound(t *testing.T) { -// t.Parallel() -// -// resource.Test(t, resource.TestCase{ -// // Provider definition omitted. -// Steps: []resource.TestStep{ -// { -// // Example resource containing a computed attribute named "computed_attribute" -// Config: `resource "test_resource" "one" {}`, -// ConfigStateChecks: []statecheck.StateCheck{ -// statecheck.ExpectKnownValue( -// "test_resource.one", -// tfjsonpath.New("computed_attribute"), -// knownvalue.NotNull(), -// ), -// }, -// }, -// }, -// }) -// } -func TestCheckResourceAttrSet(name, key string) TestCheckFunc { - return checkIfIndexesIntoTypeSet(key, func(s *terraform.State) error { - is, err := primaryInstanceState(s, name) - if err != nil { - return err - } - - return testCheckResourceAttrSet(is, name, key) - }) -} - -// TestCheckModuleResourceAttrSet - as per TestCheckResourceAttrSet but with -// support for non-root modules -// -// Deprecated: This functionality is deprecated without replacement. The -// terraform-plugin-testing Go module is intended for provider testing, which -// should always be possible within the root module of a configuration. This -// functionality is a carryover of when this code was used within Terraform -// core to test both providers and modules. Modern testing implementations to -// verify interactions between modules should be tested in Terraform core or -// using tooling outside this Go module. -func TestCheckModuleResourceAttrSet(mp []string, name string, key string) TestCheckFunc { - mpt := addrs.Module(mp).UnkeyedInstanceShim() - return checkIfIndexesIntoTypeSet(key, func(s *terraform.State) error { - is, err := modulePathPrimaryInstanceState(s, mpt, name) - if err != nil { - return err - } - - return testCheckResourceAttrSet(is, name, key) - }) -} - -func testCheckResourceAttrSet(is *terraform.InstanceState, name string, key string) error { - val, ok := is.Attributes[key] - - if ok && val != "" { - return nil - } - - if _, ok := is.Attributes[key+".#"]; ok { - return fmt.Errorf( - "%s: list or set attribute '%s' must be checked by element count key (%s) or element value keys (e.g. %s). Set element value checks should use TestCheckTypeSet functions instead.", - name, - key, - key+".#", - key+".0", - ) - } - - if _, ok := is.Attributes[key+".%"]; ok { - return fmt.Errorf( - "%s: map attribute '%s' must be checked by element count key (%s) or element value keys (e.g. %s).", - name, - key, - key+".%", - key+".examplekey", - ) - } - - return fmt.Errorf("%s: Attribute '%s' expected to be set", name, key) -} - -// TestCheckResourceAttr ensures a specific value is stored in state for the -// given name and key combination. State value checking is only recommended for -// testing Computed attributes and attribute defaults. -// -// For managed resources, the name parameter is combination of the resource -// type, a period (.), and the name label. The name for the below example -// configuration would be "myprovider_thing.example". -// -// resource "myprovider_thing" "example" { ... } -// -// For data sources, the name parameter is a combination of the keyword "data", -// a period (.), the data source type, a period (.), and the name label. The -// name for the below example configuration would be -// "data.myprovider_thing.example". -// -// data "myprovider_thing" "example" { ... } -// -// The key parameter is an attribute path in Terraform CLI 0.11 and earlier -// "flatmap" syntax. Keys start with the attribute name of a top-level -// attribute. Use the following special key syntax to inspect list, map, and -// set attributes: -// -// - .{NUMBER}: List value at index, e.g. .0 to inspect the first element. -// Use the TestCheckTypeSet* and TestMatchTypeSet* functions instead -// for sets. -// - .{KEY}: Map value at key, e.g. .example to inspect the example key -// value. -// - .#: Number of elements in list or set. -// - .%: Number of elements in map. -// -// The value parameter is the stringified data to check at the given key. Use -// the following attribute type rules to set the value: -// -// - Boolean: "false" or "true". -// - Float/Integer: Stringified number, such as "1.2" or "123". -// - String: No conversion necessary. -// -// An experimental interface exists to potentially replace the -// TestCheckResourceAttr functionality in the future and feedback -// would be appreciated. This example performs the same check as -// TestCheckResourceAttr with that experimental interface, by -// using [statecheck.ExpectKnownValue]: -// -// package example_test -// -// import ( -// "testing" -// -// "github.com/hashicorp/terraform-plugin-testing/helper/resource" -// "github.com/hashicorp/terraform-plugin-testing/knownvalue" -// "github.com/hashicorp/terraform-plugin-testing/statecheck" -// "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" -// ) -// -// func TestExpectKnownValue_CheckState_Bool(t *testing.T) { -// t.Parallel() -// -// resource.Test(t, resource.TestCase{ -// // Provider definition omitted. -// Steps: []resource.TestStep{ -// { -// // Example resource containing a computed boolean attribute named "computed_attribute" -// Config: `resource "test_resource" "one" {}`, -// ConfigStateChecks: []statecheck.StateCheck{ -// statecheck.ExpectKnownValue( -// "test_resource.one", -// tfjsonpath.New("computed_attribute"), -// knownvalue.Bool(true), -// ), -// }, -// }, -// }, -// }) -// } -func TestCheckResourceAttr(name, key, value string) TestCheckFunc { - return checkIfIndexesIntoTypeSet(key, func(s *terraform.State) error { - is, err := primaryInstanceState(s, name) - if err != nil { - return err - } - - return testCheckResourceAttr(is, name, key, value) - }) -} - -// TestCheckModuleResourceAttr - as per TestCheckResourceAttr but with -// support for non-root modules -// -// Deprecated: This functionality is deprecated without replacement. The -// terraform-plugin-testing Go module is intended for provider testing, which -// should always be possible within the root module of a configuration. This -// functionality is a carryover of when this code was used within Terraform -// core to test both providers and modules. Modern testing implementations to -// verify interactions between modules should be tested in Terraform core or -// using tooling outside this Go module. -func TestCheckModuleResourceAttr(mp []string, name string, key string, value string) TestCheckFunc { - mpt := addrs.Module(mp).UnkeyedInstanceShim() - return checkIfIndexesIntoTypeSet(key, func(s *terraform.State) error { - is, err := modulePathPrimaryInstanceState(s, mpt, name) - if err != nil { - return err - } - - return testCheckResourceAttr(is, name, key, value) - }) -} - -func testCheckResourceAttr(is *terraform.InstanceState, name string, key string, value string) error { - v, ok := is.Attributes[key] - - if !ok { - // Empty containers may be elided from the state. - // If the intent here is to check for an empty container, allow the key to - // also be non-existent. - if value == "0" && (strings.HasSuffix(key, ".#") || strings.HasSuffix(key, ".%")) { - return nil - } - - if _, ok := is.Attributes[key+".#"]; ok { - return fmt.Errorf( - "%s: list or set attribute '%s' must be checked by element count key (%s) or element value keys (e.g. %s). Set element value checks should use TestCheckTypeSet functions instead.", - name, - key, - key+".#", - key+".0", - ) - } - - if _, ok := is.Attributes[key+".%"]; ok { - return fmt.Errorf( - "%s: map attribute '%s' must be checked by element count key (%s) or element value keys (e.g. %s).", - name, - key, - key+".%", - key+".examplekey", - ) - } - - return fmt.Errorf("%s: Attribute '%s' not found", name, key) - } - - if v != value { - return fmt.Errorf( - "%s: Attribute '%s' expected %#v, got %#v", - name, - key, - value, - v) - } - - return nil -} - -// CheckResourceAttrWithFunc is the callback type used to apply a custom checking logic -// when using TestCheckResourceAttrWith and a value is found for the given name and key. -// -// When this function returns an error, TestCheckResourceAttrWith will fail the check. -// -// An experimental interface exists to potentially replace the -// CheckResourceAttrWithFunc functionality in the future and feedback -// would be appreciated. This example performs the same check as -// TestCheckResourceAttrWith with that experimental interface, by -// using [statecheck.ExpectKnownValue] in combination with -// [knownvalue.StringRegexp]: -// -// package example_test -// -// import ( -// "testing" -// -// "github.com/hashicorp/terraform-plugin-testing/helper/resource" -// "github.com/hashicorp/terraform-plugin-testing/statecheck" -// "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" -// ) -// -// func TestExpectKnownValue_CheckState_String_Custom(t *testing.T) { -// t.Parallel() -// -// resource.Test(t, resource.TestCase{ -// // Provider definition omitted. -// Steps: []resource.TestStep{ -// { -// // Example resource containing a computed string attribute named "computed_attribute" -// Config: `resource "test_resource" "one" {}`, -// ConfigStateChecks: []statecheck.StateCheck{ -// statecheck.ExpectKnownValue( -// "test_resource.one", -// tfjsonpath.New("computed_attribute"), -// knownvalue.StringRegexp(regexp.MustCompile("str")), -// }, -// }, -// }, -// }) -// } -type CheckResourceAttrWithFunc func(value string) error - -// TestCheckResourceAttrWith ensures a value stored in state for the -// given name and key combination, is checked against a custom logic. -// State value checking is only recommended for testing Computed attributes -// and attribute defaults. -// -// For managed resources, the name parameter is combination of the resource -// type, a period (.), and the name label. The name for the below example -// configuration would be "myprovider_thing.example". -// -// resource "myprovider_thing" "example" { ... } -// -// For data sources, the name parameter is a combination of the keyword "data", -// a period (.), the data source type, a period (.), and the name label. The -// name for the below example configuration would be -// "data.myprovider_thing.example". -// -// data "myprovider_thing" "example" { ... } -// -// The key parameter is an attribute path in Terraform CLI 0.11 and earlier -// "flatmap" syntax. Keys start with the attribute name of a top-level -// attribute. Use the following special key syntax to inspect list, map, and -// set attributes: -// -// - .{NUMBER}: List value at index, e.g. .0 to inspect the first element. -// Use the TestCheckTypeSet* and TestMatchTypeSet* functions instead -// for sets. -// - .{KEY}: Map value at key, e.g. .example to inspect the example key -// value. -// - .#: Number of elements in list or set. -// - .%: Number of elements in map. -// -// The checkValueFunc parameter is a CheckResourceAttrWithFunc, -// and it's provided with the attribute value to apply a custom checking logic, -// if it was found in the state. The function must return an error for the -// check to fail, or `nil` to succeed. -// -// An experimental interface exists to potentially replace the -// TestCheckResourceAttrWith functionality in the future and feedback -// would be appreciated. This example performs the same check as -// TestCheckResourceAttrWith with that experimental interface, by -// using [statecheck.ExpectKnownValue] in combination with -// [knownvalue.StringRegexp]: -// -// package example_test -// -// import ( -// "testing" -// -// "github.com/hashicorp/terraform-plugin-testing/helper/resource" -// "github.com/hashicorp/terraform-plugin-testing/statecheck" -// "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" -// ) -// -// func TestExpectKnownValue_CheckState_String_Custom(t *testing.T) { -// t.Parallel() -// -// resource.Test(t, resource.TestCase{ -// // Provider definition omitted. -// Steps: []resource.TestStep{ -// { -// // Example resource containing a computed string attribute named "computed_attribute" -// Config: `resource "test_resource" "one" {}`, -// ConfigStateChecks: []statecheck.StateCheck{ -// statecheck.ExpectKnownValue( -// "test_resource.one", -// tfjsonpath.New("computed_attribute"), -// knownvalue.StringRegexp(regexp.MustCompile("str")), -// }, -// }, -// }, -// }) -// } -func TestCheckResourceAttrWith(name, key string, checkValueFunc CheckResourceAttrWithFunc) TestCheckFunc { - return checkIfIndexesIntoTypeSet(key, func(s *terraform.State) error { - is, err := primaryInstanceState(s, name) - if err != nil { - return err - } - - err = testCheckResourceAttrSet(is, name, key) - if err != nil { - return err - } - - err = checkValueFunc(is.Attributes[key]) - if err != nil { - return fmt.Errorf("%s: Attribute %q value: %w", name, key, err) - } - - return nil - }) -} - -// TestCheckNoResourceAttr ensures no value exists in the state for the -// given name and key combination. The opposite of this TestCheckFunc is -// TestCheckResourceAttrSet. State value checking is only recommended for -// testing Computed attributes and attribute defaults. -// -// For managed resources, the name parameter is combination of the resource -// type, a period (.), and the name label. The name for the below example -// configuration would be "myprovider_thing.example". -// -// resource "myprovider_thing" "example" { ... } -// -// For data sources, the name parameter is a combination of the keyword "data", -// a period (.), the data source type, a period (.), and the name label. The -// name for the below example configuration would be -// "data.myprovider_thing.example". -// -// data "myprovider_thing" "example" { ... } -// -// The key parameter is an attribute path in Terraform CLI 0.11 and earlier -// "flatmap" syntax. Keys start with the attribute name of a top-level -// attribute. Use the following special key syntax to inspect underlying -// values of a list or map attribute: -// -// - .{NUMBER}: List value at index, e.g. .0 to inspect the first element. -// - .{KEY}: Map value at key, e.g. .example to inspect the example key -// value. -// -// While it is possible to check nested attributes under list and map -// attributes using the special key syntax, checking a list, map, or set -// attribute directly is not supported. Use TestCheckResourceAttr with -// the special .# or .% key syntax for those situations instead. -// -// An experimental interface exists to potentially replace the -// TestCheckNoResourceAttr functionality in the future and feedback -// would be appreciated. This example performs the same check as -// TestCheckNoResourceAttr with that experimental interface, by -// using [statecheck.ExpectKnownValue] with [knownvalue.Null]: -// -// package example_test -// -// import ( -// "testing" -// -// "github.com/hashicorp/terraform-plugin-testing/helper/resource" -// "github.com/hashicorp/terraform-plugin-testing/statecheck" -// "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" -// ) -// -// func TestExpectKnownValue_CheckState_AttributeNull(t *testing.T) { -// t.Parallel() -// -// resource.Test(t, resource.TestCase{ -// // Provider definition omitted. -// Steps: []resource.TestStep{ -// { -// // Example resource containing a computed attribute named "computed_attribute" that has a null value -// Config: `resource "test_resource" "one" {}`, -// ConfigStateChecks: []statecheck.StateCheck{ -// statecheck.ExpectKnownValue( -// "test_resource.one", -// tfjsonpath.New("computed_attribute"), -// knownvalue.Null(), -// ), -// }, -// }, -// }, -// }) -// } -func TestCheckNoResourceAttr(name, key string) TestCheckFunc { - return checkIfIndexesIntoTypeSet(key, func(s *terraform.State) error { - is, err := primaryInstanceState(s, name) - if err != nil { - return err - } - - return testCheckNoResourceAttr(is, name, key) - }) -} - -// TestCheckModuleNoResourceAttr - as per TestCheckNoResourceAttr but with -// support for non-root modules -// -// Deprecated: This functionality is deprecated without replacement. The -// terraform-plugin-testing Go module is intended for provider testing, which -// should always be possible within the root module of a configuration. This -// functionality is a carryover of when this code was used within Terraform -// core to test both providers and modules. Modern testing implementations to -// verify interactions between modules should be tested in Terraform core or -// using tooling outside this Go module. -func TestCheckModuleNoResourceAttr(mp []string, name string, key string) TestCheckFunc { - mpt := addrs.Module(mp).UnkeyedInstanceShim() - return checkIfIndexesIntoTypeSet(key, func(s *terraform.State) error { - is, err := modulePathPrimaryInstanceState(s, mpt, name) - if err != nil { - return err - } - - return testCheckNoResourceAttr(is, name, key) - }) -} - -func testCheckNoResourceAttr(is *terraform.InstanceState, name string, key string) error { - v, ok := is.Attributes[key] - - // Empty containers may sometimes be included in the state. - // If the intent here is to check for an empty container, allow the value to - // also be "0". - if v == "0" && (strings.HasSuffix(key, ".#") || strings.HasSuffix(key, ".%")) { - return nil - } - - if ok { - return fmt.Errorf("%s: Attribute '%s' found when not expected", name, key) - } - - if _, ok := is.Attributes[key+".#"]; ok { - return fmt.Errorf( - "%s: list or set attribute '%s' must be checked by element count key (%s) or element value keys (e.g. %s). Set element value checks should use TestCheckTypeSet functions instead.", - name, - key, - key+".#", - key+".0", - ) - } - - if _, ok := is.Attributes[key+".%"]; ok { - return fmt.Errorf( - "%s: map attribute '%s' must be checked by element count key (%s) or element value keys (e.g. %s).", - name, - key, - key+".%", - key+".examplekey", - ) - } - - return nil -} - -// TestMatchResourceAttr ensures a value matching a regular expression is -// stored in state for the given name and key combination. State value checking -// is only recommended for testing Computed attributes and attribute defaults. -// -// For managed resources, the name parameter is combination of the resource -// type, a period (.), and the name label. The name for the below example -// configuration would be "myprovider_thing.example". -// -// resource "myprovider_thing" "example" { ... } -// -// For data sources, the name parameter is a combination of the keyword "data", -// a period (.), the data source type, a period (.), and the name label. The -// name for the below example configuration would be -// "data.myprovider_thing.example". -// -// data "myprovider_thing" "example" { ... } -// -// The key parameter is an attribute path in Terraform CLI 0.11 and earlier -// "flatmap" syntax. Keys start with the attribute name of a top-level -// attribute. Use the following special key syntax to inspect list, map, and -// set attributes: -// -// - .{NUMBER}: List value at index, e.g. .0 to inspect the first element. -// Use the TestCheckTypeSet* and TestMatchTypeSet* functions instead -// for sets. -// - .{KEY}: Map value at key, e.g. .example to inspect the example key -// value. -// - .#: Number of elements in list or set. -// - .%: Number of elements in map. -// -// The value parameter is a compiled regular expression. A typical pattern is -// using the regexp.MustCompile() function, which will automatically ensure the -// regular expression is supported by the Go regular expression handlers during -// compilation. -// -// An experimental interface exists to potentially replace the -// TestMatchResourceAttr functionality in the future and feedback -// would be appreciated. This example performs the same check as -// TestMatchResourceAttr with that experimental interface, by -// using [statecheck.ExpectKnownValue] in combination with -// [knownvalue.StringRegexp]: -// -// package example_test -// -// import ( -// "testing" -// -// "github.com/hashicorp/terraform-plugin-testing/helper/resource" -// "github.com/hashicorp/terraform-plugin-testing/statecheck" -// "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" -// ) -// -// func TestExpectKnownValue_CheckState_String_Custom(t *testing.T) { -// t.Parallel() -// -// resource.Test(t, resource.TestCase{ -// // Provider definition omitted. -// Steps: []resource.TestStep{ -// { -// // Example resource containing a computed string attribute named "computed_attribute" -// Config: `resource "test_resource" "one" {}`, -// ConfigStateChecks: []statecheck.StateCheck{ -// statecheck.ExpectKnownValue( -// "test_resource.one", -// tfjsonpath.New("computed_attribute"), -// knownvalue.StringRegexp(regexp.MustCompile("str")), -// }, -// }, -// }, -// }) -// } -func TestMatchResourceAttr(name, key string, r *regexp.Regexp) TestCheckFunc { - return checkIfIndexesIntoTypeSet(key, func(s *terraform.State) error { - is, err := primaryInstanceState(s, name) - if err != nil { - return err - } - - return testMatchResourceAttr(is, name, key, r) - }) -} - -// TestModuleMatchResourceAttr - as per TestMatchResourceAttr but with -// support for non-root modules -// -// Deprecated: This functionality is deprecated without replacement. The -// terraform-plugin-testing Go module is intended for provider testing, which -// should always be possible within the root module of a configuration. This -// functionality is a carryover of when this code was used within Terraform -// core to test both providers and modules. Modern testing implementations to -// verify interactions between modules should be tested in Terraform core or -// using tooling outside this Go module. -func TestModuleMatchResourceAttr(mp []string, name string, key string, r *regexp.Regexp) TestCheckFunc { - mpt := addrs.Module(mp).UnkeyedInstanceShim() - return checkIfIndexesIntoTypeSet(key, func(s *terraform.State) error { - is, err := modulePathPrimaryInstanceState(s, mpt, name) - if err != nil { - return err - } - - return testMatchResourceAttr(is, name, key, r) - }) -} - -func testMatchResourceAttr(is *terraform.InstanceState, name string, key string, r *regexp.Regexp) error { - if !r.MatchString(is.Attributes[key]) { - return fmt.Errorf( - "%s: Attribute '%s' didn't match %q, got %#v", - name, - key, - r.String(), - is.Attributes[key]) - } - - return nil -} - -// TestCheckResourceAttrPtr is like TestCheckResourceAttr except the -// value is a pointer so that it can be updated while the test is running. -// It will only be dereferenced at the point this step is run. -// -// Refer to the TestCheckResourceAttr documentation for more information about -// setting the name, key, and value parameters. -func TestCheckResourceAttrPtr(name string, key string, value *string) TestCheckFunc { - return func(s *terraform.State) error { - return TestCheckResourceAttr(name, key, *value)(s) - } -} - -// TestCheckModuleResourceAttrPtr - as per TestCheckResourceAttrPtr but with -// support for non-root modules -// -// Deprecated: This functionality is deprecated without replacement. The -// terraform-plugin-testing Go module is intended for provider testing, which -// should always be possible within the root module of a configuration. This -// functionality is a carryover of when this code was used within Terraform -// core to test both providers and modules. Modern testing implementations to -// verify interactions between modules should be tested in Terraform core or -// using tooling outside this Go module. -func TestCheckModuleResourceAttrPtr(mp []string, name string, key string, value *string) TestCheckFunc { - return func(s *terraform.State) error { - return TestCheckModuleResourceAttr(mp, name, key, *value)(s) - } -} - -// TestCheckResourceAttrPair ensures value equality in state between the first -// given name and key combination and the second name and key combination. -// State value checking is only recommended for testing Computed attributes -// and attribute defaults. -// -// For managed resources, the name parameter is combination of the resource -// type, a period (.), and the name label. The name for the below example -// configuration would be "myprovider_thing.example". -// -// resource "myprovider_thing" "example" { ... } -// -// For data sources, the name parameter is a combination of the keyword "data", -// a period (.), the data source type, a period (.), and the name label. The -// name for the below example configuration would be -// "data.myprovider_thing.example". -// -// data "myprovider_thing" "example" { ... } -// -// The first and second names may use any combination of managed resources -// and/or data sources. -// -// The key parameter is an attribute path in Terraform CLI 0.11 and earlier -// "flatmap" syntax. Keys start with the attribute name of a top-level -// attribute. Use the following special key syntax to inspect list, map, and -// set attributes: -// -// - .{NUMBER}: List value at index, e.g. .0 to inspect the first element. -// Use the TestCheckTypeSet* and TestMatchTypeSet* functions instead -// for sets. -// - .{KEY}: Map value at key, e.g. .example to inspect the example key -// value. -// - .#: Number of elements in list or set. -// - .%: Number of elements in map. -func TestCheckResourceAttrPair(nameFirst, keyFirst, nameSecond, keySecond string) TestCheckFunc { - return checkIfIndexesIntoTypeSetPair(keyFirst, keySecond, func(s *terraform.State) error { - isFirst, err := primaryInstanceState(s, nameFirst) - if err != nil { - return err - } - - isSecond, err := primaryInstanceState(s, nameSecond) - if err != nil { - return err - } - - return testCheckResourceAttrPair(isFirst, nameFirst, keyFirst, isSecond, nameSecond, keySecond) - }) -} - -// TestCheckModuleResourceAttrPair - as per TestCheckResourceAttrPair but with -// support for non-root modules -// -// Deprecated: This functionality is deprecated without replacement. The -// terraform-plugin-testing Go module is intended for provider testing, which -// should always be possible within the root module of a configuration. This -// functionality is a carryover of when this code was used within Terraform -// core to test both providers and modules. Modern testing implementations to -// verify interactions between modules should be tested in Terraform core or -// using tooling outside this Go module. -func TestCheckModuleResourceAttrPair(mpFirst []string, nameFirst string, keyFirst string, mpSecond []string, nameSecond string, keySecond string) TestCheckFunc { - mptFirst := addrs.Module(mpFirst).UnkeyedInstanceShim() - mptSecond := addrs.Module(mpSecond).UnkeyedInstanceShim() - return checkIfIndexesIntoTypeSetPair(keyFirst, keySecond, func(s *terraform.State) error { - isFirst, err := modulePathPrimaryInstanceState(s, mptFirst, nameFirst) - if err != nil { - return err - } - - isSecond, err := modulePathPrimaryInstanceState(s, mptSecond, nameSecond) - if err != nil { - return err - } - - return testCheckResourceAttrPair(isFirst, nameFirst, keyFirst, isSecond, nameSecond, keySecond) - }) -} - -func testCheckResourceAttrPair(isFirst *terraform.InstanceState, nameFirst string, keyFirst string, isSecond *terraform.InstanceState, nameSecond string, keySecond string) error { - if nameFirst == nameSecond && keyFirst == keySecond { - return fmt.Errorf( - "comparing self: resource %s attribute %s", - nameFirst, - keyFirst, - ) - } - - vFirst, okFirst := isFirst.Attributes[keyFirst] - vSecond, okSecond := isSecond.Attributes[keySecond] - - // Container count values of 0 should not be relied upon, and not reliably - // maintained by helper/schema. For the purpose of tests, consider unset and - // 0 to be equal. - if len(keyFirst) > 2 && len(keySecond) > 2 && keyFirst[len(keyFirst)-2:] == keySecond[len(keySecond)-2:] && - (strings.HasSuffix(keyFirst, ".#") || strings.HasSuffix(keyFirst, ".%")) { - // they have the same suffix, and it is a collection count key. - if vFirst == "0" || vFirst == "" { - okFirst = false - } - if vSecond == "0" || vSecond == "" { - okSecond = false - } - } - - if okFirst != okSecond { - if !okFirst { - return fmt.Errorf("%s: Attribute %q not set, but %q is set in %s as %q", nameFirst, keyFirst, keySecond, nameSecond, vSecond) - } - return fmt.Errorf("%s: Attribute %q is %q, but %q is not set in %s", nameFirst, keyFirst, vFirst, keySecond, nameSecond) - } - if !(okFirst || okSecond) { - // If they both don't exist then they are equally unset, so that's okay. - return nil - } - - if vFirst != vSecond { - return fmt.Errorf( - "%s: Attribute '%s' expected %#v, got %#v", - nameFirst, - keyFirst, - vSecond, - vFirst) - } - - return nil -} - -// TestCheckOutput checks an output in the Terraform configuration -// -// An experimental interface exists to potentially replace the -// TestCheckOutput functionality in the future and feedback -// would be appreciated. This example performs the same check as -// TestCheckOutput with that experimental interface, by -// using [statecheck.ExpectKnownOutputValue]: -// -// package example_test -// -// import ( -// "testing" -// -// "github.com/hashicorp/terraform-plugin-testing/helper/resource" -// "github.com/hashicorp/terraform-plugin-testing/knownvalue" -// "github.com/hashicorp/terraform-plugin-testing/statecheck" -// "github.com/hashicorp/terraform-plugin-testing/tfversion" -// ) -// -// func TestExpectKnownOutputValue_CheckState_Bool(t *testing.T) { -// t.Parallel() -// -// resource.Test(t, resource.TestCase{ -// TerraformVersionChecks: []tfversion.TerraformVersionCheck{ -// tfversion.SkipBelow(tfversion.Version1_8_0), -// }, -// // Provider definition omitted. -// Steps: []resource.TestStep{ -// { -// // Example provider containing a provider-defined function named "bool" -// Config: `output "test" { -// value = provider::example::bool(true) -// }`, -// ConfigStateChecks: []statecheck.StateCheck{ -// statecheck.ExpectKnownOutputValue("test", knownvalue.Bool(true)), -// }, -// }, -// }, -// }) -// } -// -// An experimental interface exists to potentially replace the -// TestCheckOutput functionality in the future and feedback -// would be appreciated. This example performs the same check as -// TestCheckOutput with that experimental interface, by using -// [statecheck.ExpectKnownOutputValueAtPath]: -// -// package example_test -// -// import ( -// "testing" -// -// "github.com/hashicorp/terraform-plugin-testing/helper/resource" -// "github.com/hashicorp/terraform-plugin-testing/knownvalue" -// "github.com/hashicorp/terraform-plugin-testing/statecheck" -// "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" -// ) -// -// func TestExpectKnownOutputValueAtPath_CheckState_Bool(t *testing.T) { -// t.Parallel() -// -// resource.Test(t, resource.TestCase{ -// // Provider definition omitted. -// Steps: []resource.TestStep{ -// { -// // Example resource containing a computed boolean attribute named "computed_attribute" -// Config: `resource "test_resource" "one" {} -// -// // Generally, it is not necessary to use an output to test a resource attribute, -// // the resource attribute should be tested directly instead, by inspecting the -// // value of the resource attribute. For instance: -// // -// // ConfigStateChecks: []statecheck.StateCheck{ -// // statecheck.ExpectKnownValue( -// // "test_resource.one", -// // tfjsonpath.New("computed_attribute"), -// // knownvalue.Bool(true), -// // ), -// // }, -// // -// // This is only shown as an example. -// output test_resource_one_output { -// value = test_resource.one -// }`, -// ConfigStateChecks: []statecheck.StateCheck{ -// statecheck.ExpectKnownOutputValueAtPath( -// "test_resource_one_output", -// tfjsonpath.New("computed_attribute"), -// knownvalue.Bool(true), -// ), -// }, -// }, -// }, -// }) -// } -func TestCheckOutput(name, value string) TestCheckFunc { - return func(s *terraform.State) error { - ms := s.RootModule() - rs, ok := ms.Outputs[name] - if !ok { - return fmt.Errorf("Not found: %s", name) - } - - if rs.Value != value { - return fmt.Errorf( - "Output '%s': expected %#v, got %#v", - name, - value, - rs) - } - - return nil - } -} - -// TestMatchOutput ensures a value matching a regular expression is -// stored in state for the given name. State value checking is only -// recommended for testing Computed attributes and attribute defaults. -// -// An experimental interface exists to potentially replace the -// TestMatchOutput functionality in the future and feedback -// would be appreciated. This example performs the same check as -// TestMatchOutput with that experimental interface, by using -// [statecheck.ExpectKnownOutputValueAtPath] in combination with -// [knownvalue.StringRegexp]: -// -// package example_test -// -// import ( -// "testing" -// -// "github.com/hashicorp/terraform-plugin-testing/helper/resource" -// "github.com/hashicorp/terraform-plugin-testing/statecheck" -// "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" -// ) -// -// func TestExpectKnownOutputValueAtPath_CheckState_String_Custom(t *testing.T) { -// t.Parallel() -// -// resource.Test(t, resource.TestCase{ -// // Provider definition omitted. -// Steps: []resource.TestStep{ -// { -// // Example resource containing a computed string attribute named "computed_attribute" -// Config: `resource "test_resource" "one" {} -// -// // Generally, it is not necessary to use an output to test a resource attribute, -// // the resource attribute should be tested directly instead, by inspecting the -// // value of the resource attribute. For instance: -// // -// // ConfigStateChecks: []statecheck.StateCheck{ -// // statecheck.ExpectKnownValue( -// // "test_resource.one", -// // tfjsonpath.New("computed_attribute"), -// // knownvalue.StringRegexp(regexp.MustCompile("str")), -// // ), -// // }, -// // -// // This is only shown as an example. -// output test_resource_one_output { -// value = test_resource.one -// }`, -// ConfigStateChecks: []statecheck.StateCheck{ -// statecheck.ExpectKnownOutputValueAtPath( -// "test_resource_one_output", -// tfjsonpath.New("computed_attribute"), -// knownvalue.StringRegexp(regexp.MustCompile("str"), -// ), -// }, -// }, -// }, -// }) -// } -func TestMatchOutput(name string, r *regexp.Regexp) TestCheckFunc { - return func(s *terraform.State) error { - ms := s.RootModule() - rs, ok := ms.Outputs[name] - if !ok { - return fmt.Errorf("Not found: %s", name) - } - - valStr, ok := rs.Value.(string) - if !ok { - return fmt.Errorf("unexpected type %T for resource value", rs.Value) - } - - if !r.MatchString(valStr) { - return fmt.Errorf( - "Output '%s': %#v didn't match %q", - name, - rs, - r.String()) - } - - return nil - } -} - -// modulePrimaryInstanceState returns the instance state for the given resource -// name in a ModuleState -func modulePrimaryInstanceState(ms *terraform.ModuleState, name string) (*terraform.InstanceState, error) { - rs, ok := ms.Resources[name] - if !ok { - return nil, fmt.Errorf("Not found: %s in %s", name, ms.Path) - } - - is := rs.Primary - if is == nil { - return nil, fmt.Errorf("No primary instance: %s in %s", name, ms.Path) - } - - return is, nil -} - -// modulePathPrimaryInstanceState returns the primary instance state for the -// given resource name in a given module path. -func modulePathPrimaryInstanceState(s *terraform.State, mp addrs.ModuleInstance, name string) (*terraform.InstanceState, error) { - ms := s.ModuleByPath(mp) //nolint:staticcheck // legacy usage - if ms == nil { - return nil, fmt.Errorf("No module found at: %s", mp) - } - - return modulePrimaryInstanceState(ms, name) -} - -// primaryInstanceState returns the primary instance state for the given -// resource name in the root module. -func primaryInstanceState(s *terraform.State, name string) (*terraform.InstanceState, error) { - ms := s.RootModule() //nolint:staticcheck // legacy usage - return modulePrimaryInstanceState(ms, name) -} - -// indexesIntoTypeSet is a heuristic to try and identify if a flatmap style -// string address uses a precalculated TypeSet hash, which are integers and -// typically are large and obviously not a list index -func indexesIntoTypeSet(key string) bool { - for _, part := range strings.Split(key, ".") { - if i, err := strconv.Atoi(part); err == nil && i > 100 { - return true - } - } - return false -} - -func checkIfIndexesIntoTypeSet(key string, f TestCheckFunc) TestCheckFunc { - return func(s *terraform.State) error { - err := f(s) - if err != nil && indexesIntoTypeSet(key) { - return fmt.Errorf("Error in test check: %s\nTest check address %q likely indexes into TypeSet\nThis is currently not possible in the SDK", err, key) - } - return err - } -} - -func checkIfIndexesIntoTypeSetPair(keyFirst, keySecond string, f TestCheckFunc) TestCheckFunc { - return func(s *terraform.State) error { - err := f(s) - if err != nil && (indexesIntoTypeSet(keyFirst) || indexesIntoTypeSet(keySecond)) { - return fmt.Errorf("Error in test check: %s\nTest check address %q or %q likely indexes into TypeSet\nThis is currently not possible in the SDK", err, keyFirst, keySecond) - } - return err - } -} diff --git a/querycheck/testing_config.go b/querycheck/testing_config.go deleted file mode 100644 index e369f349..00000000 --- a/querycheck/testing_config.go +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-testing/internal/logging" - - "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" -) - -func testStepTaint(ctx context.Context, step TestStep, wd *plugintest.WorkingDir) error { - if len(step.Taint) == 0 { - return nil - } - - logging.HelperResourceTrace(ctx, fmt.Sprintf("Using TestStep Taint: %v", step.Taint)) - - for _, p := range step.Taint { - err := wd.Taint(ctx, p) - if err != nil { - return fmt.Errorf("error tainting resource: %s", err) - } - } - return nil -} diff --git a/querycheck/testing_new.go b/querycheck/testing_new.go deleted file mode 100644 index fb367a69..00000000 --- a/querycheck/testing_new.go +++ /dev/null @@ -1,701 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -import ( - "context" - "fmt" - "os" - "path/filepath" - "reflect" - "strconv" - "strings" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/go-version" - tfjson "github.com/hashicorp/terraform-json" - "github.com/mitchellh/go-testing-interface" - - "github.com/hashicorp/terraform-plugin-testing/config" - "github.com/hashicorp/terraform-plugin-testing/internal/logging" - "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" - "github.com/hashicorp/terraform-plugin-testing/internal/teststep" - "github.com/hashicorp/terraform-plugin-testing/terraform" -) - -func runPostTestDestroy(ctx context.Context, t testing.T, c TestCase, wd *plugintest.WorkingDir, providers *providerFactories, statePreDestroy *terraform.State) error { - t.Helper() - - err := runProviderCommand(ctx, t, wd, providers, func() error { - return wd.Destroy(ctx) - }) - if err != nil { - return err - } - - if c.CheckDestroy != nil { - logging.HelperResourceTrace(ctx, "Using TestCase CheckDestroy") - logging.HelperResourceDebug(ctx, "Calling TestCase CheckDestroy") - - if err := c.CheckDestroy(statePreDestroy); err != nil { - return err - } - - logging.HelperResourceDebug(ctx, "Called TestCase CheckDestroy") - } - - return nil -} - -func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest.Helper) { - t.Helper() - - wd := helper.RequireNewWorkingDir(ctx, t, c.WorkingDir) - - ctx = logging.TestTerraformPathContext(ctx, wd.GetHelper().TerraformExecPath()) - ctx = logging.TestWorkingDirectoryContext(ctx, wd.GetHelper().WorkingDirectory()) - - providers := &providerFactories{ - legacy: c.ProviderFactories, - protov5: c.ProtoV5ProviderFactories, - protov6: c.ProtoV6ProviderFactories, - } - - defer func() { - t.Helper() - - var statePreDestroy *terraform.State - var err error - err = runProviderCommand(ctx, t, wd, providers, func() error { - _, statePreDestroy, err = getState(ctx, t, wd) - if err != nil { - return err - } - return nil - }) - if err != nil { - logging.HelperResourceError(ctx, - "Error retrieving state, there may be dangling resources", - map[string]interface{}{logging.KeyError: err}, - ) - t.Fatalf("Error retrieving state, there may be dangling resources: %s", err.Error()) - return - } - - if !stateIsEmpty(statePreDestroy) { - err := runPostTestDestroy(ctx, t, c, wd, providers, statePreDestroy) - if err != nil { - logging.HelperResourceError(ctx, - "Error running post-test destroy, there may be dangling resources", - map[string]interface{}{logging.KeyError: err}, - ) - t.Fatalf("Error running post-test destroy, there may be dangling resources: %s", err.Error()) - } - } - - wd.Close() - }() - - // Return value from c.ProviderConfig() is assigned to Raw as this was previously being - // passed to wd.SetConfig() when the second argument accept a configuration string. - if c.hasProviders(ctx) { - config := teststep.Configuration( - teststep.ConfigurationRequest{ - Raw: teststep.Pointer(c.providerConfig(ctx, false)), - }, - ) - - err := wd.SetConfig(ctx, config, nil) - - if err != nil { - logging.HelperResourceError(ctx, - "TestCase error setting provider configuration", - map[string]interface{}{logging.KeyError: err}, - ) - t.Fatalf("TestCase error setting provider configuration: %s", err) - } - - err = runProviderCommand(ctx, t, wd, providers, func() error { - return wd.Init(ctx) - }) - - if err != nil { - logging.HelperResourceError(ctx, - "TestCase error running init", - map[string]interface{}{logging.KeyError: err}, - ) - t.Fatalf("TestCase error running init: %s", err.Error()) - } - } - - // use this to track last step successfully applied - // acts as default for import tests - var appliedCfg teststep.Config - var stepNumber int - - for stepIndex, step := range c.Steps { - if stepNumber > 0 { - copyWorkingDir(ctx, t, stepNumber, wd) - } - - stepNumber = stepIndex + 1 // 1-based indexing for humans - - configRequest := teststep.PrepareConfigurationRequest{ - Directory: step.ConfigDirectory, - File: step.ConfigFile, - Raw: step.Config, - TestStepConfigRequest: config.TestStepConfigRequest{ - StepNumber: stepNumber, - TestName: t.Name(), - }, - }.Exec() - - cfg := teststep.Configuration(configRequest) - - ctx = logging.TestStepNumberContext(ctx, stepNumber) - - logging.HelperResourceDebug(ctx, "Starting TestStep") - - if step.PreConfig != nil { - logging.HelperResourceDebug(ctx, "Calling TestStep PreConfig") - step.PreConfig() - logging.HelperResourceDebug(ctx, "Called TestStep PreConfig") - } - - if step.SkipFunc != nil { - logging.HelperResourceDebug(ctx, "Calling TestStep SkipFunc") - - skip, err := step.SkipFunc() - if err != nil { - logging.HelperResourceError(ctx, - "Error calling TestStep SkipFunc", - map[string]interface{}{logging.KeyError: err}, - ) - t.Fatalf("Error calling TestStep SkipFunc: %s", err.Error()) - } - - logging.HelperResourceDebug(ctx, "Called TestStep SkipFunc") - - if skip { - t.Logf("Skipping step %d/%d due to SkipFunc", stepNumber, len(c.Steps)) - logging.HelperResourceWarn(ctx, "Skipping TestStep due to SkipFunc") - continue - } - } - - if cfg != nil && !step.Destroy && len(step.Taint) > 0 { - err := testStepTaint(ctx, step, wd) - - if err != nil { - logging.HelperResourceError(ctx, - "TestStep error tainting resources", - map[string]interface{}{logging.KeyError: err}, - ) - t.Fatalf("TestStep %d/%d error tainting resources: %s", stepNumber, len(c.Steps), err) - } - } - - hasProviders, err := step.hasProviders(ctx, stepIndex, t.Name()) - - if err != nil { - logging.HelperResourceError(ctx, - "TestStep error checking for providers", - map[string]interface{}{logging.KeyError: err}, - ) - t.Fatalf("TestStep %d/%d error checking for providers: %s", stepNumber, len(c.Steps), err) - } - - if hasProviders { - providers = &providerFactories{ - legacy: sdkProviderFactories(c.ProviderFactories).merge(step.ProviderFactories), - protov5: protov5ProviderFactories(c.ProtoV5ProviderFactories).merge(step.ProtoV5ProviderFactories), - protov6: protov6ProviderFactories(c.ProtoV6ProviderFactories).merge(step.ProtoV6ProviderFactories), - } - - var hasProviderBlock bool - - if cfg != nil { - hasProviderBlock, err = cfg.HasProviderBlock(ctx) - - if err != nil { - logging.HelperResourceError(ctx, - "TestStep error determining whether configuration contains provider block", - map[string]interface{}{logging.KeyError: err}, - ) - t.Fatalf("TestStep %d/%d error determining whether configuration contains provider block: %s", stepNumber, len(c.Steps), err) - } - } - - var testStepConfig teststep.Config - - rawCfg, err := step.providerConfig(ctx, hasProviderBlock, helper.TerraformVersion()) - - if err != nil { - logging.HelperResourceError(ctx, - "TestStep error generating provider configuration", - map[string]interface{}{logging.KeyError: err}, - ) - t.Fatalf("TestStep %d/%d error generating provider configuration: %s", stepNumber, len(c.Steps), err) - } - - // Return value from step.providerConfig() is assigned to Raw as this was previously being - // passed to wd.SetConfig() directly when the second argument to wd.SetConfig() accepted a - // configuration string. - confRequest := teststep.PrepareConfigurationRequest{ - Directory: step.ConfigDirectory, - File: step.ConfigFile, - Raw: rawCfg, - TestStepConfigRequest: config.TestStepConfigRequest{ - StepNumber: stepNumber, - TestName: t.Name(), - }, - }.Exec() - - testStepConfig = teststep.Configuration(confRequest) - - if !step.Query { - err = wd.SetConfig(ctx, testStepConfig, step.ConfigVariables) - } - - if err != nil { - logging.HelperResourceError(ctx, - "TestStep error setting provider configuration", - map[string]interface{}{logging.KeyError: err}, - ) - t.Fatalf("TestStep %d/%d error setting test provider configuration: %s", stepNumber, len(c.Steps), err) - } - - err = runProviderCommand(ctx, t, wd, providers, func() error { - return wd.Init(ctx) - }) - - if err != nil { - logging.HelperResourceError(ctx, - "TestStep error running init", - map[string]interface{}{logging.KeyError: err}, - ) - t.Fatalf("TestStep %d/%d running init: %s", stepNumber, len(c.Steps), err.Error()) - return - } - } - - if step.ImportState { - logging.HelperResourceTrace(ctx, "TestStep is ImportState mode") - - err := testStepNewImportState(ctx, t, helper, wd, step, appliedCfg, providers, stepNumber) - if step.ExpectError != nil { - logging.HelperResourceDebug(ctx, "Checking TestStep ExpectError") - if err == nil { - logging.HelperResourceError(ctx, - "Error running import: expected an error but got none", - ) - t.Fatalf("Step %d/%d error running import: expected an error but got none", stepNumber, len(c.Steps)) - } - if !step.ExpectError.MatchString(err.Error()) { - logging.HelperResourceError(ctx, - fmt.Sprintf("Error running import: expected an error with pattern (%s)", step.ExpectError.String()), - map[string]interface{}{logging.KeyError: err}, - ) - t.Fatalf("Step %d/%d error running import, expected an error with pattern (%s), no match on: %s", stepNumber, len(c.Steps), step.ExpectError.String(), err) - } - } else { - if err != nil && c.ErrorCheck != nil { - logging.HelperResourceDebug(ctx, "Calling TestCase ErrorCheck") - err = c.ErrorCheck(err) - logging.HelperResourceDebug(ctx, "Called TestCase ErrorCheck") - } - if err != nil { - logging.HelperResourceError(ctx, - "Error running import", - map[string]interface{}{logging.KeyError: err}, - ) - t.Fatalf("Step %d/%d error running import: %s", stepNumber, len(c.Steps), err) - } - } - - logging.HelperResourceDebug(ctx, "Finished TestStep") - - continue - } - - if step.RefreshState { - logging.HelperResourceTrace(ctx, "TestStep is RefreshState mode") - - err := testStepNewRefreshState(ctx, t, wd, step, providers) - if step.ExpectError != nil { - logging.HelperResourceDebug(ctx, "Checking TestStep ExpectError") - if err == nil { - logging.HelperResourceError(ctx, - "Error running refresh: expected an error but got none", - ) - t.Fatalf("Step %d/%d error running refresh: expected an error but got none", stepNumber, len(c.Steps)) - } - if !step.ExpectError.MatchString(err.Error()) { - logging.HelperResourceError(ctx, - fmt.Sprintf("Error running refresh: expected an error with pattern (%s)", step.ExpectError.String()), - map[string]interface{}{logging.KeyError: err}, - ) - t.Fatalf("Step %d/%d error running refresh, expected an error with pattern (%s), no match on: %s", stepNumber, len(c.Steps), step.ExpectError.String(), err) - } - } else { - if err != nil && c.ErrorCheck != nil { - logging.HelperResourceDebug(ctx, "Calling TestCase ErrorCheck") - err = c.ErrorCheck(err) - logging.HelperResourceDebug(ctx, "Called TestCase ErrorCheck") - } - if err != nil { - logging.HelperResourceError(ctx, - "Error running refresh", - map[string]interface{}{logging.KeyError: err}, - ) - t.Fatalf("Step %d/%d error running refresh: %s", stepNumber, len(c.Steps), err) - } - } - - logging.HelperResourceDebug(ctx, "Finished TestStep") - - continue - } - - if step.Query { - logging.HelperResourceTrace(ctx, "TestStep is Query mode") - - queryConfigRequest := teststep.ConfigurationRequest{ - Raw: &step.Config, - } - err := wd.SetQuery(ctx, teststep.Configuration(queryConfigRequest), step.ConfigVariables) - if err != nil { - t.Fatalf("Step %d/%d error setting query: %s", stepNumber, len(c.Steps), err) - } - - err = runProviderCommand(ctx, t, wd, providers, func() error { - return wd.Init(ctx) - }) - if err != nil { - t.Fatalf("Step %d/%d error running init: %s", stepNumber, len(c.Steps), err) - } - - var queryOut []tfjson.LogMsg - err = runProviderCommand(ctx, t, wd, providers, func() error { - var err error - queryOut, err = wd.Query(ctx) - return err - }) - if err != nil { - fmt.Printf("Step %d/%d Query Output:\n%s\n", stepNumber, len(c.Steps), queryOut) - t.Fatalf("Step %d/%d error running query: %s", stepNumber, len(c.Steps), err) - } - - err = RunQueryChecks(ctx, t, queryOut, step.QueryResultChecks) - if err != nil { - t.Fatalf("Step %d/%d error running query checks: %s", stepNumber, len(c.Steps), err) - } - - fmt.Printf("Step %d/%d Query Output:\n%s\n", stepNumber, len(c.Steps), queryOut) - continue - } - - if cfg != nil { - logging.HelperResourceTrace(ctx, "TestStep is Config mode") - - err := testStepNewConfig(ctx, t, c, wd, step, providers, stepIndex, helper) - if step.ExpectError != nil { - logging.HelperResourceDebug(ctx, "Checking TestStep ExpectError") - - if err == nil { - logging.HelperResourceError(ctx, - "Expected an error but got none", - ) - t.Fatalf("Step %d/%d, expected an error but got none", stepNumber, len(c.Steps)) - } - if !step.ExpectError.MatchString(err.Error()) { - logging.HelperResourceError(ctx, - fmt.Sprintf("Expected an error with pattern (%s)", step.ExpectError.String()), - map[string]interface{}{logging.KeyError: err}, - ) - t.Fatalf("Step %d/%d, expected an error with pattern, no match on: %s", stepNumber, len(c.Steps), err) - } - } else { - if err != nil && c.ErrorCheck != nil { - logging.HelperResourceDebug(ctx, "Calling TestCase ErrorCheck") - - err = c.ErrorCheck(err) - - logging.HelperResourceDebug(ctx, "Called TestCase ErrorCheck") - } - if err != nil { - logging.HelperResourceError(ctx, - "Unexpected error", - map[string]interface{}{logging.KeyError: err}, - ) - t.Fatalf("Step %d/%d error: %s", stepNumber, len(c.Steps), err) - } - } - - var hasTerraformBlock bool - var hasProviderBlock bool - - if cfg != nil { - hasTerraformBlock, err = cfg.HasTerraformBlock(ctx) - - if err != nil { - logging.HelperResourceError(ctx, - "Error determining whether configuration contains terraform block", - map[string]interface{}{logging.KeyError: err}, - ) - t.Fatalf("Error determining whether configuration contains terraform block: %s", err) - } - - hasProviderBlock, err = cfg.HasProviderBlock(ctx) - - if err != nil { - logging.HelperResourceError(ctx, - "Error determining whether configuration contains provider block", - map[string]interface{}{logging.KeyError: err}, - ) - t.Fatalf("Error determining whether configuration contains provider block: %s", err) - } - } - - mergedConfig, err := step.mergedConfig(ctx, c, hasTerraformBlock, hasProviderBlock, helper.TerraformVersion()) - - if err != nil { - logging.HelperResourceError(ctx, - "Error generating merged configuration", - map[string]interface{}{logging.KeyError: err}, - ) - t.Fatalf("Error generating merged configuration: %s", err) - } - - // Preserve the step config for future test steps to use (import state) - confRequest := teststep.PrepareConfigurationRequest{ - Directory: step.ConfigDirectory, - File: step.ConfigFile, - Raw: mergedConfig, - TestStepConfigRequest: config.TestStepConfigRequest{ - StepNumber: stepNumber, - TestName: t.Name(), - }, - }.Exec() - - appliedCfg = teststep.Configuration(confRequest) - - logging.HelperResourceDebug(ctx, "Finished TestStep") - - continue - } - - t.Fatalf("Step %d/%d, unsupported test mode", stepNumber, len(c.Steps)) - } - - if stepNumber > 0 { - copyWorkingDir(ctx, t, stepNumber, wd) - } -} - -func getState(ctx context.Context, t testing.T, wd *plugintest.WorkingDir) (*tfjson.State, *terraform.State, error) { - t.Helper() - - jsonState, err := wd.State(ctx) - if err != nil { - return nil, nil, err - } - state, err := shimStateFromJson(jsonState) - if err != nil { - t.Fatal(err) - } - return jsonState, state, nil -} - -func stateIsEmpty(state *terraform.State) bool { - return state.Empty() || !state.HasResources() //nolint:staticcheck // legacy usage -} - -func planIsEmpty(plan *tfjson.Plan, tfVersion *version.Version) bool { - for _, rc := range plan.ResourceChanges { - for _, a := range rc.Change.Actions { - if a != tfjson.ActionNoop { - return false - } - } - } - - if tfVersion.LessThan(expectNonEmptyPlanOutputChangesMinTFVersion) { - return true - } - - for _, change := range plan.OutputChanges { - if !change.Actions.NoOp() { - return false - } - } - - return true -} - -func testIDRefresh(ctx context.Context, t testing.T, c TestCase, wd *plugintest.WorkingDir, step TestStep, r *terraform.ResourceState, providers *providerFactories, stepIndex int, helper *plugintest.Helper) error { - t.Helper() - - // Build the state. The state is just the resource with an ID. There - // are no attributes. We only set what is needed to perform a refresh. - state := terraform.NewState() //nolint:staticcheck // legacy usage - state.RootModule().Resources = make(map[string]*terraform.ResourceState) - state.RootModule().Resources[c.IDRefreshName] = &terraform.ResourceState{} - - configRequest := teststep.PrepareConfigurationRequest{ - Directory: step.ConfigDirectory, - File: step.ConfigFile, - Raw: step.Config, - TestStepConfigRequest: config.TestStepConfigRequest{ - StepNumber: stepIndex + 1, - TestName: t.Name(), - }, - }.Exec() - - cfg := teststep.Configuration(configRequest) - - var hasProviderBlock bool - - if cfg != nil { - var err error - - hasProviderBlock, err = cfg.HasProviderBlock(ctx) - - if err != nil { - logging.HelperResourceError(ctx, - "Error determining whether configuration contains provider block for import test config", - map[string]interface{}{logging.KeyError: err}, - ) - t.Fatalf("Error determining whether configuration contains provider block for import test config: %s", err) - } - } - - // Return value from c.ProviderConfig() is assigned to Raw as this was previously being - // passed to wd.SetConfig() when the second argument accept a configuration string. - testStepConfig := teststep.Configuration( - teststep.ConfigurationRequest{ - Raw: teststep.Pointer(c.providerConfig(ctx, hasProviderBlock)), - }, - ) - - // Temporarily set the config to a minimal provider config for the refresh - // test. After the refresh we can reset it. - err := wd.SetConfig(ctx, testStepConfig, step.ConfigVariables) - if err != nil { - t.Fatalf("Error setting import test config: %s", err) - } - - rawCfg, err := step.providerConfig(ctx, hasProviderBlock, helper.TerraformVersion()) - - if err != nil { - t.Fatalf("Error generating import provider config: %s", err) - } - - defer func() { - t.Helper() - - confRequest := teststep.PrepareConfigurationRequest{ - Directory: step.ConfigDirectory, - File: step.ConfigFile, - Raw: rawCfg, - TestStepConfigRequest: config.TestStepConfigRequest{ - StepNumber: stepIndex + 1, - TestName: t.Name(), - }, - }.Exec() - - testStepConfigDefer := teststep.Configuration(confRequest) - - err = wd.SetConfig(ctx, testStepConfigDefer, step.ConfigVariables) - - if err != nil { - t.Fatalf("Error resetting test config: %s", err) - } - }() - - // Refresh! - err = runProviderCommand(ctx, t, wd, providers, func() error { - err = wd.Refresh(ctx) - if err != nil { - t.Fatalf("Error running terraform refresh: %s", err) - } - _, state, err = getState(ctx, t, wd) - if err != nil { - return err - } - return nil - }) - if err != nil { - return err - } - - // Verify attribute equivalence. - actualR := state.RootModule().Resources[c.IDRefreshName] - if actualR == nil { - return fmt.Errorf("Resource gone!") - } - if actualR.Primary == nil { - return fmt.Errorf("Resource has no primary instance") - } - actual := actualR.Primary.Attributes - expected := r.Primary.Attributes - - if len(c.IDRefreshIgnore) > 0 { - logging.HelperResourceTrace(ctx, fmt.Sprintf("Using TestCase IDRefreshIgnore: %v", c.IDRefreshIgnore)) - } - - // Remove fields we're ignoring - for _, v := range c.IDRefreshIgnore { - for k := range actual { - if strings.HasPrefix(k, v) { - delete(actual, k) - } - } - for k := range expected { - if strings.HasPrefix(k, v) { - delete(expected, k) - } - } - } - - if !reflect.DeepEqual(actual, expected) { - // Determine only the different attributes - for k, v := range expected { - if av, ok := actual[k]; ok && v == av { - delete(expected, k) - delete(actual, k) - } - } - - if diff := cmp.Diff(expected, actual); diff != "" { - return fmt.Errorf("IDRefreshName attributes not equivalent. Difference is shown below. The - symbol indicates attributes missing after refresh.\n\n%s", diff) - } - } - - return nil -} - -func copyWorkingDir(ctx context.Context, t testing.T, stepNumber int, wd *plugintest.WorkingDir) { - if os.Getenv(plugintest.EnvTfAccPersistWorkingDir) == "" { - return - } - - workingDir := wd.GetHelper().WorkingDirectory() - - dest := filepath.Join(workingDir, fmt.Sprintf("%s%s", "step_", strconv.Itoa(stepNumber))) - - baseDir := wd.BaseDir() - rootBaseDir := strings.TrimPrefix(baseDir, workingDir) - - err := plugintest.CopyDir(workingDir, dest, rootBaseDir) - if err != nil { - logging.HelperResourceError(ctx, - "Unexpected error copying working directory files", - map[string]interface{}{logging.KeyError: err}, - ) - t.Fatalf("TestStep %d/%d error copying working directory files: %s", stepNumber, err) - } - - t.Logf("Working directory and files have been copied to: %s", dest) -} diff --git a/querycheck/testing_new_config.go b/querycheck/testing_new_config.go deleted file mode 100644 index 38114cd1..00000000 --- a/querycheck/testing_new_config.go +++ /dev/null @@ -1,469 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -import ( - "context" - "errors" - "fmt" - "os" - - "github.com/hashicorp/terraform-exec/tfexec" - tfjson "github.com/hashicorp/terraform-json" - "github.com/mitchellh/go-testing-interface" - - "github.com/hashicorp/terraform-plugin-testing/config" - "github.com/hashicorp/terraform-plugin-testing/internal/teststep" - "github.com/hashicorp/terraform-plugin-testing/terraform" - "github.com/hashicorp/terraform-plugin-testing/tfversion" - - "github.com/hashicorp/terraform-plugin-testing/internal/logging" - "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" -) - -// expectNonEmptyPlanOutputChangesMinTFVersion is used to keep compatibility for -// Terraform 0.12 and 0.13 after enabling ExpectNonEmptyPlan to check output -// changes. Those older versions will always show outputs being created. -var expectNonEmptyPlanOutputChangesMinTFVersion = tfversion.Version0_14_0 - -func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugintest.WorkingDir, step TestStep, providers *providerFactories, stepIndex int, helper *plugintest.Helper) error { - t.Helper() - - // When `refreshAfterApply` is true, a `Config`-mode test step will invoke - // a refresh before successful completion. This is a compatibility measure - // for test cases that have different -- but semantically-equal -- state - // representations in their test steps. When comparing two states, the - // testing framework is not aware of semantic equality or set equality. - _, refreshAfterApply := os.LookupEnv(EnvTfAccRefreshAfterApply) - - configRequest := teststep.PrepareConfigurationRequest{ - Directory: step.ConfigDirectory, - File: step.ConfigFile, - Raw: step.Config, - TestStepConfigRequest: config.TestStepConfigRequest{ - StepNumber: stepIndex + 1, - TestName: t.Name(), - }, - }.Exec() - - cfg := teststep.Configuration(configRequest) - - var hasTerraformBlock bool - var hasProviderBlock bool - - if cfg != nil { - var err error - - hasTerraformBlock, err = cfg.HasTerraformBlock(ctx) - - if err != nil { - logging.HelperResourceError(ctx, - "Error determining whether configuration contains terraform block", - map[string]interface{}{logging.KeyError: err}, - ) - t.Fatalf("Error determining whether configuration contains terraform block: %s", err) - } - - hasProviderBlock, err = cfg.HasProviderBlock(ctx) - - if err != nil { - logging.HelperResourceError(ctx, - "Error determining whether configuration contains provider block", - map[string]interface{}{logging.KeyError: err}, - ) - t.Fatalf("Error determining whether configuration contains provider block: %s", err) - } - } - - mergedConfig, err := step.mergedConfig(ctx, c, hasTerraformBlock, hasProviderBlock, helper.TerraformVersion()) - - if err != nil { - logging.HelperResourceError(ctx, - "Error generating merged configuration", - map[string]interface{}{logging.KeyError: err}, - ) - t.Fatalf("Error generating merged configuration: %s", err) - } - - confRequest := teststep.PrepareConfigurationRequest{ - Directory: step.ConfigDirectory, - File: step.ConfigFile, - Raw: mergedConfig, - TestStepConfigRequest: config.TestStepConfigRequest{ - StepNumber: stepIndex + 1, - TestName: t.Name(), - }, - }.Exec() - - testStepConfig := teststep.Configuration(confRequest) - - err = wd.SetConfig(ctx, testStepConfig, step.ConfigVariables) - if err != nil { - return fmt.Errorf("Error setting config: %w", err) - } - - // If this step is a PlanOnly step, skip over this first Plan and - // subsequent Apply, and use the follow-up Plan that checks for - // permadiffs - if !step.PlanOnly { - logging.HelperResourceDebug(ctx, "Running Terraform CLI plan and apply") - - // Plan! - err := runProviderCommand(ctx, t, wd, providers, func() error { - var opts []tfexec.PlanOption - if step.Destroy { - opts = append(opts, tfexec.Destroy(true)) - } - - if c.AdditionalCLIOptions != nil { - if c.AdditionalCLIOptions.Plan.AllowDeferral { - opts = append(opts, tfexec.AllowDeferral(true)) - } - if c.AdditionalCLIOptions.Plan.NoRefresh { - opts = append(opts, tfexec.Refresh(false)) - } - } - - return wd.CreatePlan(ctx, opts...) - }) - if err != nil { - return fmt.Errorf("Error running pre-apply plan: %w", err) - } - - // Run pre-apply plan checks - if len(step.ConfigPlanChecks.PreApply) > 0 { - var plan *tfjson.Plan - err = runProviderCommand(ctx, t, wd, providers, func() error { - var err error - plan, err = wd.SavedPlan(ctx) - return err - }) - if err != nil { - return fmt.Errorf("Error retrieving pre-apply plan: %w", err) - } - - err = runPlanChecks(ctx, t, plan, step.ConfigPlanChecks.PreApply) - if err != nil { - return fmt.Errorf("Pre-apply plan check(s) failed:\n%w", err) - } - } - - // We need to keep a copy of the state prior to destroying such - // that the destroy steps can verify their behavior in the - // check function - var stateBeforeApplication *terraform.State - - if step.Check != nil && step.Destroy { - // Refresh the state before shimming it for destroy checks later. - // This re-implements previously existing test step logic for the - // specific situation that a provider developer has applied a - // resource with a previous schema version and is destroying it with - // a provider that has a newer schema version. Without this refresh - // the shim logic will return an error such as: - // - // Failed to marshal state to json: schema version 0 for null_resource.test in state does not match version 1 from the provider - err := runProviderCommand(ctx, t, wd, providers, func() error { - return wd.Refresh(ctx) - }) - - if err != nil { - return fmt.Errorf("Error running pre-apply refresh: %w", err) - } - - err = runProviderCommand(ctx, t, wd, providers, func() error { - _, stateBeforeApplication, err = getState(ctx, t, wd) - if err != nil { - return err - } - return nil - }) - - if err != nil { - return fmt.Errorf("Error retrieving pre-apply state: %w", err) - } - } - - // Apply the diff, creating real resources - err = runProviderCommand(ctx, t, wd, providers, func() error { - var opts []tfexec.ApplyOption - - if c.AdditionalCLIOptions != nil && c.AdditionalCLIOptions.Apply.AllowDeferral { - opts = append(opts, tfexec.AllowDeferral(true)) - } - - return wd.Apply(ctx, opts...) - }) - if err != nil { - if step.Destroy { - return fmt.Errorf("Error running destroy: %w", err) - } - return fmt.Errorf("Error running apply: %w", err) - } - - // Run any configured checks - if step.Check != nil { - logging.HelperResourceTrace(ctx, "Using TestStep Check") - - if step.Destroy { - if err := step.Check(stateBeforeApplication); err != nil { - return fmt.Errorf("Check failed: %w", err) - } - } else { - var state *terraform.State - - err := runProviderCommand(ctx, t, wd, providers, func() error { - _, state, err = getState(ctx, t, wd) - if err != nil { - return err - } - return nil - }) - - if err != nil { - return fmt.Errorf("Error retrieving state after apply: %w", err) - } - - if err := step.Check(state); err != nil { - return fmt.Errorf("Check failed: %w", err) - } - } - } - - // Run state checks - if len(step.ConfigStateChecks) > 0 { - var state *tfjson.State - - err = runProviderCommand(ctx, t, wd, providers, func() error { - var err error - state, err = wd.State(ctx) - return err - }) - - if err != nil { - return fmt.Errorf("Error retrieving post-apply, post-refresh state: %w", err) - } - - err = runStateChecks(ctx, t, state, step.ConfigStateChecks) - if err != nil { - return fmt.Errorf("Post-apply refresh state check(s) failed:\n%w", err) - } - } - } - - // Test for perpetual diffs by performing a plan, a refresh, and another plan - logging.HelperResourceDebug(ctx, "Running Terraform CLI plan to check for perpetual differences") - - // do a plan - err = runProviderCommand(ctx, t, wd, providers, func() error { - opts := []tfexec.PlanOption{ - tfexec.Refresh(false), - } - if step.Destroy { - opts = append(opts, tfexec.Destroy(true)) - } - - if c.AdditionalCLIOptions != nil { - if c.AdditionalCLIOptions.Plan.AllowDeferral { - opts = append(opts, tfexec.AllowDeferral(true)) - } - if c.AdditionalCLIOptions.Plan.NoRefresh { - opts = append(opts, tfexec.Refresh(false)) - } - } - - return wd.CreatePlan(ctx, opts...) - }) - if err != nil { - if step.PlanOnly { - return fmt.Errorf("Error running non-refresh plan: %w", err) - } - - return fmt.Errorf("Error running post-apply non-refresh plan: %w", err) - } - - var plan *tfjson.Plan - err = runProviderCommand(ctx, t, wd, providers, func() error { - var err error - plan, err = wd.SavedPlan(ctx) - return err - }) - if err != nil { - if step.PlanOnly { - return fmt.Errorf("Error reading saved non-refresh plan: %w", err) - } - - return fmt.Errorf("Error reading saved post-apply non-refresh plan: %w", err) - } - - // Run post-apply, pre-refresh plan checks - if len(step.ConfigPlanChecks.PostApplyPreRefresh) > 0 { - err = runPlanChecks(ctx, t, plan, step.ConfigPlanChecks.PostApplyPreRefresh) - if err != nil { - if step.PlanOnly { - return fmt.Errorf("Non-refresh plan checks(s) failed:\n%w", err) - } - - return fmt.Errorf("Post-apply, pre-refresh plan check(s) failed:\n%w", err) - } - } - - if !planIsEmpty(plan, helper.TerraformVersion()) && !step.ExpectNonEmptyPlan { - var stdout string - err = runProviderCommand(ctx, t, wd, providers, func() error { - var err error - stdout, err = wd.SavedPlanRawStdout(ctx) - return err - }) - if err != nil { - return fmt.Errorf("Error reading saved human-readable non-refresh plan output: %w", err) - } - - if step.PlanOnly { - return fmt.Errorf("The non-refresh plan was not empty.\nstdout:\n\n%s", stdout) - } - - return fmt.Errorf("After applying this test step, the non-refresh plan was not empty.\nstdout:\n\n%s", stdout) - } - - // do another plan - err = runProviderCommand(ctx, t, wd, providers, func() error { - var opts []tfexec.PlanOption - if step.Destroy { - opts = append(opts, tfexec.Destroy(true)) - - if step.PreventPostDestroyRefresh { - opts = append(opts, tfexec.Refresh(false)) - } - } - - if c.AdditionalCLIOptions != nil { - if c.AdditionalCLIOptions.Plan.AllowDeferral { - opts = append(opts, tfexec.AllowDeferral(true)) - } - if c.AdditionalCLIOptions.Plan.NoRefresh { - opts = append(opts, tfexec.Refresh(false)) - } - } - - return wd.CreatePlan(ctx, opts...) - }) - if err != nil { - if step.PlanOnly { - return fmt.Errorf("Error running refresh plan: %w", err) - } - - return fmt.Errorf("Error running post-apply refresh plan: %w", err) - } - - err = runProviderCommand(ctx, t, wd, providers, func() error { - var err error - plan, err = wd.SavedPlan(ctx) - return err - }) - if err != nil { - if step.PlanOnly { - return fmt.Errorf("Error reading refresh plan: %w", err) - } - - return fmt.Errorf("Error reading post-apply refresh plan: %w", err) - } - - // Run post-apply, post-refresh plan checks - if len(step.ConfigPlanChecks.PostApplyPostRefresh) > 0 { - err = runPlanChecks(ctx, t, plan, step.ConfigPlanChecks.PostApplyPostRefresh) - if err != nil { - return fmt.Errorf("Post-apply refresh plan check(s) failed:\n%w", err) - } - } - - // check if plan is empty - if !planIsEmpty(plan, helper.TerraformVersion()) && !step.ExpectNonEmptyPlan { - var stdout string - err = runProviderCommand(ctx, t, wd, providers, func() error { - var err error - stdout, err = wd.SavedPlanRawStdout(ctx) - return err - }) - if err != nil { - return fmt.Errorf("Error reading human-readable refresh plan output: %w", err) - } - - if step.PlanOnly { - return fmt.Errorf("The refresh plan was not empty.\nstdout\n\n%s", stdout) - } - - return fmt.Errorf("After applying this test step, the refresh plan was not empty.\nstdout\n\n%s", stdout) - } else if step.ExpectNonEmptyPlan && planIsEmpty(plan, helper.TerraformVersion()) { - return errors.New("Expected a non-empty plan, but got an empty refresh plan") - } - - // ID-ONLY REFRESH - // If we've never checked an id-only refresh and our state isn't - // empty, find the first resource and test it. - if c.IDRefreshName != "" { - logging.HelperResourceTrace(ctx, "Using TestCase IDRefreshName") - - var state *terraform.State - - err = runProviderCommand(ctx, t, wd, providers, func() error { - _, state, err = getState(ctx, t, wd) - if err != nil { - return err - } - return nil - }) - - if err != nil { - return err - } - - //nolint:staticcheck // legacy usage - if state.Empty() { - return nil - } - - var idRefreshCheck *terraform.ResourceState - - // Find the first non-nil resource in the state - for _, m := range state.Modules { - if len(m.Resources) > 0 { - if v, ok := m.Resources[c.IDRefreshName]; ok { - idRefreshCheck = v - } - - break - } - } - - // If we have an instance to check for refreshes, do it - // immediately. We do it in the middle of another test - // because it shouldn't affect the overall state (refresh - // is read-only semantically) and we want to fail early if - // this fails. If refresh isn't read-only, then this will have - // caught a different bug. - if idRefreshCheck != nil { - fmt.Println("Not Writing by testing ID Refresh") - if err := testIDRefresh(ctx, t, c, wd, step, idRefreshCheck, providers, stepIndex, helper); err != nil { - return fmt.Errorf( - "[ERROR] Test: ID-only test failed: %s", err) - } - } - } - - if refreshAfterApply && !step.Destroy && !step.PlanOnly { - if len(c.Steps) > stepIndex+1 { - // If the next step is a refresh, then we have no need to refresh here - if !c.Steps[stepIndex+1].RefreshState { - // Log a searchable message to easily determine when this is no longer being used - logging.HelperResourceDebug(ctx, EnvTfAccRefreshAfterApply+": running apply -refresh-only -refresh=true") - err := runProviderCommandApplyRefreshOnly(ctx, t, wd, providers) - if err != nil { - return fmt.Errorf("Error running apply refresh-only: %w", err) - } - } - } - } - - return nil -} diff --git a/querycheck/testing_new_import_state.go b/querycheck/testing_new_import_state.go deleted file mode 100644 index 59639333..00000000 --- a/querycheck/testing_new_import_state.go +++ /dev/null @@ -1,571 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -import ( - "context" - "encoding/json" - "fmt" - "reflect" - "strings" - - "github.com/hashicorp/go-version" - - tfjson "github.com/hashicorp/terraform-json" - - "github.com/google/go-cmp/cmp" - "github.com/mitchellh/go-testing-interface" - - "github.com/hashicorp/terraform-plugin-testing/config" - "github.com/hashicorp/terraform-plugin-testing/internal/logging" - "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" - "github.com/hashicorp/terraform-plugin-testing/internal/teststep" - "github.com/hashicorp/terraform-plugin-testing/terraform" - "github.com/hashicorp/terraform-plugin-testing/tfversion" -) - -func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest.Helper, testCaseWorkingDir *plugintest.WorkingDir, step TestStep, priorStepCfg teststep.Config, providers *providerFactories, stepNumber int) error { - t.Helper() - - // step.ImportStateKind implicitly defaults to the zero-value (ImportCommandWithID) for backward compatibility - kind := step.ImportStateKind - importStatePersist := step.ImportStatePersist - - if err := importStatePreconditions(t, helper, step); err != nil { - return err - } - - resourceName := step.ResourceName - if resourceName == "" { - t.Fatal("ResourceName is required for an import state test") - } - - // get state from check sequence - var state *terraform.State - var stateJSON *tfjson.State - var err error - - err = runProviderCommand(ctx, t, testCaseWorkingDir, providers, func() error { - stateJSON, state, err = getState(ctx, t, testCaseWorkingDir) - if err != nil { - return err - } - return nil - }) - if err != nil { - t.Fatalf("Error getting state: %s", err) - } - - // Determine the ID to import - var importId string - switch { - case step.ImportStateIdFunc != nil: - logging.HelperResourceTrace(ctx, "Using TestStep ImportStateIdFunc for import identifier") - - var err error - - logging.HelperResourceDebug(ctx, "Calling TestStep ImportStateIdFunc") - - importId, err = step.ImportStateIdFunc(state) - - if err != nil { - t.Fatal(err) - } - - logging.HelperResourceDebug(ctx, "Called TestStep ImportStateIdFunc") - case step.ImportStateId != "": - logging.HelperResourceTrace(ctx, "Using TestStep ImportStateId for import identifier") - - importId = step.ImportStateId - default: - logging.HelperResourceTrace(ctx, "Using resource identifier for import identifier") - - resource, err := testResource(resourceName, state) - if err != nil { - t.Fatal(err) - } - importId = resource.Primary.ID - } - - if step.ImportStateIdPrefix != "" { - logging.HelperResourceTrace(ctx, "Prepending TestStep ImportStateIdPrefix for import identifier") - - importId = step.ImportStateIdPrefix + importId - } - - logging.HelperResourceTrace(ctx, fmt.Sprintf("Using import identifier: %s", importId)) - - var priorIdentityValues map[string]any - - if kind.plannable() && kind.resourceIdentity() { - 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) - } - } - - testStepConfigRequest := config.TestStepConfigRequest{ - StepNumber: stepNumber, - TestName: t.Name(), - } - testStepConfig := teststep.Configuration(teststep.PrepareConfigurationRequest{ - Directory: step.ConfigDirectory, - File: step.ConfigFile, - Raw: step.Config, - TestStepConfigRequest: testStepConfigRequest, - }.Exec()) - - // If the current import state test step doesn't have configuration, use the prior test step config - if testStepConfig == nil { - if priorStepCfg == nil { - t.Fatal("Cannot import state with no specified config") - } - - logging.HelperResourceTrace(ctx, "Using prior TestStep Config for import") - - testStepConfig = priorStepCfg - } - - switch { - case step.ImportStateConfigExact: - break - - case kind.plannable() && kind.resourceIdentity(): - testStepConfig = appendImportBlockWithIdentity(testStepConfig, resourceName, priorIdentityValues) - - case kind.plannable(): - testStepConfig = appendImportBlock(testStepConfig, resourceName, importId) - } - - var workingDir *plugintest.WorkingDir - if importStatePersist { - workingDir = testCaseWorkingDir - } else { - workingDir = helper.RequireNewWorkingDir(ctx, t, "") - defer workingDir.Close() - } - - err = workingDir.SetConfig(ctx, testStepConfig, step.ConfigVariables) - if err != nil { - t.Fatalf("Error setting test config: %s", err) - } - - if kind.plannable() { - if stepNumber > 1 { - err = workingDir.CopyState(ctx, testCaseWorkingDir.StateFilePath()) - if err != nil { - t.Fatalf("copying state: %s", err) - } - - err = runProviderCommand(ctx, t, workingDir, providers, func() error { - return workingDir.RemoveResource(ctx, resourceName) - }) - if err != nil { - t.Fatalf("removing resource %s from copied state: %s", resourceName, err) - } - } - } - - if !importStatePersist { - err = runProviderCommand(ctx, t, workingDir, providers, func() error { - return workingDir.Init(ctx) - }) - if err != nil { - t.Fatalf("Error running init: %s", err) - } - } - - if kind.plannable() { - return testImportBlock(ctx, t, workingDir, providers, resourceName, step, priorIdentityValues) - } else { - return testImportCommand(ctx, t, workingDir, providers, resourceName, importId, step, state) - } -} - -func testImportBlock(ctx context.Context, t testing.T, workingDir *plugintest.WorkingDir, providers *providerFactories, resourceName string, step TestStep, priorIdentityValues map[string]any) error { - kind := step.ImportStateKind - - err := runProviderCommandCreatePlan(ctx, t, workingDir, providers) - if err != nil { - return fmt.Errorf("generating plan with import config: %s", err) - } - - plan, err := runProviderCommandSavedPlan(ctx, t, workingDir, providers) - if err != nil { - return fmt.Errorf("reading generated plan with import config: %s", err) - } - - logging.HelperResourceDebug(ctx, fmt.Sprintf("ImportBlockWithId: %d resource changes", len(plan.ResourceChanges))) - - // Verify reasonable things about the plan - var resourceChangeUnderTest *tfjson.ResourceChange - - if len(plan.ResourceChanges) == 0 { - return fmt.Errorf("importing resource %s: expected a resource change, got no changes", resourceName) - } - - for _, change := range plan.ResourceChanges { - if change.Address == resourceName { - resourceChangeUnderTest = change - } - } - - if resourceChangeUnderTest == nil || resourceChangeUnderTest.Change == nil || resourceChangeUnderTest.Change.Actions == nil { - return fmt.Errorf("importing resource %s: expected a resource change, got no changes", resourceName) - } - - change := resourceChangeUnderTest.Change - actions := change.Actions - importing := change.Importing - - switch { - case importing == nil: - return fmt.Errorf("importing resource %s: expected an import operation, got %q action with plan \nstdout:\n\n%s", resourceChangeUnderTest.Address, actions, savedPlanRawStdout(ctx, t, workingDir, providers)) - // By default we want to ensure there isn't a proposed plan after importing, but for some resources this is unavoidable. - // An example would be importing a resource that cannot read it's entire value back from the remote API. - case !step.ExpectNonEmptyPlan && !actions.NoOp(): - return fmt.Errorf("importing resource %s: expected a no-op import operation, got %q action with plan \nstdout:\n\n%s", resourceChangeUnderTest.Address, actions, savedPlanRawStdout(ctx, t, workingDir, providers)) - } - - if err := runPlanChecks(ctx, t, plan, step.ImportPlanChecks.PreApply); err != nil { - return err - } - - if kind.resourceIdentity() { - 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 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) - }) - if err != nil { - return err - } - - var importState *terraform.State - err = runProviderCommand(ctx, t, workingDir, providers, func() error { - _, importState, err = getState(ctx, t, workingDir) - if err != nil { - return err - } - return nil - }) - if err != nil { - t.Fatalf("Error getting state: %s", err) - } - - logging.HelperResourceDebug(ctx, fmt.Sprintf("State after import: %d resources in the root module", len(importState.RootModule().Resources))) - - // Go through the imported state and verify - if step.ImportStateCheck != nil { - logging.HelperResourceTrace(ctx, "Using TestStep ImportStateCheck") - runImportStateCheckFunction(ctx, t, importState, step) - } - - // Verify that all the states match - if step.ImportStateVerify { - logging.HelperResourceTrace(ctx, "Using TestStep ImportStateVerify") - - // Ensure that we do not match against data sources as they - // cannot be imported and are not what we want to verify. - // Mode is not present in ResourceState so we use the - // stringified ResourceStateKey for comparison. - newResources := make(map[string]*terraform.ResourceState) - for k, v := range importState.RootModule().Resources { - if !strings.HasPrefix(k, "data.") { - newResources[k] = v - } - } - oldResources := make(map[string]*terraform.ResourceState) - for k, v := range state.RootModule().Resources { - if !strings.HasPrefix(k, "data.") { - oldResources[k] = v - } - } - - identifierAttribute := step.ImportStateVerifyIdentifierAttribute - - if identifierAttribute == "" { - identifierAttribute = "id" - } - - for _, r := range newResources { - rIdentifier, ok := r.Primary.Attributes[identifierAttribute] - - if !ok { - t.Fatalf("ImportStateVerify: New resource missing identifier attribute %q, ensure attribute value is properly set or use ImportStateVerifyIdentifierAttribute to choose different attribute", identifierAttribute) - } - - // Find the existing resource - var oldR *terraform.ResourceState - for _, r2 := range oldResources { - if r2.Primary == nil || r2.Type != r.Type || r2.Provider != r.Provider { - continue - } - - r2Identifier, ok := r2.Primary.Attributes[identifierAttribute] - - if !ok { - t.Fatalf("ImportStateVerify: Old resource missing identifier attribute %q, ensure attribute value is properly set or use ImportStateVerifyIdentifierAttribute to choose different attribute", identifierAttribute) - } - - if r2Identifier == rIdentifier { - oldR = r2 - break - } - } - if oldR == nil || oldR.Primary == nil { - t.Fatalf( - "Failed state verification, resource with ID %s not found", - rIdentifier) - } - - // don't add empty flatmapped containers, so we can more easily - // compare the attributes - skipEmpty := func(k, v string) bool { - if strings.HasSuffix(k, ".#") || strings.HasSuffix(k, ".%") { - if v == "0" { - return true - } - } - return false - } - - // Compare their attributes - actual := make(map[string]string) - for k, v := range r.Primary.Attributes { - if skipEmpty(k, v) { - continue - } - actual[k] = v - } - - expected := make(map[string]string) - for k, v := range oldR.Primary.Attributes { - if skipEmpty(k, v) { - continue - } - expected[k] = v - } - - // Remove fields we're ignoring - for _, v := range step.ImportStateVerifyIgnore { - for k := range actual { - if strings.HasPrefix(k, v) { - delete(actual, k) - } - } - for k := range expected { - if strings.HasPrefix(k, v) { - delete(expected, k) - } - } - } - - // timeouts are only _sometimes_ added to state. To - // account for this, just don't compare timeouts at - // all. - for k := range actual { - if strings.HasPrefix(k, "timeouts.") { - delete(actual, k) - } - if k == "timeouts" { - delete(actual, k) - } - } - for k := range expected { - if strings.HasPrefix(k, "timeouts.") { - delete(expected, k) - } - if k == "timeouts" { - delete(expected, k) - } - } - - if !reflect.DeepEqual(actual, expected) { - // Determine only the different attributes - // go-cmp tries to show surrounding identical map key/value for - // context of differences, which may be confusing. - for k, v := range expected { - if av, ok := actual[k]; ok && v == av { - delete(expected, k) - delete(actual, k) - } - } - - if diff := cmp.Diff(expected, actual); diff != "" { - return fmt.Errorf("ImportStateVerify attributes not equivalent. Difference is shown below. The - symbol indicates attributes missing after import.\n\n%s", diff) - } - } - } - } - - return nil -} - -func appendImportBlock(config teststep.Config, resourceName string, importID string) teststep.Config { - return config.Append( - fmt.Sprintf(``+"\n"+ - `import {`+"\n"+ - ` to = %s`+"\n"+ - ` id = %q`+"\n"+ - `}`, - resourceName, importID)) -} - -func appendImportBlockWithIdentity(config teststep.Config, resourceName string, identityValues map[string]any) teststep.Config { - configBuilder := strings.Builder{} - configBuilder.WriteString(fmt.Sprintf(``+"\n"+ - `import {`+"\n"+ - ` to = %s`+"\n"+ - ` identity = {`+"\n", - resourceName)) - - for k, v := range identityValues { - // It's valid for identity attributes to be null, we can just omit it from config - if v == nil { - continue - } - - switch v := v.(type) { - case bool: - configBuilder.WriteString(fmt.Sprintf(` %q = %t`+"\n", k, v)) - - case []any: - var quotedV []string - for _, v := range v { - quotedV = append(quotedV, fmt.Sprintf(`%q`, v)) - } - configBuilder.WriteString(fmt.Sprintf(` %q = [%s]`+"\n", k, strings.Join(quotedV, ", "))) - - case json.Number: - configBuilder.WriteString(fmt.Sprintf(` %q = %s`+"\n", k, v)) - - case string: - configBuilder.WriteString(fmt.Sprintf(` %q = %q`+"\n", k, v)) - - default: - panic(fmt.Sprintf("unexpected type %T for identity value %q", v, k)) - } - } - - configBuilder.WriteString(` }` + "\n") - configBuilder.WriteString(`}` + "\n") - - return config.Append(configBuilder.String()) -} - -func importStatePreconditions(t testing.T, helper *plugintest.Helper, step TestStep) error { - t.Helper() - - kind := step.ImportStateKind - versionUnderTest := *helper.TerraformVersion().Core() - resourceIdentityMinimumVersion := version.Must(version.NewVersion("1.12.0")) - - // Instead of calling [t.Fatal], we return an error. This package's unit tests can use [TestStep.ExpectError] to match - // on the error message. An alternative, [plugintest.TestExpectTFatal], does not have access to logged error messages, - // so it is open to false positives on this complex code path. - // - // Multiple cases may match, so check the most specific cases first - switch { - case kind.resourceIdentity() && versionUnderTest.LessThan(resourceIdentityMinimumVersion): - return fmt.Errorf( - `ImportState steps using resource identity require Terraform 1.12.0 or later. Either ` + - `upgrade the Terraform version running the test or add a ` + "`TerraformVersionChecks`" + ` to ` + - `the test case to skip this test.` + "\n\n" + - `https://developer.hashicorp.com/terraform/plugin/testing/acceptance-tests/tfversion-checks#skip-version-checks`) - - case kind.plannable() && versionUnderTest.LessThan(tfversion.Version1_5_0): - return fmt.Errorf( - `ImportState steps using plannable import blocks require Terraform 1.5.0 or later. Either ` + - `upgrade the Terraform version running the test or add a ` + "`TerraformVersionChecks`" + ` to ` + - `the test case to skip this test.` + "\n\n" + - `https://developer.hashicorp.com/terraform/plugin/testing/acceptance-tests/tfversion-checks#skip-version-checks`) - - case kind.plannable() && step.ImportStatePersist: - return fmt.Errorf(`ImportStatePersist is not supported with plannable import blocks`) - - case kind.plannable() && step.ImportStateVerify: - return fmt.Errorf(`ImportStateVerify is not supported with plannable import blocks`) - } - - return nil -} - -func resourcesFromState(stateValues *tfjson.StateValues) []*tfjson.StateResource { - if stateValues == nil || stateValues.RootModule == nil { - return []*tfjson.StateResource{} - } - - return stateValues.RootModule.Resources -} - -func identityValuesFromStateValues(stateValues *tfjson.StateValues, resourceName string) map[string]any { - var resource *tfjson.StateResource - resources := resourcesFromState(stateValues) - - for _, r := range resources { - if r.Address == resourceName { - resource = r - break - } - } - - if resource == nil || len(resource.IdentityValues) == 0 { - return map[string]any{} - } - - return resource.IdentityValues -} - -func runImportStateCheckFunction(ctx context.Context, t testing.T, importState *terraform.State, step TestStep) { - t.Helper() - - var states []*terraform.InstanceState - for address, r := range importState.RootModule().Resources { - if strings.HasPrefix(address, "data.") { - continue - } - - if r.Primary == nil { - continue - } - - is := r.Primary.DeepCopy() //nolint:staticcheck // legacy usage - is.Ephemeral.Type = r.Type // otherwise the check function cannot see the type - states = append(states, is) - } - - logging.HelperResourceTrace(ctx, "Calling TestStep ImportStateCheck") - - if err := step.ImportStateCheck(states); err != nil { - t.Fatal(err) - } - - logging.HelperResourceTrace(ctx, "Called TestStep ImportStateCheck") -} - -func savedPlanRawStdout(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, providers *providerFactories) string { - t.Helper() - - var stdout string - - err := runProviderCommand(ctx, t, wd, providers, func() error { - var err error - stdout, err = wd.SavedPlanRawStdout(ctx) - return err - }) - - if err != nil { - return fmt.Sprintf("error retrieving formatted plan output: %s", err) - } - return stdout -} diff --git a/querycheck/testing_new_refresh_state.go b/querycheck/testing_new_refresh_state.go deleted file mode 100644 index 62aaaad4..00000000 --- a/querycheck/testing_new_refresh_state.go +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -import ( - "context" - "fmt" - - tfjson "github.com/hashicorp/terraform-json" - "github.com/mitchellh/go-testing-interface" - - "github.com/hashicorp/terraform-plugin-testing/terraform" - - "github.com/hashicorp/terraform-plugin-testing/internal/logging" - "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" -) - -func testStepNewRefreshState(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, step TestStep, providers *providerFactories) error { - t.Helper() - - var err error - // Explicitly ensure prior state exists before refresh. - err = runProviderCommand(ctx, t, wd, providers, func() error { - _, _, err = getState(ctx, t, wd) - if err != nil { - return err - } - return nil - }) - if err != nil { - t.Fatalf("Error getting state: %s", err) - } - - err = runProviderCommand(ctx, t, wd, providers, func() error { - return wd.Refresh(ctx) - }) - if err != nil { - return err - } - - var refreshState *terraform.State - err = runProviderCommand(ctx, t, wd, providers, func() error { - _, refreshState, err = getState(ctx, t, wd) - if err != nil { - return err - } - return nil - }) - if err != nil { - t.Fatalf("Error getting state: %s", err) - } - - // Go through the refreshed state and verify - if step.Check != nil { - logging.HelperResourceDebug(ctx, "Calling TestStep Check for RefreshState") - - if err := step.Check(refreshState); err != nil { - t.Fatal(err) - } - - logging.HelperResourceDebug(ctx, "Called TestStep Check for RefreshState") - } - - // do a plan - err = runProviderCommand(ctx, t, wd, providers, func() error { - return wd.CreatePlan(ctx) - }) - if err != nil { - return fmt.Errorf("Error running post-refresh plan: %w", err) - } - - var plan *tfjson.Plan - err = runProviderCommand(ctx, t, wd, providers, func() error { - var err error - plan, err = wd.SavedPlan(ctx) - return err - }) - if err != nil { - return fmt.Errorf("Error retrieving post-refresh plan: %w", err) - } - - // Run post-refresh plan checks - if len(step.RefreshPlanChecks.PostRefresh) > 0 { - err = runPlanChecks(ctx, t, plan, step.RefreshPlanChecks.PostRefresh) - if err != nil { - return fmt.Errorf("Post-refresh plan check(s) failed:\n%w", err) - } - } - - if !planIsEmpty(plan, wd.GetHelper().TerraformVersion()) && !step.ExpectNonEmptyPlan { - var stdout string - err = runProviderCommand(ctx, t, wd, providers, func() error { - var err error - stdout, err = wd.SavedPlanRawStdout(ctx) - return err - }) - if err != nil { - return fmt.Errorf("Error retrieving formatted plan output: %w", err) - } - return fmt.Errorf("After refreshing state during this test step, a followup plan was not empty.\nstdout:\n\n%s", stdout) - } - - return nil -} diff --git a/querycheck/teststep_providers.go b/querycheck/teststep_providers.go deleted file mode 100644 index 11eb6fba..00000000 --- a/querycheck/teststep_providers.go +++ /dev/null @@ -1,258 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -import ( - "context" - "encoding/json" - "fmt" - "strings" - - "github.com/hashicorp/go-version" -) - -// tfBlockMinReqTFVersion is used to prevent errors arising from -// adding required providers to the terraform block when Terraform -// is any version prior to v1.0.0 -const tfBlockMinReqTFVersion = "1.0.0" - -// mergedConfig prepends any necessary terraform configuration blocks to the -// TestStep Config. -// -// If there are ExternalProviders configurations in either the TestCase or -// TestStep, the terraform configuration block should be included with the -// step configuration to prevent errors with providers outside the -// registry.terraform.io hostname or outside the hashicorp namespace. -// This is only necessary when using TestStep.Config. -// -// When TestStep.ConfigDirectory is used, the expectation is that the -// Terraform configuration files will specify a terraform configuration -// block and/or provider blocks as necessary. -func (s TestStep) mergedConfig(ctx context.Context, testCase TestCase, configHasTerraformBlock, configHasProviderBlock bool, tfVersion *version.Version) (string, error) { - var config strings.Builder - - // Prevent issues with existing configurations containing the terraform - // configuration block. - if configHasTerraformBlock { - config.WriteString(s.Config) - - return config.String(), nil - } - - if testCase.hasProviders(ctx) { - cfg, err := s.providerConfigTestCase(ctx, configHasProviderBlock, testCase, tfVersion) - - if err != nil { - return "", err - } - - config.WriteString(cfg) - } else { - cfg, err := s.providerConfig(ctx, configHasProviderBlock, tfVersion) - - if err != nil { - return "", err - } - - config.WriteString(cfg) - } - - config.WriteString(s.Config) - - return config.String(), nil -} - -// providerConfig takes the list of providers in a TestStep and returns a -// config with only empty provider blocks. This is useful for Import, where no -// config is provided, but the providers must be defined. -func (s TestStep) providerConfig(_ context.Context, skipProviderBlock bool, tfVersion *version.Version) (string, error) { - var providerBlocks, requiredProviderBlocks strings.Builder - - for name, externalProvider := range s.ExternalProviders { - if !skipProviderBlock { - providerBlocks.WriteString(fmt.Sprintf("provider %q {}\n", name)) - } - - if externalProvider.Source == "" && externalProvider.VersionConstraint == "" { - continue - } - - requiredProviderBlocks.WriteString(fmt.Sprintf(" %s = {\n", name)) - - if externalProvider.Source != "" { - requiredProviderBlocks.WriteString(fmt.Sprintf(" source = %q\n", externalProvider.Source)) - } - - if externalProvider.VersionConstraint != "" { - requiredProviderBlocks.WriteString(fmt.Sprintf(" version = %q\n", externalProvider.VersionConstraint)) - } - - requiredProviderBlocks.WriteString(" }\n") - } - - minReqVersion, err := version.NewVersion(tfBlockMinReqTFVersion) - - if err != nil { - return "", err - } - - for name := range s.ProviderFactories { - if tfVersion.LessThan(minReqVersion) { - break - } - - requiredProviderBlocks.WriteString(addTerraformBlockSource(name, s.Config)) - } - - for name := range s.ProtoV5ProviderFactories { - if tfVersion.LessThan(minReqVersion) { - break - } - - requiredProviderBlocks.WriteString(addTerraformBlockSource(name, s.Config)) - } - - for name := range s.ProtoV6ProviderFactories { - if tfVersion.LessThan(minReqVersion) { - break - } - - requiredProviderBlocks.WriteString(addTerraformBlockSource(name, s.Config)) - } - - if requiredProviderBlocks.Len() > 0 { - return fmt.Sprintf(` -terraform { - required_providers { -%[1]s - } -} - -%[2]s -`, strings.TrimSuffix(requiredProviderBlocks.String(), "\n"), providerBlocks.String()), nil - } - - return providerBlocks.String(), nil -} - -func (s TestStep) providerConfigTestCase(_ context.Context, skipProviderBlock bool, testCase TestCase, tfVersion *version.Version) (string, error) { - var providerBlocks, requiredProviderBlocks strings.Builder - - providerNames := make(map[string]struct{}, len(testCase.Providers)) - - for name := range testCase.Providers { - providerNames[name] = struct{}{} - } - - for name := range testCase.ProviderFactories { - delete(providerNames, name) - } - - // [BF] The Providers field handling predates the logic being moved to this - // method. It's not entirely clear to me at this time why this field - // is being used and not the others, but leaving it here just in case - // it does have a special purpose that wasn't being unit tested prior. - for name := range providerNames { - providerBlocks.WriteString(fmt.Sprintf("provider %q {}\n", name)) - - requiredProviderBlocks.WriteString(fmt.Sprintf(" %s = {\n", name)) - - requiredProviderBlocks.WriteString(" }\n") - } - - for name, externalProvider := range testCase.ExternalProviders { - if !skipProviderBlock { - providerBlocks.WriteString(fmt.Sprintf("provider %q {}\n", name)) - } - - if externalProvider.Source == "" && externalProvider.VersionConstraint == "" { - continue - } - - requiredProviderBlocks.WriteString(fmt.Sprintf(" %s = {\n", name)) - - if externalProvider.Source != "" { - requiredProviderBlocks.WriteString(fmt.Sprintf(" source = %q\n", externalProvider.Source)) - } - - if externalProvider.VersionConstraint != "" { - requiredProviderBlocks.WriteString(fmt.Sprintf(" version = %q\n", externalProvider.VersionConstraint)) - } - - requiredProviderBlocks.WriteString(" }\n") - } - - minReqVersion, err := version.NewVersion(tfBlockMinReqTFVersion) - - if err != nil { - return "", err - } - - for name := range testCase.ProviderFactories { - if tfVersion.LessThan(minReqVersion) { - break - } - - providerFactoryBlocks := addTerraformBlockSource(name, s.Config) - - if len(providerFactoryBlocks) > 0 { - requiredProviderBlocks.WriteString(providerFactoryBlocks) - } - } - - for name := range testCase.ProtoV5ProviderFactories { - if tfVersion.LessThan(minReqVersion) { - break - } - - protov5ProviderFactoryBlocks := addTerraformBlockSource(name, s.Config) - - if len(protov5ProviderFactoryBlocks) > 0 { - requiredProviderBlocks.WriteString(protov5ProviderFactoryBlocks) - } - } - - for name := range testCase.ProtoV6ProviderFactories { - if tfVersion.LessThan(minReqVersion) { - break - } - - protov6ProviderFactoryBlocks := addTerraformBlockSource(name, s.Config) - - if len(protov6ProviderFactoryBlocks) > 0 { - requiredProviderBlocks.WriteString(addTerraformBlockSource(name, s.Config)) - } - } - - if requiredProviderBlocks.Len() > 0 { - return fmt.Sprintf(` -terraform { - required_providers { -%[1]s - } -} - -%[2]s -`, strings.TrimSuffix(requiredProviderBlocks.String(), "\n"), providerBlocks.String()), nil - } - - return providerBlocks.String(), nil -} - -func addTerraformBlockSource(name, config string) string { - var js json.RawMessage - - // Do not process JSON. - if err := json.Unmarshal([]byte(config), &js); err == nil { - return "" - } - - var providerBlocks strings.Builder - - providerBlocks.WriteString(fmt.Sprintf(" %s = {\n", name)) - providerBlocks.WriteString(fmt.Sprintf(" source = %q\n", getProviderAddr(name))) - providerBlocks.WriteString(" }\n") - - return providerBlocks.String() -} diff --git a/querycheck/teststep_validate.go b/querycheck/teststep_validate.go deleted file mode 100644 index bc8d90dc..00000000 --- a/querycheck/teststep_validate.go +++ /dev/null @@ -1,245 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-testing/config" - "github.com/hashicorp/terraform-plugin-testing/internal/logging" - "github.com/hashicorp/terraform-plugin-testing/internal/teststep" -) - -// testStepValidateRequest contains data for the (TestStep).validate() method. -type testStepValidateRequest struct { - // StepConfiguration contains the TestStep configuration derived from - // TestStep.Config, TestStep.ConfigDirectory, or TestStep.ConfigFile. - StepConfiguration teststep.Config - - // StepNumber is the index of the TestStep in the TestCase.Steps. - StepNumber int - - // TestCaseHasExternalProviders is enabled if the TestCase has - // ExternalProviders. - TestCaseHasExternalProviders bool - - // TestCaseHasProviders is enabled if the TestCase has set any of - // ExternalProviders, ProtoV5ProviderFactories, ProtoV6ProviderFactories, - // or ProviderFactories. - TestCaseHasProviders bool - - // TestName is the name of the test. - TestName string -} - -// hasExternalProviders returns true if the TestStep has -// ExternalProviders set. -func (s TestStep) hasExternalProviders() bool { - return len(s.ExternalProviders) > 0 -} - -// hasProviders returns true if the TestStep has set any of the -// ExternalProviders, ProtoV5ProviderFactories, ProtoV6ProviderFactories, or -// ProviderFactories fields. It will also return true if ConfigDirectory or -// Config contain terraform configuration which specify a provider block. -func (s TestStep) hasProviders(ctx context.Context, stepIndex int, testName string) (bool, error) { - if len(s.ExternalProviders) > 0 { - return true, nil - } - - if len(s.ProtoV5ProviderFactories) > 0 { - return true, nil - } - - if len(s.ProtoV6ProviderFactories) > 0 { - return true, nil - } - - if len(s.ProviderFactories) > 0 { - return true, nil - } - - configRequest := teststep.PrepareConfigurationRequest{ - Directory: s.ConfigDirectory, - File: s.ConfigFile, - TestStepConfigRequest: config.TestStepConfigRequest{ - StepNumber: stepIndex + 1, - TestName: testName, - }, - }.Exec() - - cfg := teststep.Configuration(configRequest) - - var cfgHasProviders bool - - if cfg != nil { - var err error - - cfgHasProviders, err = cfg.HasProviderBlock(ctx) - - if err != nil { - return false, err - } - } - - if cfgHasProviders { - return true, nil - } - - return false, nil -} - -// validate ensures the TestStep is valid based on the following criteria: -// -// - Config or ImportState or RefreshState is set. -// - Config and RefreshState are not both set. -// - RefreshState and Destroy are not both set. -// - RefreshState is not the first TestStep. -// - Providers are not specified (ExternalProviders, -// ProtoV5ProviderFactories, ProtoV6ProviderFactories, ProviderFactories) -// if specified at the TestCase level. -// - Providers are specified (ExternalProviders, ProtoV5ProviderFactories, -// ProtoV6ProviderFactories, ProviderFactories) if not specified at the -// TestCase level. -// - No overlapping ExternalProviders and ProviderFactories entries -// - ResourceName is not empty when ImportState is true, ImportStateIdFunc -// is not set, and ImportStateId is not set. -// - ConfigPlanChecks (PreApply, PostApplyPreRefresh, PostApplyPostRefresh) are only set when Config is set. -// - ConfigPlanChecks.PreApply are only set when PlanOnly is false. -// - RefreshPlanChecks (PostRefresh) are only set when RefreshState is set. -func (s TestStep) validate(ctx context.Context, req testStepValidateRequest) error { - ctx = logging.TestStepNumberContext(ctx, req.StepNumber) - - logging.HelperResourceTrace(ctx, "Validating TestStep") - - if req.StepConfiguration == nil && !s.ImportState && !s.RefreshState { - err := fmt.Errorf("TestStep missing Config or ConfigDirectory or ConfigFile or ImportState or RefreshState") - logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) - return err - } - - if req.StepConfiguration != nil && s.RefreshState { - err := fmt.Errorf("TestStep cannot have Config or ConfigDirectory or ConfigFile and RefreshState") - logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) - return err - } - - if s.RefreshState && s.Destroy { - err := fmt.Errorf("TestStep cannot have RefreshState and Destroy") - logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) - return err - } - - if s.RefreshState && req.StepNumber == 1 { - err := fmt.Errorf("TestStep cannot have RefreshState as first step") - logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) - return err - } - - if s.ImportState && s.RefreshState { - err := fmt.Errorf("TestStep cannot have ImportState and RefreshState in same step") - logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) - return err - } - - for name := range s.ExternalProviders { - if _, ok := s.ProviderFactories[name]; ok { - err := fmt.Errorf("TestStep provider %q set in both ExternalProviders and ProviderFactories", name) - logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) - return err - } - } - - if req.TestCaseHasExternalProviders && req.StepConfiguration != nil && req.StepConfiguration.HasConfigurationFiles() { - err := fmt.Errorf("Providers must only be specified within the terraform configuration files when using TestStep.Config") - logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) - return err - } - - if s.hasExternalProviders() && req.StepConfiguration != nil && req.StepConfiguration.HasConfigurationFiles() { - err := fmt.Errorf("Providers must only be specified within the terraform configuration files when using TestStep.Config") - logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) - return err - } - - // We need a 0-based step index for consistency - hasProviders, err := s.hasProviders(ctx, req.StepNumber-1, req.TestName) - - if err != nil { - logging.HelperResourceError(ctx, "TestStep error checking for providers", map[string]interface{}{logging.KeyError: err}) - return err - } - - if req.TestCaseHasProviders && hasProviders { - err := fmt.Errorf("Providers must only be specified either at the TestCase or TestStep level") - logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) - return err - } - - var cfgHasProviderBlock bool - - if req.StepConfiguration != nil { - cfgHasProviderBlock, err = req.StepConfiguration.HasProviderBlock(ctx) - - if err != nil { - logging.HelperResourceError(ctx, "TestStep error checking for if configuration has provider block", map[string]interface{}{logging.KeyError: err}) - return err - } - } - - if !req.TestCaseHasProviders && !hasProviders && !cfgHasProviderBlock { - err := fmt.Errorf("Providers must be specified at the TestCase level, or in all TestStep, or in TestStep.ConfigDirectory or TestStep.ConfigFile") - logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) - return err - } - - if s.ImportState { - if s.ImportStateId == "" && s.ImportStateIdFunc == nil && s.ResourceName == "" { - err := fmt.Errorf("TestStep ImportState must be specified with ImportStateId, ImportStateIdFunc, or ResourceName") - logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) - return err - } - } - - if len(s.ConfigPlanChecks.PreApply) > 0 { - if req.StepConfiguration == nil { - err := fmt.Errorf("TestStep ConfigPlanChecks.PreApply must only be specified with Config, ConfigDirectory or ConfigFile") - logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) - return err - } - - if s.PlanOnly { - err := fmt.Errorf("TestStep ConfigPlanChecks.PreApply cannot be run with PlanOnly") - logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) - return err - } - } - - if len(s.ConfigPlanChecks.PostApplyPreRefresh) > 0 && req.StepConfiguration == nil { - err := fmt.Errorf("TestStep ConfigPlanChecks.PostApplyPreRefresh must only be specified with Config, ConfigDirectory or ConfigFile") - logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) - return err - } - - if len(s.ConfigPlanChecks.PostApplyPostRefresh) > 0 && req.StepConfiguration == nil { - err := fmt.Errorf("TestStep ConfigPlanChecks.PostApplyPostRefresh must only be specified with Config, ConfigDirectory or ConfigFile") - logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) - return err - } - - if len(s.RefreshPlanChecks.PostRefresh) > 0 && !s.RefreshState { - err := fmt.Errorf("TestStep RefreshPlanChecks.PostRefresh must only be specified with RefreshState") - logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) - return err - } - - if len(s.ConfigStateChecks) > 0 && req.StepConfiguration == nil { - err := fmt.Errorf("TestStep ConfigStateChecks must only be specified with Config, ConfigDirectory or ConfigFile") - logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) - return err - } - - return nil -} diff --git a/querycheck/tfversion_checks.go b/querycheck/tfversion_checks.go deleted file mode 100644 index 50941749..00000000 --- a/querycheck/tfversion_checks.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -import ( - "context" - - "github.com/hashicorp/go-version" - "github.com/mitchellh/go-testing-interface" - - "github.com/hashicorp/terraform-plugin-testing/tfversion" -) - -func runTFVersionChecks(ctx context.Context, t testing.T, terraformVersion *version.Version, terraformVersionChecks []tfversion.TerraformVersionCheck) { - t.Helper() - - for _, tfVersionCheck := range terraformVersionChecks { - resp := tfversion.CheckTerraformVersionResponse{} - tfVersionCheck.CheckTerraformVersion(ctx, tfversion.CheckTerraformVersionRequest{TerraformVersion: terraformVersion}, &resp) - - if resp.Error != nil { - t.Fatalf(resp.Error.Error()) - } - - if resp.Skip != "" { - t.Skip(resp.Skip) - } - } - -} diff --git a/querycheck/types_test.go b/querycheck/types_test.go deleted file mode 100644 index 808f2478..00000000 --- a/querycheck/types_test.go +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -import ( - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-go/tftypes" -) - -func RequiredBoolAttribute(name string) *tfprotov6.SchemaAttribute { - return &tfprotov6.SchemaAttribute{ - Name: name, - Type: tftypes.Bool, - Required: true, - } -} - -func OptionalComputedListAttribute(name string, elementType tftypes.Type) *tfprotov6.SchemaAttribute { - return &tfprotov6.SchemaAttribute{ - Name: name, - Type: tftypes.List{ElementType: elementType}, - Optional: true, - Computed: true, - } -} - -func RequiredListAttribute(name string, elementType tftypes.Type) *tfprotov6.SchemaAttribute { - return &tfprotov6.SchemaAttribute{ - Name: name, - Type: tftypes.List{ElementType: elementType}, - Required: true, - } -} - -func RequiredNumberAttribute(name string) *tfprotov6.SchemaAttribute { - return &tfprotov6.SchemaAttribute{ - Name: name, - Type: tftypes.Number, - Required: true, - } -} - -func OptionalNumberAttribute(name string) *tfprotov6.SchemaAttribute { - return &tfprotov6.SchemaAttribute{ - Name: name, - Type: tftypes.Number, - Optional: true, - } -} - -func ComputedStringAttribute(name string) *tfprotov6.SchemaAttribute { - return &tfprotov6.SchemaAttribute{ - Name: name, - Type: tftypes.String, - Computed: true, - } -} - -func OptionalStringAttribute(name string) *tfprotov6.SchemaAttribute { - return &tfprotov6.SchemaAttribute{ - Name: name, - Type: tftypes.String, - Optional: true, - } -} - -func RequiredStringAttribute(name string) *tfprotov6.SchemaAttribute { - return &tfprotov6.SchemaAttribute{ - Name: name, - Type: tftypes.String, - Required: true, - } -} From 84b0e715cc23222d54b4d83c0bca19e13a015457 Mon Sep 17 00:00:00 2001 From: Rain Date: Thu, 25 Sep 2025 14:52:12 -0400 Subject: [PATCH 5/9] Updating with correct packages --- querycheck/contains_name_test.go | 18 +- querycheck/examplecloud_list_test.go | 233 ++++++++++++++++++ querycheck/examplecloud_test.go | 143 +++++++++++ querycheck/expect_identity_test.go | 119 +++++++-- querycheck/expect_known_value_test.go | 129 ++++++++-- .../expect_result_length_atleast_test.go | 70 ++++-- querycheck/expect_result_length_exact_test.go | 70 ++++-- querycheck/types_test.go | 74 ++++++ 8 files changed, 789 insertions(+), 67 deletions(-) create mode 100644 querycheck/examplecloud_list_test.go create mode 100644 querycheck/examplecloud_test.go create mode 100644 querycheck/types_test.go diff --git a/querycheck/contains_name_test.go b/querycheck/contains_name_test.go index 6b9cdb73..cd5bf94a 100644 --- a/querycheck/contains_name_test.go +++ b/querycheck/contains_name_test.go @@ -1,14 +1,15 @@ package querycheck_test import ( + "regexp" + "testing" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" r "github.com/hashicorp/terraform-plugin-testing/helper/resource" "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/querycheck" "github.com/hashicorp/terraform-plugin-testing/tfversion" - "regexp" - "testing" ) func TestContainsResourceWithName(t *testing.T) { @@ -21,13 +22,11 @@ func TestContainsResourceWithName(t *testing.T) { ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ ListResources: map[string]testprovider.ListResource{ - // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use - - //"examplecloud_containerette": examplecloudListResource(), + "examplecloud_containerette": examplecloudListResource(), }, Resources: map[string]testprovider.Resource{ - //"examplecloud_containerette": examplecloudResource(), + "examplecloud_containerette": examplecloudResource(), }, }), }, @@ -78,13 +77,10 @@ func TestContainsResourceWithName_NotFound(t *testing.T) { ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ ListResources: map[string]testprovider.ListResource{ - - // TODO: define a resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use - - //"examplecloud_containerette": examplecloudListResource(), + "examplecloud_containerette": examplecloudListResource(), }, Resources: map[string]testprovider.Resource{ - //"examplecloud_containerette": examplecloudResource(), + "examplecloud_containerette": examplecloudResource(), }, }), }, diff --git a/querycheck/examplecloud_list_test.go b/querycheck/examplecloud_list_test.go new file mode 100644 index 00000000..af7cce88 --- /dev/null +++ b/querycheck/examplecloud_list_test.go @@ -0,0 +1,233 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck_test + +import ( + "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/list" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" +) + +func examplecloudListResource() testprovider.ListResource { + return testprovider.ListResource{ + IncludeResource: true, + SchemaResponse: &list.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "resource_group_name", + Type: tftypes.String, + Required: true, + }, + }, + }, + }, + }, + ListResultsStream: &list.ListResultsStream{ + Results: func(push func(list.ListResult) bool) { + push(list.ListResult{ + Resource: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + "resource_group_name": tftypes.String, + "instances": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "foo/banane"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "banane"), + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "instances": tftypes.NewValue(tftypes.Number, 5), + }, + )), + Identity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "resource_group_name": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "name": tftypes.NewValue(tftypes.String, "banane"), + }, + )), + DisplayName: "banane", + }) + push(list.ListResult{ + Resource: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + "resource_group_name": tftypes.String, + "instances": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "foo/ananas"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "ananas"), + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "instances": tftypes.NewValue(tftypes.Number, 9000), + }, + )), + Identity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "resource_group_name": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "name": tftypes.NewValue(tftypes.String, "ananas"), + }, + )), + DisplayName: "ananas", + }) + push(list.ListResult{ + Resource: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + "resource_group_name": tftypes.String, + "instances": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "foo/kiwi"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "kiwi"), + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "instances": tftypes.NewValue(tftypes.Number, 88), + }, + )), + Identity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "resource_group_name": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "name": tftypes.NewValue(tftypes.String, "kiwi"), + }, + )), + DisplayName: "kiwi", + }) + push(list.ListResult{ + Resource: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + "resource_group_name": tftypes.String, + "instances": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "bar/papaya"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "banane"), + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "instances": tftypes.NewValue(tftypes.Number, 3), + }, + )), + Identity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "resource_group_name": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "resource_group_name": tftypes.NewValue(tftypes.String, "bar"), + "name": tftypes.NewValue(tftypes.String, "papaya"), + }, + )), + DisplayName: "papaya", + }) + push(list.ListResult{ + Resource: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + "resource_group_name": tftypes.String, + "instances": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "bar/birne"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "birne"), + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "instances": tftypes.NewValue(tftypes.Number, 8564), + }, + )), + Identity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "resource_group_name": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "resource_group_name": tftypes.NewValue(tftypes.String, "bar"), + "name": tftypes.NewValue(tftypes.String, "birne"), + }, + )), + DisplayName: "birne", + }) + push(list.ListResult{ + Resource: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + "resource_group_name": tftypes.String, + "instances": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "bar/kirsche"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "kirsche"), + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "instances": tftypes.NewValue(tftypes.Number, 500), + }, + )), + Identity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "resource_group_name": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "resource_group_name": tftypes.NewValue(tftypes.String, "bar"), + "name": tftypes.NewValue(tftypes.String, "kirsche"), + }, + )), + DisplayName: "kirsche", + }) + }, + }, + } +} diff --git a/querycheck/examplecloud_test.go b/querycheck/examplecloud_test.go new file mode 100644 index 00000000..9984f84d --- /dev/null +++ b/querycheck/examplecloud_test.go @@ -0,0 +1,143 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck_test + +import ( + "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/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" +) + +func examplecloudResource() testprovider.Resource { + return testprovider.Resource{ + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + "resource_group_name": tftypes.String, + "instances": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "foo/banana"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "banana"), + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "instances": tftypes.NewValue(tftypes.Number, int64(5)), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "resource_group_name": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "name": tftypes.NewValue(tftypes.String, "banana"), + }, + )), + }, + ReadResponse: &resource.ReadResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + "resource_group_name": tftypes.String, + "instances": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "foo/banana"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "banana"), + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "instances": tftypes.NewValue(tftypes.Number, int64(5)), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "resource_group_name": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "name": tftypes.NewValue(tftypes.String, "banana"), + }, + )), + }, + ImportStateResponse: &resource.ImportStateResponse{ + State: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + "resource_group_name": tftypes.String, + "instances": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "foo/banana"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "banana"), + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "instances": tftypes.NewValue(tftypes.Number, int64(5)), + }, + ), + Identity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "resource_group_name": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "resource_group_name": tftypes.NewValue(tftypes.String, "foo"), + "name": tftypes.NewValue(tftypes.String, "banana"), + }, + )), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + ComputedStringAttribute("id"), + RequiredStringAttribute("location"), + RequiredStringAttribute("name"), + RequiredStringAttribute("resource_group_name"), + OptionalNumberAttribute("instances"), + }, + }, + }, + }, + IdentitySchemaResponse: &resource.IdentitySchemaResponse{ + Schema: &tfprotov6.ResourceIdentitySchema{ + Version: 1, + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "resource_group_name", + Type: tftypes.String, + RequiredForImport: true, + }, + { + Name: "name", + Type: tftypes.String, + RequiredForImport: true, + }, + }, + }, + }, + } +} diff --git a/querycheck/expect_identity_test.go b/querycheck/expect_identity_test.go index d296949a..50aeed5a 100644 --- a/querycheck/expect_identity_test.go +++ b/querycheck/expect_identity_test.go @@ -1,12 +1,15 @@ -package querycheck +package querycheck_test import ( + "testing" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" r "github.com/hashicorp/terraform-plugin-testing/helper/resource" "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/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/querycheck" "github.com/hashicorp/terraform-plugin-testing/tfversion" - "testing" ) func TestExpectIdentity(t *testing.T) { @@ -19,18 +22,66 @@ func TestExpectIdentity(t *testing.T) { ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ ListResources: map[string]testprovider.ListResource{ - - // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use - - //"examplecloud_containerette": examplecloudListResource(), + "examplecloud_containerette": examplecloudListResource(), }, Resources: map[string]testprovider.Resource{ - //"examplecloud_containerette": examplecloudResource(), + "examplecloud_containerette": examplecloudResource(), }, }), }, Steps: []r.TestStep{ - // TODO + { // Query mode step 2, operates on .tfquery.hcl files (needs tf file with terraform providers block) + // ```provider "examplecloud" {}``` has a slightly different syntax for a .tfquery.hcl file + // provider bock simulates a real providers workflow + // "config" in this case means configuration of the list resource/filters + + Query: true, + Config: ` + provider "examplecloud" {} + + list "examplecloud_containerette" "test" { + provider = examplecloud + + config { + resource_group_name = "foo" + } + } + + list "examplecloud_containerette" "test2" { + provider = examplecloud + + config { + resource_group_name = "bar" + } + } + `, + QueryResultChecks: []querycheck.QueryResultCheck{ + querycheck.ExpectIdentity("examplecloud_containerette.test", map[string]knownvalue.Check{ + "name": knownvalue.StringExact("banane"), + "resource_group_name": knownvalue.StringExact("foo"), + }), + querycheck.ExpectIdentity("examplecloud_containerette.test", map[string]knownvalue.Check{ + "name": knownvalue.StringExact("ananas"), + "resource_group_name": knownvalue.StringExact("foo"), + }), + querycheck.ExpectIdentity("examplecloud_containerette.test", map[string]knownvalue.Check{ + "name": knownvalue.StringExact("kiwi"), + "resource_group_name": knownvalue.StringExact("foo"), + }), + querycheck.ExpectIdentity("examplecloud_containerette.test2", map[string]knownvalue.Check{ + "name": knownvalue.StringExact("papaya"), + "resource_group_name": knownvalue.StringExact("bar"), + }), + querycheck.ExpectIdentity("examplecloud_containerette.test2", map[string]knownvalue.Check{ + "name": knownvalue.StringExact("birne"), + "resource_group_name": knownvalue.StringExact("bar"), + }), + querycheck.ExpectIdentity("examplecloud_containerette.test2", map[string]knownvalue.Check{ + "name": knownvalue.StringExact("kirsche"), + "resource_group_name": knownvalue.StringExact("bar"), + }), + }, + }, }, }) } @@ -46,18 +97,58 @@ func TestExpectIdentity_NotFound(t *testing.T) { ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ ListResources: map[string]testprovider.ListResource{ - - // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use - - //"examplecloud_containerette": examplecloudListResource(), + "examplecloud_containerette": examplecloudListResource(), }, Resources: map[string]testprovider.Resource{ - //"examplecloud_containerette": examplecloudResource(), + "examplecloud_containerette": examplecloudResource(), }, }), }, Steps: []r.TestStep{ - // TODO + { // config mode step 1 needs tf file with terraform providers block + // this step should provision all the resources that the query is support to list + // for simplicity we're only "provisioning" one here + Config: ` + resource "examplecloud_containerette" "primary" { + name = "banana" + resource_group_name = "foo" + location = "westeurope" + + instances = 5 + }`, + }, + { // Query mode step 2, operates on .tfquery.hcl files (needs tf file with terraform providers block) + // ```provider "examplecloud" {}``` has a slightly different syntax for a .tfquery.hcl file + // provider bock simulates a real providers workflow + // "config" in this case means configuration of the list resource/filters + + Query: true, + Config: ` + provider "examplecloud" {} + + list "examplecloud_containerette" "test" { + provider = examplecloud + + config { + resource_group_name = "foo" + } + } + + list "examplecloud_containerette" "test2" { + provider = examplecloud + + config { + resource_group_name = "bar" + } + } + `, + QueryResultChecks: []querycheck.QueryResultCheck{ + querycheck.ExpectIdentity("examplecloud_containerette.test", map[string]knownvalue.Check{ + "name": knownvalue.StringExact("owo"), + "resource_group_name": knownvalue.StringExact("uwu"), + }), + }, + }, }, }) } diff --git a/querycheck/expect_known_value_test.go b/querycheck/expect_known_value_test.go index d0121e39..dae6ce85 100644 --- a/querycheck/expect_known_value_test.go +++ b/querycheck/expect_known_value_test.go @@ -1,12 +1,17 @@ -package querycheck +package querycheck_test import ( + "math/big" + "testing" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" r "github.com/hashicorp/terraform-plugin-testing/helper/resource" "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/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/querycheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" "github.com/hashicorp/terraform-plugin-testing/tfversion" - "testing" ) func TestExpectKnownValue(t *testing.T) { @@ -19,18 +24,66 @@ func TestExpectKnownValue(t *testing.T) { ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ ListResources: map[string]testprovider.ListResource{ - - // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use - - //"examplecloud_containerette": examplecloudListResource(), + "examplecloud_containerette": examplecloudListResource(), }, Resources: map[string]testprovider.Resource{ - //"examplecloud_containerette": examplecloudResource(), + "examplecloud_containerette": examplecloudResource(), }, }), }, Steps: []r.TestStep{ - // TODO + { // config mode step 1 needs tf file with terraform providers block + // this step should provision all the resources that the query is support to list + // for simplicity we're only "provisioning" one here + Config: ` + resource "examplecloud_containerette" "primary" { + name = "banana" + resource_group_name = "foo" + location = "westeurope" + + instances = 5 + }`, + }, + { // Query mode step 2, operates on .tfquery.hcl files (needs tf file with terraform providers block) + // ```provider "examplecloud" {}``` has a slightly different syntax for a .tfquery.hcl file + // provider bock simulates a real providers workflow + // "config" in this case means configuration of the list resource/filters + + Query: true, + Config: ` + provider "examplecloud" {} + + list "examplecloud_containerette" "test" { + provider = examplecloud + + config { + resource_group_name = "foo" + } + } + + list "examplecloud_containerette" "test2" { + provider = examplecloud + + config { + resource_group_name = "bar" + } + } + `, + QueryResultChecks: []querycheck.QueryResultCheck{ + querycheck.ExpectKnownValue( + "examplecloud_containerette.test", + "banane", + tfjsonpath.New("instances"), + knownvalue.NumberExact(big.NewFloat(5)), + ), + querycheck.ExpectKnownValue( + "examplecloud_containerette.test2", + "papaya", + tfjsonpath.New("instances"), + knownvalue.NumberExact(big.NewFloat(3)), + ), + }, + }, }, }) } @@ -46,18 +99,66 @@ func TestExpectKnownValue_ValueIncorrect(t *testing.T) { ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ ListResources: map[string]testprovider.ListResource{ - - // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use - - //"examplecloud_containerette": examplecloudListResource(), + "examplecloud_containerette": examplecloudListResource(), }, Resources: map[string]testprovider.Resource{ - //"examplecloud_containerette": examplecloudResource(), + "examplecloud_containerette": examplecloudResource(), }, }), }, Steps: []r.TestStep{ - // TODO + { // config mode step 1 needs tf file with terraform providers block + // this step should provision all the resources that the query is support to list + // for simplicity we're only "provisioning" one here + Config: ` + resource "examplecloud_containerette" "primary" { + name = "banana" + resource_group_name = "foo" + location = "westeurope" + + instances = 5 + }`, + }, + { // Query mode step 2, operates on .tfquery.hcl files (needs tf file with terraform providers block) + // ```provider "examplecloud" {}``` has a slightly different syntax for a .tfquery.hcl file + // provider bock simulates a real providers workflow + // "config" in this case means configuration of the list resource/filters + + Query: true, + Config: ` + provider "examplecloud" {} + + list "examplecloud_containerette" "test" { + provider = examplecloud + + config { + resource_group_name = "foo" + } + } + + list "examplecloud_containerette" "test2" { + provider = examplecloud + + config { + resource_group_name = "bar" + } + } + `, + QueryResultChecks: []querycheck.QueryResultCheck{ + querycheck.ExpectKnownValue( + "examplecloud_containerette.test", + "banane", + tfjsonpath.New("instances"), + knownvalue.NumberExact(big.NewFloat(4)), + ), + querycheck.ExpectKnownValue( + "examplecloud_containerette.test2", + "papaya", + tfjsonpath.New("instances"), + knownvalue.NumberExact(big.NewFloat(2)), + ), + }, + }, }, }) } diff --git a/querycheck/expect_result_length_atleast_test.go b/querycheck/expect_result_length_atleast_test.go index afb351bb..2ca6703c 100644 --- a/querycheck/expect_result_length_atleast_test.go +++ b/querycheck/expect_result_length_atleast_test.go @@ -1,12 +1,14 @@ -package querycheck +package querycheck_test import ( + "testing" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" r "github.com/hashicorp/terraform-plugin-testing/helper/resource" "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/querycheck" "github.com/hashicorp/terraform-plugin-testing/tfversion" - "testing" ) func TestResultLengthAtLeast(t *testing.T) { @@ -19,18 +21,38 @@ func TestResultLengthAtLeast(t *testing.T) { ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ ListResources: map[string]testprovider.ListResource{ - - // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use - - //"examplecloud_containerette": examplecloudListResource(), + "examplecloud_containerette": examplecloudListResource(), }, Resources: map[string]testprovider.Resource{ - //"examplecloud_containerette": examplecloudResource(), + "examplecloud_containerette": examplecloudResource(), }, }), }, Steps: []r.TestStep{ - // TODO + { + Query: true, + Config: ` + provider "examplecloud" {} + list "examplecloud_containerette" "test" { + provider = examplecloud + + config { + id = "westeurope/somevalue" + } + } + list "examplecloud_containerette" "test2" { + provider = examplecloud + + config { + id = "foo" + } + } + `, + QueryResultChecks: []querycheck.QueryResultCheck{ + querycheck.ExpectLengthAtLeast("examplecloud_containerette.test", 2), + querycheck.ExpectLengthAtLeast("examplecloud_containerette.test2", 1), + }, + }, }, }) } @@ -46,18 +68,38 @@ func TestResultLengthAtLeast_TooFewResults(t *testing.T) { ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ ListResources: map[string]testprovider.ListResource{ - - // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use - - //"examplecloud_containerette": examplecloudListResource(), + "examplecloud_containerette": examplecloudListResource(), }, Resources: map[string]testprovider.Resource{ - //"examplecloud_containerette": examplecloudResource(), + "examplecloud_containerette": examplecloudResource(), }, }), }, Steps: []r.TestStep{ - // TODO + { + Query: true, + Config: ` + provider "examplecloud" {} + list "examplecloud_containerette" "test" { + provider = examplecloud + + config { + id = "westeurope/somevalue" + } + } + list "examplecloud_containerette" "test2" { + provider = examplecloud + + config { + id = "foo" + } + } + `, + QueryResultChecks: []querycheck.QueryResultCheck{ + querycheck.ExpectLengthAtLeast("examplecloud_containerette.test", 1), + querycheck.ExpectLengthAtLeast("examplecloud_containerette.test2", 0), + }, + }, }, }) } diff --git a/querycheck/expect_result_length_exact_test.go b/querycheck/expect_result_length_exact_test.go index e3346838..efdb97da 100644 --- a/querycheck/expect_result_length_exact_test.go +++ b/querycheck/expect_result_length_exact_test.go @@ -1,12 +1,14 @@ -package querycheck +package querycheck_test import ( + "testing" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" r "github.com/hashicorp/terraform-plugin-testing/helper/resource" "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/querycheck" "github.com/hashicorp/terraform-plugin-testing/tfversion" - "testing" ) func TestResultLengthExact(t *testing.T) { @@ -19,18 +21,38 @@ func TestResultLengthExact(t *testing.T) { ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ ListResources: map[string]testprovider.ListResource{ - - // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use - - //"examplecloud_containerette": examplecloudListResource(), + "examplecloud_containerette": examplecloudListResource(), }, Resources: map[string]testprovider.Resource{ - //"examplecloud_containerette": examplecloudResource(), + "examplecloud_containerette": examplecloudResource(), }, }), }, Steps: []r.TestStep{ - // TODO + { + Query: true, + Config: ` + provider "examplecloud" {} + list "examplecloud_containerette" "test" { + provider = examplecloud + + config { + id = "westeurope/somevalue" + } + } + list "examplecloud_containerette" "test2" { + provider = examplecloud + + config { + id = "foo" + } + } + `, + QueryResultChecks: []querycheck.QueryResultCheck{ + querycheck.ExpectLength("examplecloud_containerette.test", 3), + querycheck.ExpectLength("examplecloud_containerette.test2", 3), + }, + }, }, }) } @@ -46,18 +68,38 @@ func TestResultLengthExact_WrongAmount(t *testing.T) { ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ ListResources: map[string]testprovider.ListResource{ - - // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use - - //"examplecloud_containerette": examplecloudListResource(), + "examplecloud_containerette": examplecloudListResource(), }, Resources: map[string]testprovider.Resource{ - //"examplecloud_containerette": examplecloudResource(), + "examplecloud_containerette": examplecloudResource(), }, }), }, Steps: []r.TestStep{ - // TODO + { + Query: true, + Config: ` + provider "examplecloud" {} + list "examplecloud_containerette" "test" { + provider = examplecloud + + config { + id = "westeurope/somevalue" + } + } + list "examplecloud_containerette" "test2" { + provider = examplecloud + + config { + id = "foo" + } + } + `, + QueryResultChecks: []querycheck.QueryResultCheck{ + querycheck.ExpectLength("examplecloud_containerette.test", 2), + querycheck.ExpectLength("examplecloud_containerette.test2", 1), + }, + }, }, }) } diff --git a/querycheck/types_test.go b/querycheck/types_test.go new file mode 100644 index 00000000..18c8ff50 --- /dev/null +++ b/querycheck/types_test.go @@ -0,0 +1,74 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck_test + +import ( + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func RequiredBoolAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.Bool, + Required: true, + } +} + +func OptionalComputedListAttribute(name string, elementType tftypes.Type) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.List{ElementType: elementType}, + Optional: true, + Computed: true, + } +} + +func RequiredListAttribute(name string, elementType tftypes.Type) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.List{ElementType: elementType}, + Required: true, + } +} + +func RequiredNumberAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.Number, + Required: true, + } +} + +func OptionalNumberAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.Number, + Optional: true, + } +} + +func ComputedStringAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.String, + Computed: true, + } +} + +func OptionalStringAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.String, + Optional: true, + } +} + +func RequiredStringAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.String, + Required: true, + } +} From e7fe718437f35605413ea0b38569adf5b3acd4a4 Mon Sep 17 00:00:00 2001 From: Rain Date: Thu, 16 Oct 2025 17:17:50 -0400 Subject: [PATCH 6/9] Almost passing all tests --- querycheck/contains_name_test.go | 209 +----------------- querycheck/expect_identity_test.go | 2 + querycheck/expect_known_value_test.go | 35 +-- .../expect_result_length_atleast_test.go | 21 +- querycheck/expect_result_length_exact_test.go | 23 +- 5 files changed, 31 insertions(+), 259 deletions(-) diff --git a/querycheck/contains_name_test.go b/querycheck/contains_name_test.go index b8a8a8e7..5c343d11 100644 --- a/querycheck/contains_name_test.go +++ b/querycheck/contains_name_test.go @@ -7,162 +7,13 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-go/tftypes" r "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" - "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/list" "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/internal/teststep" "github.com/hashicorp/terraform-plugin-testing/querycheck" "github.com/hashicorp/terraform-plugin-testing/tfversion" ) -func dessertsThatStartWithPResource() testprovider.Resource { - return testprovider.Resource{ - CreateResponse: &resource.CreateResponse{ - NewState: tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "name": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "name": tftypes.NewValue(tftypes.String, "pie"), - }, - ), - NewIdentity: teststep.Pointer(tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "name": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "name": tftypes.NewValue(tftypes.String, "pie"), - }, - )), - }, - ReadResponse: &resource.ReadResponse{ - NewState: tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "name": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "name": tftypes.NewValue(tftypes.String, "pie"), - }, - ), - NewIdentity: teststep.Pointer(tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "name": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "name": tftypes.NewValue(tftypes.String, "pie"), - }, - )), - }, - ImportStateResponse: &resource.ImportStateResponse{ - State: tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "name": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "name": tftypes.NewValue(tftypes.String, "pie"), - }, - ), - Identity: teststep.Pointer(tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "name": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "name": tftypes.NewValue(tftypes.String, "pie"), - }, - )), - }, - SchemaResponse: &resource.SchemaResponse{ - Schema: &tfprotov6.Schema{ - Block: &tfprotov6.SchemaBlock{ - Attributes: []*tfprotov6.SchemaAttribute{ - { - Name: "name", - Type: tftypes.String, - Required: true, - }, - }, - }, - }, - }, - IdentitySchemaResponse: &resource.IdentitySchemaResponse{ - Schema: &tfprotov6.ResourceIdentitySchema{ - Version: 1, - IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ - { - Name: "name", - Type: tftypes.String, - RequiredForImport: true, - }, - }, - }, - }, - } -} - -func dessertsThatStartWithPListResource() testprovider.ListResource { - return testprovider.ListResource{ - IncludeResource: true, - SchemaResponse: &list.SchemaResponse{ - Schema: &tfprotov6.Schema{ - Block: &tfprotov6.SchemaBlock{ - Attributes: []*tfprotov6.SchemaAttribute{ - { - Name: "group", - Type: tftypes.String, - Computed: true, - }, - }, - }, - }, - }, - ListResultsStream: &list.ListResultsStream{ - Results: func(push func(list.ListResult) bool) { - push(list.ListResult{ - Identity: teststep.Pointer(tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "name": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "name": tftypes.NewValue(tftypes.String, "pie"), - }, - )), - DisplayName: "pie", - }) - push(list.ListResult{ - Identity: teststep.Pointer(tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "name": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "name": tftypes.NewValue(tftypes.String, "pudding"), - }, - )), - DisplayName: "pudding", - }) - }, - }, - } -} - func TestContainsResourceWithName(t *testing.T) { t.Parallel() @@ -173,17 +24,10 @@ func TestContainsResourceWithName(t *testing.T) { ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ ListResources: map[string]testprovider.ListResource{ - // TODO: define a simpler resource and list resource here or copy the `examplecloud_test.go` and `examplecloud_list_resource.go` files here for use "examplecloud_containerette": examplecloudListResource(), }, Resources: map[string]testprovider.Resource{ "examplecloud_containerette": examplecloudResource(), - "dessertcloud": providerserver.NewProviderServer(testprovider.Provider{ - ListResources: map[string]testprovider.ListResource{ - "dessert_letter_p": dessertsThatStartWithPListResource(), - }, - Resources: map[string]testprovider.Resource{ - "dessert_letter_p": dessertsThatStartWithPResource(), }, }), }, @@ -207,24 +51,6 @@ func TestContainsResourceWithName(t *testing.T) { config { resource_group_name = "bar" - { - Query: true, - Config: ` - provider "dessertcloud" {} - - list "dessert_letter_p" "test" { - provider = dessertcloud - - config { - group = "foo" - } - } - - list "dessert_letter_p" "test2" { - provider = dessertcloud - - config { - group = "bar" } } `, @@ -242,14 +68,6 @@ func TestContainsResourceWithName(t *testing.T) { } // Let's add a test case that checks the failure scenario when a resource of a given name is not found. - querycheck.ContainsResourceWithName("dessert_letter_p.test", "pie"), - querycheck.ContainsResourceWithName("dessert_letter_p.test", "pudding"), - }, - }, - }, - }) -} - func TestContainsResourceWithName_NotFound(t *testing.T) { t.Parallel() @@ -264,12 +82,6 @@ func TestContainsResourceWithName_NotFound(t *testing.T) { }, Resources: map[string]testprovider.Resource{ "examplecloud_containerette": examplecloudResource(), - "dessertcloud": providerserver.NewProviderServer(testprovider.Provider{ - ListResources: map[string]testprovider.ListResource{ - "dessert_letter_p": dessertsThatStartWithPListResource(), - }, - Resources: map[string]testprovider.Resource{ - "dessert_letter_p": dessertsThatStartWithPResource(), }, }), }, @@ -292,32 +104,13 @@ func TestContainsResourceWithName_NotFound(t *testing.T) { config { resource_group_name = "bar" - provider "dessertcloud" {} - - list "dessert_letter_p" "test" { - provider = dessertcloud - - config { - group = "foo" - } - } - - list "dessert_letter_p" "test2" { - provider = dessertcloud - - config { - group = "bar" } } `, QueryResultChecks: []querycheck.QueryResultCheck{ querycheck.ContainsResourceWithName("examplecloud_containerette.test", "pflaume"), }, - // TODO update expected error message to match what we output - ExpectError: regexp.MustCompile("examplecloud_containerette.test - there are no pflaumen here!"), - querycheck.ContainsResourceWithName("dessert_letter_p.test", "pavlova"), - }, - ExpectError: regexp.MustCompile("expected to find resource with display name \"pavlova\" in results but resource was not found"), + ExpectError: regexp.MustCompile("expected to find resource with display name \"pflaume\" in results but resource was not found"), }, }, }) diff --git a/querycheck/expect_identity_test.go b/querycheck/expect_identity_test.go index 50aeed5a..7ce0debe 100644 --- a/querycheck/expect_identity_test.go +++ b/querycheck/expect_identity_test.go @@ -1,6 +1,7 @@ package querycheck_test import ( + "regexp" "testing" "github.com/hashicorp/terraform-plugin-go/tfprotov6" @@ -148,6 +149,7 @@ func TestExpectIdentity_NotFound(t *testing.T) { "resource_group_name": knownvalue.StringExact("uwu"), }), }, + ExpectError: regexp.MustCompile("an identity with the following attributes was not found\nattribute \"name\": owo\nattribute \"resource_group_name\": uwu\naddress: examplecloud_containerette.test"), }, }, }) diff --git a/querycheck/expect_known_value_test.go b/querycheck/expect_known_value_test.go index dae6ce85..7cc6d718 100644 --- a/querycheck/expect_known_value_test.go +++ b/querycheck/expect_known_value_test.go @@ -2,6 +2,7 @@ package querycheck_test import ( "math/big" + "regexp" "testing" "github.com/hashicorp/terraform-plugin-go/tfprotov6" @@ -60,29 +61,16 @@ func TestExpectKnownValue(t *testing.T) { resource_group_name = "foo" } } - - list "examplecloud_containerette" "test2" { - provider = examplecloud - - config { - resource_group_name = "bar" - } - } `, QueryResultChecks: []querycheck.QueryResultCheck{ querycheck.ExpectKnownValue( "examplecloud_containerette.test", - "banane", + "banana", tfjsonpath.New("instances"), knownvalue.NumberExact(big.NewFloat(5)), ), - querycheck.ExpectKnownValue( - "examplecloud_containerette.test2", - "papaya", - tfjsonpath.New("instances"), - knownvalue.NumberExact(big.NewFloat(3)), - ), }, + ExpectError: regexp.MustCompile("examplecloud_containerette.test - the resource banana was not found"), }, }, }) @@ -135,29 +123,16 @@ func TestExpectKnownValue_ValueIncorrect(t *testing.T) { resource_group_name = "foo" } } - - list "examplecloud_containerette" "test2" { - provider = examplecloud - - config { - resource_group_name = "bar" - } - } `, QueryResultChecks: []querycheck.QueryResultCheck{ querycheck.ExpectKnownValue( "examplecloud_containerette.test", - "banane", + "banana", tfjsonpath.New("instances"), knownvalue.NumberExact(big.NewFloat(4)), ), - querycheck.ExpectKnownValue( - "examplecloud_containerette.test2", - "papaya", - tfjsonpath.New("instances"), - knownvalue.NumberExact(big.NewFloat(2)), - ), }, + ExpectError: regexp.MustCompile("examplecloud_containerette.test - the resource banana was not found"), }, }, }) diff --git a/querycheck/expect_result_length_atleast_test.go b/querycheck/expect_result_length_atleast_test.go index 2ca6703c..912b659b 100644 --- a/querycheck/expect_result_length_atleast_test.go +++ b/querycheck/expect_result_length_atleast_test.go @@ -1,6 +1,7 @@ package querycheck_test import ( + "regexp" "testing" "github.com/hashicorp/terraform-plugin-go/tfprotov6" @@ -37,15 +38,15 @@ func TestResultLengthAtLeast(t *testing.T) { provider = examplecloud config { - id = "westeurope/somevalue" - } + resource_group_name = "foo" + } } list "examplecloud_containerette" "test2" { provider = examplecloud config { - id = "foo" - } + resource_group_name = "bar" + } } `, QueryResultChecks: []querycheck.QueryResultCheck{ @@ -84,21 +85,21 @@ func TestResultLengthAtLeast_TooFewResults(t *testing.T) { provider = examplecloud config { - id = "westeurope/somevalue" - } + resource_group_name = "foo" + } } list "examplecloud_containerette" "test2" { provider = examplecloud config { - id = "foo" - } + resource_group_name = "bar" + } } `, QueryResultChecks: []querycheck.QueryResultCheck{ - querycheck.ExpectLengthAtLeast("examplecloud_containerette.test", 1), - querycheck.ExpectLengthAtLeast("examplecloud_containerette.test2", 0), + querycheck.ExpectLengthAtLeast("examplecloud_containerette.test", 8), }, + ExpectError: regexp.MustCompile("Query result of at least length 8 - expected but got 6."), }, }, }) diff --git a/querycheck/expect_result_length_exact_test.go b/querycheck/expect_result_length_exact_test.go index efdb97da..07e44a7e 100644 --- a/querycheck/expect_result_length_exact_test.go +++ b/querycheck/expect_result_length_exact_test.go @@ -1,6 +1,7 @@ package querycheck_test import ( + "regexp" "testing" "github.com/hashicorp/terraform-plugin-go/tfprotov6" @@ -37,20 +38,20 @@ func TestResultLengthExact(t *testing.T) { provider = examplecloud config { - id = "westeurope/somevalue" - } + resource_group_name = "foo" + } } list "examplecloud_containerette" "test2" { provider = examplecloud config { - id = "foo" - } + resource_group_name = "bar" + } } `, QueryResultChecks: []querycheck.QueryResultCheck{ - querycheck.ExpectLength("examplecloud_containerette.test", 3), - querycheck.ExpectLength("examplecloud_containerette.test2", 3), + querycheck.ExpectLength("examplecloud_containerette.test", 6), + querycheck.ExpectLength("examplecloud_containerette.test2", 6), }, }, }, @@ -84,21 +85,21 @@ func TestResultLengthExact_WrongAmount(t *testing.T) { provider = examplecloud config { - id = "westeurope/somevalue" - } + resource_group_name = "foo" + } } list "examplecloud_containerette" "test2" { provider = examplecloud config { - id = "foo" - } + resource_group_name = "bar" + } } `, QueryResultChecks: []querycheck.QueryResultCheck{ querycheck.ExpectLength("examplecloud_containerette.test", 2), - querycheck.ExpectLength("examplecloud_containerette.test2", 1), }, + ExpectError: regexp.MustCompile("number of found resources 2 - expected but got 6."), }, }, }) From 9af5b5bbe58b4ea6d6314cd23ca3584fb5e0d021 Mon Sep 17 00:00:00 2001 From: Rain Date: Thu, 16 Oct 2025 18:09:41 -0400 Subject: [PATCH 7/9] Added missing copywrite --- querycheck/expect_identity_test.go | 3 +++ querycheck/expect_known_value_test.go | 3 +++ querycheck/expect_result_length_atleast_test.go | 3 +++ querycheck/expect_result_length_exact_test.go | 3 +++ 4 files changed, 12 insertions(+) diff --git a/querycheck/expect_identity_test.go b/querycheck/expect_identity_test.go index 7ce0debe..e3203550 100644 --- a/querycheck/expect_identity_test.go +++ b/querycheck/expect_identity_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package querycheck_test import ( diff --git a/querycheck/expect_known_value_test.go b/querycheck/expect_known_value_test.go index 7cc6d718..f804aecb 100644 --- a/querycheck/expect_known_value_test.go +++ b/querycheck/expect_known_value_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package querycheck_test import ( diff --git a/querycheck/expect_result_length_atleast_test.go b/querycheck/expect_result_length_atleast_test.go index 912b659b..9e6edf67 100644 --- a/querycheck/expect_result_length_atleast_test.go +++ b/querycheck/expect_result_length_atleast_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package querycheck_test import ( diff --git a/querycheck/expect_result_length_exact_test.go b/querycheck/expect_result_length_exact_test.go index 07e44a7e..f5a0e90e 100644 --- a/querycheck/expect_result_length_exact_test.go +++ b/querycheck/expect_result_length_exact_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package querycheck_test import ( From 25cbb89a50dd0213d9fb144912e96cda2a64a49f Mon Sep 17 00:00:00 2001 From: Steph Date: Fri, 17 Oct 2025 09:35:08 +0200 Subject: [PATCH 8/9] remove early returns and retrieve resource schema in the providerserver --- .../testsdk/providerserver/providerserver.go | 26 ++++++++++++------- querycheck/expect_known_value_test.go | 9 ++++--- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/internal/testing/testsdk/providerserver/providerserver.go b/internal/testing/testsdk/providerserver/providerserver.go index 0f14d0da..72be6cbe 100644 --- a/internal/testing/testsdk/providerserver/providerserver.go +++ b/internal/testing/testsdk/providerserver/providerserver.go @@ -1062,22 +1062,28 @@ func (s ProviderServer) ListResource(ctx context.Context, req *tfprotov6.ListRes return nil, fmt.Errorf("failed to retrieve resource identity schema: %v", err) } - schemaReq := list.SchemaRequest{} - schemaResp := &list.SchemaResponse{} + configSchemaReq := list.SchemaRequest{} + configSchemaResp := &list.SchemaResponse{} - listresource.Schema(ctx, schemaReq, schemaResp) - if len(schemaResp.Diagnostics) > 0 { - return nil, fmt.Errorf("failed to retrieve resource schema: %v", schemaResp.Diagnostics) + listresource.Schema(ctx, configSchemaReq, configSchemaResp) + if len(configSchemaResp.Diagnostics) > 0 { + return nil, fmt.Errorf("failed to retrieve resource schema: %v", configSchemaResp.Diagnostics) + } + + resourceSchemaResp := &resource.SchemaResponse{} + r.Schema(ctx, resource.SchemaRequest{}, resourceSchemaResp) + if resourceSchemaResp.Schema == nil { + return nil, fmt.Errorf("failed to retrieve resource schema: %v", resourceSchemaResp.Schema) } listReq := list.ListRequest{ TypeName: req.TypeName, IncludeResource: req.IncludeResource, Limit: req.Limit, - ResourceSchema: schemaResp.Schema, + ResourceSchema: resourceSchemaResp.Schema, } - listReq.Config, diag = DynamicValueToValue(schemaResp.Schema, req.Config) + listReq.Config, diag = DynamicValueToValue(configSchemaResp.Schema, req.Config) if diag != nil { return nil, fmt.Errorf("failed to convert config to value: %v", err) } @@ -1157,8 +1163,9 @@ func processListResult(req list.ListRequest, result list.ListResult) tfprotov6.L } listResourceResult.Resource, diag = ValuetoDynamicValue(req.ResourceSchema, *result.Resource) - listResourceResult.Diagnostics = append(listResourceResult.Diagnostics, diag) - return listResourceResult + if diag != nil { + listResourceResult.Diagnostics = append(listResourceResult.Diagnostics, diag) + } } listResourceResult.Identity = &tfprotov6.ResourceIdentityData{} @@ -1167,7 +1174,6 @@ func processListResult(req list.ListRequest, result list.ListResult) tfprotov6.L listResourceResult.Identity.IdentityData, diag = IdentityValuetoDynamicValue(req.ResourceIdentitySchema, *result.Identity) if diag != nil { listResourceResult.Diagnostics = append(listResourceResult.Diagnostics, diag) - return listResourceResult } } diff --git a/querycheck/expect_known_value_test.go b/querycheck/expect_known_value_test.go index f804aecb..e453088f 100644 --- a/querycheck/expect_known_value_test.go +++ b/querycheck/expect_known_value_test.go @@ -58,7 +58,8 @@ func TestExpectKnownValue(t *testing.T) { provider "examplecloud" {} list "examplecloud_containerette" "test" { - provider = examplecloud + provider = examplecloud + include_resource = true config { resource_group_name = "foo" @@ -68,12 +69,11 @@ func TestExpectKnownValue(t *testing.T) { QueryResultChecks: []querycheck.QueryResultCheck{ querycheck.ExpectKnownValue( "examplecloud_containerette.test", - "banana", + "banane", tfjsonpath.New("instances"), knownvalue.NumberExact(big.NewFloat(5)), ), }, - ExpectError: regexp.MustCompile("examplecloud_containerette.test - the resource banana was not found"), }, }, }) @@ -120,7 +120,8 @@ func TestExpectKnownValue_ValueIncorrect(t *testing.T) { provider "examplecloud" {} list "examplecloud_containerette" "test" { - provider = examplecloud + provider = examplecloud + include_resource = true config { resource_group_name = "foo" From 93f8293c1f089cc3096a5cc7b1fd4124676606e6 Mon Sep 17 00:00:00 2001 From: Rain Date: Fri, 17 Oct 2025 08:21:02 -0400 Subject: [PATCH 9/9] Addressing PR comments --- helper/resource/query/query_test.go | 50 ------------------- querycheck/contains_name_test.go | 26 +++++++++- querycheck/expect_identity_test.go | 13 ++++- querycheck/expect_known_value_test.go | 5 +- .../expect_result_length_atleast_test.go | 25 +++++++++- querycheck/expect_result_length_exact_test.go | 25 +++++++++- 6 files changed, 86 insertions(+), 58 deletions(-) diff --git a/helper/resource/query/query_test.go b/helper/resource/query/query_test.go index 35549fd4..26ac069f 100644 --- a/helper/resource/query/query_test.go +++ b/helper/resource/query/query_test.go @@ -97,56 +97,6 @@ func TestQuery(t *testing.T) { }), }, }, - - // Commented out since these will fail now - //{ - // Query: true, - // Config: ` - // provider "examplecloud" {} - // list "examplecloud_containerette" "test" { - // provider = examplecloud - // - // config { - // id = "westeurope/somevalue" - // } - // } - // list "examplecloud_containerette" "test2" { - // provider = examplecloud - // - // config { - // id = "foo" - // } - // } - // `, - // QueryResultChecks: []querycheck.QueryResultCheck{ - // querycheck.ExpectLength("examplecloud_containerette.test", 3), - // querycheck.ExpectLength("examplecloud_containerette.test2", 3), - // }, - //}, - //{ - // Query: true, - // Config: ` - // provider "examplecloud" {} - // list "examplecloud_containerette" "test" { - // provider = examplecloud - // - // config { - // id = "westeurope/somevalue" - // } - // } - // list "examplecloud_containerette" "test2" { - // provider = examplecloud - // - // config { - // id = "foo" - // } - // } - // `, - // QueryResultChecks: []querycheck.QueryResultCheck{ - // querycheck.ExpectLengthAtLeast("examplecloud_containerette.test", 2), - // querycheck.ExpectLengthAtLeast("examplecloud_containerette.test2", 1), - // }, - //}, }, }) } diff --git a/querycheck/contains_name_test.go b/querycheck/contains_name_test.go index 5c343d11..8ae703b1 100644 --- a/querycheck/contains_name_test.go +++ b/querycheck/contains_name_test.go @@ -32,7 +32,18 @@ func TestContainsResourceWithName(t *testing.T) { }), }, Steps: []r.TestStep{ - // We'll skip the first test step where we simulate creating the resource that will be returned when we query for it for simplicity. + { // config mode step 1 needs tf file with terraform providers block + // this step should provision all the resources that the query is support to list + // for simplicity we're only "provisioning" one here + Config: ` + resource "examplecloud_containerette" "primary" { + name = "banana" + resource_group_name = "foo" + location = "westeurope" + + instances = 5 + }`, + }, { Query: true, Config: ` @@ -67,7 +78,6 @@ func TestContainsResourceWithName(t *testing.T) { }) } -// Let's add a test case that checks the failure scenario when a resource of a given name is not found. func TestContainsResourceWithName_NotFound(t *testing.T) { t.Parallel() @@ -86,6 +96,18 @@ func TestContainsResourceWithName_NotFound(t *testing.T) { }), }, Steps: []r.TestStep{ + { // config mode step 1 needs tf file with terraform providers block + // this step should provision all the resources that the query is support to list + // for simplicity we're only "provisioning" one here + Config: ` + resource "examplecloud_containerette" "primary" { + name = "banana" + resource_group_name = "foo" + location = "westeurope" + + instances = 5 + }`, + }, { Query: true, Config: ` diff --git a/querycheck/expect_identity_test.go b/querycheck/expect_identity_test.go index e3203550..64a3d7d6 100644 --- a/querycheck/expect_identity_test.go +++ b/querycheck/expect_identity_test.go @@ -34,6 +34,18 @@ func TestExpectIdentity(t *testing.T) { }), }, Steps: []r.TestStep{ + { // config mode step 1 needs tf file with terraform providers block + // this step should provision all the resources that the query is support to list + // for simplicity we're only "provisioning" one here + Config: ` + resource "examplecloud_containerette" "primary" { + name = "banana" + resource_group_name = "foo" + location = "westeurope" + + instances = 5 + }`, + }, { // Query mode step 2, operates on .tfquery.hcl files (needs tf file with terraform providers block) // ```provider "examplecloud" {}``` has a slightly different syntax for a .tfquery.hcl file // provider bock simulates a real providers workflow @@ -90,7 +102,6 @@ func TestExpectIdentity(t *testing.T) { }) } -// Let's add a test case that checks the failure scenario when an identity is not found. func TestExpectIdentity_NotFound(t *testing.T) { t.Parallel() diff --git a/querycheck/expect_known_value_test.go b/querycheck/expect_known_value_test.go index e453088f..a12ab688 100644 --- a/querycheck/expect_known_value_test.go +++ b/querycheck/expect_known_value_test.go @@ -79,7 +79,6 @@ func TestExpectKnownValue(t *testing.T) { }) } -// Let's add a test case that checks the failure scenario when the value is incorrect. func TestExpectKnownValue_ValueIncorrect(t *testing.T) { t.Parallel() @@ -131,12 +130,12 @@ func TestExpectKnownValue_ValueIncorrect(t *testing.T) { QueryResultChecks: []querycheck.QueryResultCheck{ querycheck.ExpectKnownValue( "examplecloud_containerette.test", - "banana", + "banane", tfjsonpath.New("instances"), knownvalue.NumberExact(big.NewFloat(4)), ), }, - ExpectError: regexp.MustCompile("examplecloud_containerette.test - the resource banana was not found"), + ExpectError: regexp.MustCompile("the following errors were found while checking values: error checking value for attribute at path: instances for resource banane, err: expected value 4 for NumberExact check, got: 5;"), }, }, }) diff --git a/querycheck/expect_result_length_atleast_test.go b/querycheck/expect_result_length_atleast_test.go index 9e6edf67..4ddb5267 100644 --- a/querycheck/expect_result_length_atleast_test.go +++ b/querycheck/expect_result_length_atleast_test.go @@ -33,6 +33,18 @@ func TestResultLengthAtLeast(t *testing.T) { }), }, Steps: []r.TestStep{ + { // config mode step 1 needs tf file with terraform providers block + // this step should provision all the resources that the query is support to list + // for simplicity we're only "provisioning" one here + Config: ` + resource "examplecloud_containerette" "primary" { + name = "banana" + resource_group_name = "foo" + location = "westeurope" + + instances = 5 + }`, + }, { Query: true, Config: ` @@ -61,7 +73,6 @@ func TestResultLengthAtLeast(t *testing.T) { }) } -// Let's add a test case that checks the failure scenario when there are too few results. func TestResultLengthAtLeast_TooFewResults(t *testing.T) { t.Parallel() @@ -80,6 +91,18 @@ func TestResultLengthAtLeast_TooFewResults(t *testing.T) { }), }, Steps: []r.TestStep{ + { // config mode step 1 needs tf file with terraform providers block + // this step should provision all the resources that the query is support to list + // for simplicity we're only "provisioning" one here + Config: ` + resource "examplecloud_containerette" "primary" { + name = "banana" + resource_group_name = "foo" + location = "westeurope" + + instances = 5 + }`, + }, { Query: true, Config: ` diff --git a/querycheck/expect_result_length_exact_test.go b/querycheck/expect_result_length_exact_test.go index f5a0e90e..a69b959d 100644 --- a/querycheck/expect_result_length_exact_test.go +++ b/querycheck/expect_result_length_exact_test.go @@ -33,6 +33,18 @@ func TestResultLengthExact(t *testing.T) { }), }, Steps: []r.TestStep{ + { // config mode step 1 needs tf file with terraform providers block + // this step should provision all the resources that the query is support to list + // for simplicity we're only "provisioning" one here + Config: ` + resource "examplecloud_containerette" "primary" { + name = "banana" + resource_group_name = "foo" + location = "westeurope" + + instances = 5 + }`, + }, { Query: true, Config: ` @@ -61,7 +73,6 @@ func TestResultLengthExact(t *testing.T) { }) } -// Let's add a test case that checks the failure scenario when there are the wrong amount of results. func TestResultLengthExact_WrongAmount(t *testing.T) { t.Parallel() @@ -80,6 +91,18 @@ func TestResultLengthExact_WrongAmount(t *testing.T) { }), }, Steps: []r.TestStep{ + { // config mode step 1 needs tf file with terraform providers block + // this step should provision all the resources that the query is support to list + // for simplicity we're only "provisioning" one here + Config: ` + resource "examplecloud_containerette" "primary" { + name = "banana" + resource_group_name = "foo" + location = "westeurope" + + instances = 5 + }`, + }, { Query: true, Config: `