From 1bc7abd7fc3e2c0462eb3d82a395d5e9618ff14d Mon Sep 17 00:00:00 2001 From: Steph Date: Fri, 10 Oct 2025 14:29:54 +0200 Subject: [PATCH 1/2] add support for expect error in query mode --- helper/resource/testing_new.go | 31 ++- querycheck/{contains.go => contains_name.go} | 0 querycheck/contains_name_test.go | 258 +++++++++++++++++++ 3 files changed, 286 insertions(+), 3 deletions(-) rename querycheck/{contains.go => contains_name.go} (100%) create mode 100644 querycheck/contains_name_test.go diff --git a/helper/resource/testing_new.go b/helper/resource/testing_new.go index c94a97c0..84a43673 100644 --- a/helper/resource/testing_new.go +++ b/helper/resource/testing_new.go @@ -389,11 +389,36 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest } err = query.RunQueryChecks(ctx, t, queryOut, step.QueryResultChecks) - if err != nil { - t.Fatalf("Step %d/%d error running query checks: %s", stepNumber, len(c.Steps), err) + + if step.ExpectError != nil { + logging.HelperResourceDebug(ctx, "Checking TestStep ExpectError") + if err == nil { + logging.HelperResourceError(ctx, "Error running query: expected an error but got none") + t.Fatalf("Step %d/%d error running query: expected an error but got none", stepNumber, len(c.Steps)) + } + if !step.ExpectError.MatchString(err.Error()) { + logging.HelperResourceError(ctx, fmt.Sprintf("Error running query: expected an error with pattern (%s)", step.ExpectError.String()), + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("Step %d/%d error running query, 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 query", + map[string]interface{}{logging.KeyError: err}, + ) + 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) } - fmt.Printf("Step %d/%d Query Output:\n%s\n", stepNumber, len(c.Steps), queryOut) + logging.HelperResourceDebug(ctx, "Finished TestStep") + continue } 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..4388f504 --- /dev/null +++ b/querycheck/contains_name_test.go @@ -0,0 +1,258 @@ +package querycheck_test + +import ( + "regexp" + "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() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "dessertcloud": providerserver.NewProviderServer(testprovider.Provider{ + ListResources: map[string]testprovider.ListResource{ + "dessert_letter_p": dessertsThatStartWithPListResource(), + }, + Resources: map[string]testprovider.Resource{ + "dessert_letter_p": dessertsThatStartWithPResource(), + }, + }), + }, + Steps: []r.TestStep{ + { + 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" + } + } + `, + QueryResultChecks: []querycheck.QueryResultCheck{ + querycheck.ContainsResourceWithName("dessert_letter_p.test", "pie"), + querycheck.ContainsResourceWithName("dessert_letter_p.test", "pudding"), + }, + }, + }, + }) +} + +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){ + "dessertcloud": providerserver.NewProviderServer(testprovider.Provider{ + ListResources: map[string]testprovider.ListResource{ + "dessert_letter_p": dessertsThatStartWithPListResource(), + }, + Resources: map[string]testprovider.Resource{ + "dessert_letter_p": dessertsThatStartWithPResource(), + }, + }), + }, + Steps: []r.TestStep{ + { + 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" + } + } + `, + QueryResultChecks: []querycheck.QueryResultCheck{ + 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"), + }, + }, + }) +} From cfc4ae0716e194183cb96cc08414393f092f3045 Mon Sep 17 00:00:00 2001 From: Steph Date: Fri, 10 Oct 2025 14:36:14 +0200 Subject: [PATCH 2/2] add header --- querycheck/contains_name_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/querycheck/contains_name_test.go b/querycheck/contains_name_test.go index 4388f504..ba8e2f2d 100644 --- a/querycheck/contains_name_test.go +++ b/querycheck/contains_name_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: MPL-2.0 + package querycheck_test import (