From d758fd83561bb1bc57fe1d2aa89aacc784bc9bfc Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Fri, 7 Nov 2025 16:26:33 -0500 Subject: [PATCH 01/15] Add `QueryFilter` interface and first implementations --- querycheck/queryfilter/filter.go | 30 +++++++++++ .../filter_by_display_name_exact.go | 21 ++++++++ .../filter_by_resource_identity.go | 51 +++++++++++++++++++ 3 files changed, 102 insertions(+) create mode 100644 querycheck/queryfilter/filter.go create mode 100644 querycheck/queryfilter/filter_by_display_name_exact.go create mode 100644 querycheck/queryfilter/filter_by_resource_identity.go diff --git a/querycheck/queryfilter/filter.go b/querycheck/queryfilter/filter.go new file mode 100644 index 00000000..2516430e --- /dev/null +++ b/querycheck/queryfilter/filter.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package queryfilter + +import ( + "context" + + tfjson "github.com/hashicorp/terraform-json" +) + +type QueryFilter interface { + Filter(context.Context, FilterQueryRequest, *FilterQueryResponse) +} + +// FilterQueryRequest is a request to a filter function. +type FilterQueryRequest struct { + // Query represents the parsed log messages relating to found resources returned by the `terraform query -json` command. + QueryItem tfjson.ListResourceFoundData +} + +// FilterQueryResponse is a response to a filter function. +type FilterQueryResponse struct { + // Include indicates whether the QueryItem should be included in CheckQueryRequest.Query + Include bool + + // Error is used to report the failure of filtering and is combined with other QueryFilter errors + // to be reported as a test failure. + Error error +} diff --git a/querycheck/queryfilter/filter_by_display_name_exact.go b/querycheck/queryfilter/filter_by_display_name_exact.go new file mode 100644 index 00000000..f41c3791 --- /dev/null +++ b/querycheck/queryfilter/filter_by_display_name_exact.go @@ -0,0 +1,21 @@ +package queryfilter + +import ( + "context" +) + +type filterByDisplayNameExact struct { + displayName string +} + +func (f filterByDisplayNameExact) Filter(ctx context.Context, req FilterQueryRequest, resp *FilterQueryResponse) { + if req.QueryItem.DisplayName == f.displayName { + resp.Include = true + } +} + +func ByDisplayNameExact(displayName string) QueryFilter { + return filterByDisplayNameExact{ + displayName: displayName, + } +} diff --git a/querycheck/queryfilter/filter_by_resource_identity.go b/querycheck/queryfilter/filter_by_resource_identity.go new file mode 100644 index 00000000..bf487634 --- /dev/null +++ b/querycheck/queryfilter/filter_by_resource_identity.go @@ -0,0 +1,51 @@ +package queryfilter + +import ( + "context" + "sort" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +type filterByResourceIdentity struct { + identity map[string]knownvalue.Check +} + +func (f filterByResourceIdentity) Filter(ctx context.Context, req FilterQueryRequest, resp *FilterQueryResponse) { + if len(req.QueryItem.Identity) != len(f.identity) { + resp.Include = false + return + } + + var keys []string + + for k := range f.identity { + keys = append(keys, k) + } + + sort.SliceStable(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + for _, k := range keys { + actualIdentityVal, ok := req.QueryItem.Identity[k] + + if !ok { + resp.Include = false + return + } + + if err := f.identity[k].CheckValue(actualIdentityVal); err != nil { + resp.Include = false + return + } + } + + resp.Include = true +} + +func ByResourceIdentity(identity map[string]knownvalue.Check) QueryFilter { + return filterByResourceIdentity{ + identity: identity, + } +} From 8615363cc31c961907ab7fe91491c3b8fe08bff8 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Fri, 7 Nov 2025 16:27:43 -0500 Subject: [PATCH 02/15] Add `ExpectResourceDisplayName` query check --- helper/resource/query/query_checks.go | 37 +++++++++ querycheck/expect_resource_display_name.go | 65 +++++++++++++++ .../expect_resource_display_name_test.go | 80 +++++++++++++++++++ querycheck/query_check.go | 10 +++ 4 files changed, 192 insertions(+) create mode 100644 querycheck/expect_resource_display_name.go create mode 100644 querycheck/expect_resource_display_name_test.go diff --git a/helper/resource/query/query_checks.go b/helper/resource/query/query_checks.go index 8ef9a3e0..6414a31e 100644 --- a/helper/resource/query/query_checks.go +++ b/helper/resource/query/query_checks.go @@ -12,6 +12,7 @@ import ( "github.com/mitchellh/go-testing-interface" "github.com/hashicorp/terraform-plugin-testing/querycheck" + "github.com/hashicorp/terraform-plugin-testing/querycheck/queryfilter" ) func RunQueryChecks(ctx context.Context, t testing.T, query []tfjson.LogMsg, queryChecks []querycheck.QueryResultCheck) error { @@ -39,6 +40,13 @@ func RunQueryChecks(ctx context.Context, t testing.T, query []tfjson.LogMsg, que } for _, queryCheck := range queryChecks { + if filterCheck, ok := queryCheck.(querycheck.QueryResultCheckWithFilters); ok { + var err error + found, err = runQueryFilters(ctx, filterCheck, found) + if err != nil { + return err + } + } resp := querycheck.CheckQueryResponse{} queryCheck.CheckQuery(ctx, querycheck.CheckQueryRequest{ Query: found, @@ -50,3 +58,32 @@ func RunQueryChecks(ctx context.Context, t testing.T, query []tfjson.LogMsg, que return errors.Join(result...) } + +func runQueryFilters(ctx context.Context, filterCheck querycheck.QueryResultCheckWithFilters, queryResults []tfjson.ListResourceFoundData) ([]tfjson.ListResourceFoundData, error) { + filters := filterCheck.QueryFilters(ctx) + filteredResults := make([]tfjson.ListResourceFoundData, 0) + + for _, result := range queryResults { + keepResult := false + + for _, filter := range filters { + + resp := queryfilter.FilterQueryResponse{} + filter.Filter(ctx, queryfilter.FilterQueryRequest{QueryItem: result}, &resp) + + if resp.Include { + keepResult = true + } + + if resp.Error != nil { + return nil, resp.Error + } + } + + if keepResult { + filteredResults = append(filteredResults, result) + } + } + + return filteredResults, nil +} diff --git a/querycheck/expect_resource_display_name.go b/querycheck/expect_resource_display_name.go new file mode 100644 index 00000000..8af51345 --- /dev/null +++ b/querycheck/expect_resource_display_name.go @@ -0,0 +1,65 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "fmt" + "strings" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/querycheck/queryfilter" +) + +var _ QueryResultCheck = expectResourceDisplayName{} +var _ QueryResultCheckWithFilters = expectResourceDisplayName{} + +type expectResourceDisplayName struct { + listResourceAddress string + filter queryfilter.QueryFilter + displayName string +} + +func (e expectResourceDisplayName) QueryFilters(ctx context.Context) []queryfilter.QueryFilter { + return []queryfilter.QueryFilter{ + e.filter, + } +} + +func (e expectResourceDisplayName) CheckQuery(_ context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { + listRes := make([]tfjson.ListResourceFoundData, 0) + for _, result := range req.Query { + if strings.TrimPrefix(result.Address, "list.") == e.listResourceAddress { + listRes = append(listRes, result) + } + } + + if len(listRes) == 0 { + resp.Error = fmt.Errorf("%s - no query results found after filtering", e.listResourceAddress) + return + } + + if len(listRes) > 1 { + resp.Error = fmt.Errorf("%s - more than 1 query result found after filtering", e.listResourceAddress) + return + } + res := listRes[0] + if strings.EqualFold(e.displayName, res.DisplayName) { + return + } + + resp.Error = fmt.Errorf("expected to find resource with display name %q in results but resource was not found", e.displayName) +} + +// ExpectResourceDisplayName returns a query check that asserts that a resource with a given display name exists within the returned results of the query. +// +// This query check can only be used with managed resources that support query. Query is only supported in Terraform v1.14+ +func ExpectResourceDisplayName(listResourceAddress string, filter queryfilter.QueryFilter, displayName string) QueryResultCheck { + return expectResourceDisplayName{ + listResourceAddress: listResourceAddress, + filter: filter, + displayName: displayName, + } +} diff --git a/querycheck/expect_resource_display_name_test.go b/querycheck/expect_resource_display_name_test.go new file mode 100644 index 00000000..3a76444d --- /dev/null +++ b/querycheck/expect_resource_display_name_test.go @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MPL-2.0 + +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/querycheck/queryfilter" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestExpectResourceDisplayName(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{ + "examplecloud_containerette": examplecloudListResource(), + }, + Resources: map[string]testprovider.Resource{ + "examplecloud_containerette": examplecloudResource(), + }, + }), + }, + 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: ` + 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.ExpectResourceDisplayName("examplecloud_containerette.test", queryfilter.ByDisplayNameExact("ananas"), "ananas"), + querycheck.ExpectResourceDisplayName("examplecloud_containerette.test", queryfilter.ByResourceIdentity(map[string]knownvalue.Check{ + "name": knownvalue.StringExact("ananas"), + "resource_group_name": knownvalue.StringExact("foo"), + }), "ananas"), + }, + }, + }, + }) +} diff --git a/querycheck/query_check.go b/querycheck/query_check.go index 66d32628..9728f11d 100644 --- a/querycheck/query_check.go +++ b/querycheck/query_check.go @@ -7,6 +7,8 @@ import ( "context" tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/querycheck/queryfilter" ) // QueryResultCheck defines an interface for implementing test logic to apply an assertion against a collection of found @@ -16,6 +18,14 @@ type QueryResultCheck interface { CheckQuery(context.Context, CheckQueryRequest, *CheckQueryResponse) } +// QueryResultCheckWithFilters is an interface type that extends QueryResultCheck to include declarative query filters. +type QueryResultCheckWithFilters interface { + QueryResultCheck + + // QueryFilters should return a slice of queryfilter.QueryFilter that will be applied to the check. + QueryFilters(context.Context) []queryfilter.QueryFilter +} + // CheckQueryRequest is a request for an invoke of the CheckQuery function. type CheckQueryRequest struct { // Query represents the parsed log messages relating to found resources returned by the `terraform query -json` command. From fc3e66d8a85a49c4da7801594698aab79e1cea6e Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 11 Nov 2025 14:54:09 -0500 Subject: [PATCH 03/15] Add more filters and tests --- helper/resource/query/query_checks.go | 5 + querycheck/expect_resource_display_name.go | 4 + .../expect_resource_display_name_test.go | 163 ++++++++++++++++- querycheck/queryfilter/filter.go | 2 + .../filter_by_display_name_exact.go | 2 + .../filter_by_display_name_exact_test.go | 71 ++++++++ .../filter_by_display_name_regexp.go | 24 +++ .../filter_by_display_name_regexp_test.go | 72 ++++++++ .../filter_by_resource_identity.go | 5 + .../filter_by_resource_identity_test.go | 165 ++++++++++++++++++ 10 files changed, 508 insertions(+), 5 deletions(-) create mode 100644 querycheck/queryfilter/filter_by_display_name_exact_test.go create mode 100644 querycheck/queryfilter/filter_by_display_name_regexp.go create mode 100644 querycheck/queryfilter/filter_by_display_name_regexp_test.go create mode 100644 querycheck/queryfilter/filter_by_resource_identity_test.go diff --git a/helper/resource/query/query_checks.go b/helper/resource/query/query_checks.go index 6414a31e..4e4c6b8d 100644 --- a/helper/resource/query/query_checks.go +++ b/helper/resource/query/query_checks.go @@ -63,6 +63,11 @@ func runQueryFilters(ctx context.Context, filterCheck querycheck.QueryResultChec filters := filterCheck.QueryFilters(ctx) filteredResults := make([]tfjson.ListResourceFoundData, 0) + // If there are no filters, just return the original results + if len(filters) == 0 { + return queryResults, nil + } + for _, result := range queryResults { keepResult := false diff --git a/querycheck/expect_resource_display_name.go b/querycheck/expect_resource_display_name.go index 8af51345..a4179f7a 100644 --- a/querycheck/expect_resource_display_name.go +++ b/querycheck/expect_resource_display_name.go @@ -23,6 +23,10 @@ type expectResourceDisplayName struct { } func (e expectResourceDisplayName) QueryFilters(ctx context.Context) []queryfilter.QueryFilter { + if e.filter == nil { + return []queryfilter.QueryFilter{} + } + return []queryfilter.QueryFilter{ e.filter, } diff --git a/querycheck/expect_resource_display_name_test.go b/querycheck/expect_resource_display_name_test.go index 3a76444d..9f10c0c0 100644 --- a/querycheck/expect_resource_display_name_test.go +++ b/querycheck/expect_resource_display_name_test.go @@ -3,6 +3,7 @@ package querycheck_test import ( + "regexp" "testing" "github.com/hashicorp/terraform-plugin-go/tfprotov6" @@ -58,22 +59,174 @@ func TestExpectResourceDisplayName(t *testing.T) { resource_group_name = "foo" } } + `, + QueryResultChecks: []querycheck.QueryResultCheck{ + querycheck.ExpectResourceDisplayName("examplecloud_containerette.test", queryfilter.ByDisplayNameExact("ananas"), "ananas"), + querycheck.ExpectResourceDisplayName("examplecloud_containerette.test", queryfilter.ByResourceIdentity(map[string]knownvalue.Check{ + "name": knownvalue.StringExact("ananas"), + "resource_group_name": knownvalue.StringExact("foo"), + }), "ananas"), + }, + }, + }, + }) +} - list "examplecloud_containerette" "test2" { +func TestExpectResourceDisplayName_TooManyResults(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{ + "examplecloud_containerette": examplecloudListResource(), + }, + Resources: map[string]testprovider.Resource{ + "examplecloud_containerette": examplecloudResource(), + }, + }), + }, + 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: ` + provider "examplecloud" {} + + list "examplecloud_containerette" "test" { provider = examplecloud config { - resource_group_name = "bar" - } + resource_group_name = "foo" + } + } + `, + QueryResultChecks: []querycheck.QueryResultCheck{ + querycheck.ExpectResourceDisplayName("examplecloud_containerette.test", nil, "ananas"), + }, + ExpectError: regexp.MustCompile("examplecloud_containerette.test - more than 1 query result found after filtering"), + }, + }, + }) +} + +func TestExpectResourceDisplayName_NoResults(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{ + "examplecloud_containerette": examplecloudListResource(), + }, + Resources: map[string]testprovider.Resource{ + "examplecloud_containerette": examplecloudResource(), + }, + }), + }, + 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: ` + provider "examplecloud" {} + + list "examplecloud_containerette" "test" { + provider = examplecloud + + config { + resource_group_name = "foo" + } + } + `, + QueryResultChecks: []querycheck.QueryResultCheck{ + querycheck.ExpectResourceDisplayName("examplecloud_containerette.test", queryfilter.ByResourceIdentity(map[string]knownvalue.Check{}), + "ananas"), + }, + ExpectError: regexp.MustCompile("examplecloud_containerette.test - no query results found after filtering"), + }, + }, + }) +} + +func TestExpectResourceDisplayName_InvalidDisplayName(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{ + "examplecloud_containerette": examplecloudListResource(), + }, + Resources: map[string]testprovider.Resource{ + "examplecloud_containerette": examplecloudResource(), + }, + }), + }, + 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: ` + provider "examplecloud" {} + + list "examplecloud_containerette" "test" { + provider = examplecloud + + config { + resource_group_name = "foo" + } } `, QueryResultChecks: []querycheck.QueryResultCheck{ - querycheck.ExpectResourceDisplayName("examplecloud_containerette.test", queryfilter.ByDisplayNameExact("ananas"), "ananas"), querycheck.ExpectResourceDisplayName("examplecloud_containerette.test", queryfilter.ByResourceIdentity(map[string]knownvalue.Check{ "name": knownvalue.StringExact("ananas"), "resource_group_name": knownvalue.StringExact("foo"), - }), "ananas"), + }), "invalid"), }, + ExpectError: regexp.MustCompile("expected to find resource with display name \"invalid\" in results but resource was not found"), }, }, }) diff --git a/querycheck/queryfilter/filter.go b/querycheck/queryfilter/filter.go index 2516430e..c0315c1e 100644 --- a/querycheck/queryfilter/filter.go +++ b/querycheck/queryfilter/filter.go @@ -9,6 +9,8 @@ import ( tfjson "github.com/hashicorp/terraform-json" ) +// QueryFilter defines an interface for implementing declarative filtering logic to apply to query results before +// the results are passed to a query check request. type QueryFilter interface { Filter(context.Context, FilterQueryRequest, *FilterQueryResponse) } diff --git a/querycheck/queryfilter/filter_by_display_name_exact.go b/querycheck/queryfilter/filter_by_display_name_exact.go index f41c3791..92477d5e 100644 --- a/querycheck/queryfilter/filter_by_display_name_exact.go +++ b/querycheck/queryfilter/filter_by_display_name_exact.go @@ -14,6 +14,8 @@ func (f filterByDisplayNameExact) Filter(ctx context.Context, req FilterQueryReq } } +// ByDisplayNameExact returns a query filter that only includes query items that match +// the specified display name. func ByDisplayNameExact(displayName string) QueryFilter { return filterByDisplayNameExact{ displayName: displayName, diff --git a/querycheck/queryfilter/filter_by_display_name_exact_test.go b/querycheck/queryfilter/filter_by_display_name_exact_test.go new file mode 100644 index 00000000..bb5f31c4 --- /dev/null +++ b/querycheck/queryfilter/filter_by_display_name_exact_test.go @@ -0,0 +1,71 @@ +package queryfilter_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/querycheck/queryfilter" +) + +func TestByDisplayNameExact(t *testing.T) { + testCases := map[string]struct { + displayName string + queryItem tfjson.ListResourceFoundData + expectInclude bool + expectedError error + }{ + "nil-query-result": { + displayName: "test", + expectInclude: false, + }, + "empty-display-name": { + displayName: "", + expectInclude: true, + }, + "included": { + displayName: "test", + queryItem: tfjson.ListResourceFoundData{ + DisplayName: "test", + }, + expectInclude: true, + }, + "not-included": { + displayName: "test", + queryItem: tfjson.ListResourceFoundData{ + DisplayName: "testsss", + }, + expectInclude: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + req := queryfilter.FilterQueryRequest{QueryItem: testCase.queryItem} + + resp := &queryfilter.FilterQueryResponse{} + + queryfilter.ByDisplayNameExact(testCase.displayName).Filter(context.TODO(), req, resp) + + if testCase.expectInclude != resp.Include { + t.Fatalf("expected included: %t, but got %t", testCase.expectInclude, resp.Include) + } + + if testCase.expectedError == nil && resp.Error != nil { + t.Errorf("unexpected error %s", resp.Error) + } + + if testCase.expectedError != nil && resp.Error == nil { + t.Errorf("expected error but got none") + } + + if diff := cmp.Diff(resp.Error, testCase.expectedError); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/querycheck/queryfilter/filter_by_display_name_regexp.go b/querycheck/queryfilter/filter_by_display_name_regexp.go new file mode 100644 index 00000000..89f31fc2 --- /dev/null +++ b/querycheck/queryfilter/filter_by_display_name_regexp.go @@ -0,0 +1,24 @@ +package queryfilter + +import ( + "context" + "regexp" +) + +type filterByDisplayNameRegexp struct { + regexp *regexp.Regexp +} + +func (f filterByDisplayNameRegexp) Filter(ctx context.Context, req FilterQueryRequest, resp *FilterQueryResponse) { + if f.regexp.MatchString(req.QueryItem.DisplayName) { + resp.Include = true + } +} + +// ByDisplayNameRegexp returns a query filter that only includes query items that match +// the specified regular expression. +func ByDisplayNameRegexp(regexp *regexp.Regexp) QueryFilter { + return filterByDisplayNameRegexp{ + regexp: regexp, + } +} diff --git a/querycheck/queryfilter/filter_by_display_name_regexp_test.go b/querycheck/queryfilter/filter_by_display_name_regexp_test.go new file mode 100644 index 00000000..7c8b27d1 --- /dev/null +++ b/querycheck/queryfilter/filter_by_display_name_regexp_test.go @@ -0,0 +1,72 @@ +package queryfilter_test + +import ( + "context" + "regexp" + "testing" + + "github.com/google/go-cmp/cmp" + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/querycheck/queryfilter" +) + +func TestByDisplayNameRegexp(t *testing.T) { + testCases := map[string]struct { + regexp *regexp.Regexp + queryItem tfjson.ListResourceFoundData + expectInclude bool + expectedError error + }{ + "nil-query-result": { + regexp: regexp.MustCompile("display"), + expectInclude: false, + }, + "empty-regexp": { + regexp: regexp.MustCompile(""), + expectInclude: true, + }, + "included": { + regexp: regexp.MustCompile("test"), + queryItem: tfjson.ListResourceFoundData{ + DisplayName: "test", + }, + expectInclude: true, + }, + "not-included": { + regexp: regexp.MustCompile("invalid"), + queryItem: tfjson.ListResourceFoundData{ + DisplayName: "testsss", + }, + expectInclude: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + req := queryfilter.FilterQueryRequest{QueryItem: testCase.queryItem} + + resp := &queryfilter.FilterQueryResponse{} + + queryfilter.ByDisplayNameRegexp(testCase.regexp).Filter(context.TODO(), req, resp) + + if testCase.expectInclude != resp.Include { + t.Fatalf("expected included: %t, but got %t", testCase.expectInclude, resp.Include) + } + + if testCase.expectedError == nil && resp.Error != nil { + t.Errorf("unexpected error %s", resp.Error) + } + + if testCase.expectedError != nil && resp.Error == nil { + t.Errorf("expected error but got none") + } + + if diff := cmp.Diff(resp.Error, testCase.expectedError); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/querycheck/queryfilter/filter_by_resource_identity.go b/querycheck/queryfilter/filter_by_resource_identity.go index bf487634..94ac954c 100644 --- a/querycheck/queryfilter/filter_by_resource_identity.go +++ b/querycheck/queryfilter/filter_by_resource_identity.go @@ -44,6 +44,11 @@ func (f filterByResourceIdentity) Filter(ctx context.Context, req FilterQueryReq resp.Include = true } +// ByResourceIdentity returns a query filter that only includes query items that match +// the given resource identity. +// +// Errors thrown by the given known value checks are only used to filter out non-matching query +// items and are otherwise ignored. func ByResourceIdentity(identity map[string]knownvalue.Check) QueryFilter { return filterByResourceIdentity{ identity: identity, diff --git a/querycheck/queryfilter/filter_by_resource_identity_test.go b/querycheck/queryfilter/filter_by_resource_identity_test.go new file mode 100644 index 00000000..e88269f1 --- /dev/null +++ b/querycheck/queryfilter/filter_by_resource_identity_test.go @@ -0,0 +1,165 @@ +package queryfilter_test + +import ( + "context" + "encoding/json" + "testing" + + "github.com/google/go-cmp/cmp" + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/querycheck/queryfilter" +) + +func TestByResourceIdentity(t *testing.T) { + testCases := map[string]struct { + identity map[string]knownvalue.Check + queryItem tfjson.ListResourceFoundData + expectInclude bool + expectedError error + }{ + "nil-query-result": { + identity: map[string]knownvalue.Check{ + "id": knownvalue.StringExact("id-123"), + }, + expectInclude: false, + }, + "nil-identity": { + expectInclude: true, + }, + "included": { + identity: map[string]knownvalue.Check{ + "id": knownvalue.StringExact("id-123"), + "list_of_numbers": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Int64Exact(1), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(4), + }, + ), + }, + queryItem: tfjson.ListResourceFoundData{ + DisplayName: "test", + Identity: map[string]any{ + "id": "id-123", + "list_of_numbers": []any{ + json.Number("1"), + json.Number("2"), + json.Number("3"), + json.Number("4"), + }, + }, + }, + expectInclude: true, + }, + "not-included-nonexistent-attribute": { + identity: map[string]knownvalue.Check{ + "id": knownvalue.StringExact("id-123"), + "nonexistent_attr": knownvalue.StringExact("hello"), + "list_of_numbers": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Int64Exact(1), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(4), + }, + ), + }, + queryItem: tfjson.ListResourceFoundData{ + DisplayName: "test", + Identity: map[string]any{ + "id": "id-123", + "list_of_numbers": []any{ + json.Number("1"), + json.Number("2"), + json.Number("3"), + json.Number("4"), + }, + }, + }, + expectInclude: false, + }, + "not-included-incorrect-string": { + identity: map[string]knownvalue.Check{ + "id": knownvalue.StringExact("id-123"), + "list_of_numbers": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Int64Exact(1), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(4), + }, + ), + }, + queryItem: tfjson.ListResourceFoundData{ + DisplayName: "test", + Identity: map[string]any{ + "id": "incorrect", + "list_of_numbers": []any{ + json.Number("1"), + json.Number("2"), + json.Number("3"), + json.Number("4"), + }, + }, + }, + expectInclude: false, + }, + "not-included-incorrect-list-item": { + identity: map[string]knownvalue.Check{ + "id": knownvalue.StringExact("id-123"), + "list_of_numbers": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Int64Exact(1), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(4), + }, + ), + }, + queryItem: tfjson.ListResourceFoundData{ + DisplayName: "test", + Identity: map[string]any{ + "id": "id-123", + "list_of_numbers": []any{ + json.Number("1"), + json.Number("2"), + json.Number("333"), + json.Number("4"), + }, + }, + }, + expectInclude: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + req := queryfilter.FilterQueryRequest{QueryItem: testCase.queryItem} + + resp := &queryfilter.FilterQueryResponse{} + + queryfilter.ByResourceIdentity(testCase.identity).Filter(context.TODO(), req, resp) + + if testCase.expectInclude != resp.Include { + t.Fatalf("expected included: %t, but got %t", testCase.expectInclude, resp.Include) + } + + if testCase.expectedError == nil && resp.Error != nil { + t.Errorf("unexpected error %s", resp.Error) + } + + if testCase.expectedError != nil && resp.Error == nil { + t.Errorf("expected error but got none") + } + + if diff := cmp.Diff(resp.Error, testCase.expectedError); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} From 1829e75df916a61c79285c7857991ed4635e69ec Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 11 Nov 2025 14:57:13 -0500 Subject: [PATCH 04/15] Add copyright headers and fix lint errors --- querycheck/queryfilter/filter_by_display_name_exact.go | 3 +++ .../queryfilter/filter_by_display_name_exact_test.go | 8 ++++++-- querycheck/queryfilter/filter_by_display_name_regexp.go | 3 +++ .../queryfilter/filter_by_display_name_regexp_test.go | 8 ++++++-- querycheck/queryfilter/filter_by_resource_identity.go | 3 +++ .../queryfilter/filter_by_resource_identity_test.go | 8 ++++++-- 6 files changed, 27 insertions(+), 6 deletions(-) diff --git a/querycheck/queryfilter/filter_by_display_name_exact.go b/querycheck/queryfilter/filter_by_display_name_exact.go index 92477d5e..d48dd2dc 100644 --- a/querycheck/queryfilter/filter_by_display_name_exact.go +++ b/querycheck/queryfilter/filter_by_display_name_exact.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package queryfilter import ( diff --git a/querycheck/queryfilter/filter_by_display_name_exact_test.go b/querycheck/queryfilter/filter_by_display_name_exact_test.go index bb5f31c4..4f932f4c 100644 --- a/querycheck/queryfilter/filter_by_display_name_exact_test.go +++ b/querycheck/queryfilter/filter_by_display_name_exact_test.go @@ -1,7 +1,9 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package queryfilter_test import ( - "context" "testing" "github.com/google/go-cmp/cmp" @@ -11,6 +13,8 @@ import ( ) func TestByDisplayNameExact(t *testing.T) { + t.Parallel() + testCases := map[string]struct { displayName string queryItem tfjson.ListResourceFoundData @@ -49,7 +53,7 @@ func TestByDisplayNameExact(t *testing.T) { resp := &queryfilter.FilterQueryResponse{} - queryfilter.ByDisplayNameExact(testCase.displayName).Filter(context.TODO(), req, resp) + queryfilter.ByDisplayNameExact(testCase.displayName).Filter(t.Context(), req, resp) if testCase.expectInclude != resp.Include { t.Fatalf("expected included: %t, but got %t", testCase.expectInclude, resp.Include) diff --git a/querycheck/queryfilter/filter_by_display_name_regexp.go b/querycheck/queryfilter/filter_by_display_name_regexp.go index 89f31fc2..ab26efcb 100644 --- a/querycheck/queryfilter/filter_by_display_name_regexp.go +++ b/querycheck/queryfilter/filter_by_display_name_regexp.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package queryfilter import ( diff --git a/querycheck/queryfilter/filter_by_display_name_regexp_test.go b/querycheck/queryfilter/filter_by_display_name_regexp_test.go index 7c8b27d1..8c7e7f1e 100644 --- a/querycheck/queryfilter/filter_by_display_name_regexp_test.go +++ b/querycheck/queryfilter/filter_by_display_name_regexp_test.go @@ -1,7 +1,9 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package queryfilter_test import ( - "context" "regexp" "testing" @@ -12,6 +14,8 @@ import ( ) func TestByDisplayNameRegexp(t *testing.T) { + t.Parallel() + testCases := map[string]struct { regexp *regexp.Regexp queryItem tfjson.ListResourceFoundData @@ -50,7 +54,7 @@ func TestByDisplayNameRegexp(t *testing.T) { resp := &queryfilter.FilterQueryResponse{} - queryfilter.ByDisplayNameRegexp(testCase.regexp).Filter(context.TODO(), req, resp) + queryfilter.ByDisplayNameRegexp(testCase.regexp).Filter(t.Context(), req, resp) if testCase.expectInclude != resp.Include { t.Fatalf("expected included: %t, but got %t", testCase.expectInclude, resp.Include) diff --git a/querycheck/queryfilter/filter_by_resource_identity.go b/querycheck/queryfilter/filter_by_resource_identity.go index 94ac954c..119da9b3 100644 --- a/querycheck/queryfilter/filter_by_resource_identity.go +++ b/querycheck/queryfilter/filter_by_resource_identity.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package queryfilter import ( diff --git a/querycheck/queryfilter/filter_by_resource_identity_test.go b/querycheck/queryfilter/filter_by_resource_identity_test.go index e88269f1..e2f1fb46 100644 --- a/querycheck/queryfilter/filter_by_resource_identity_test.go +++ b/querycheck/queryfilter/filter_by_resource_identity_test.go @@ -1,7 +1,9 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package queryfilter_test import ( - "context" "encoding/json" "testing" @@ -13,6 +15,8 @@ import ( ) func TestByResourceIdentity(t *testing.T) { + t.Parallel() + testCases := map[string]struct { identity map[string]knownvalue.Check queryItem tfjson.ListResourceFoundData @@ -143,7 +147,7 @@ func TestByResourceIdentity(t *testing.T) { resp := &queryfilter.FilterQueryResponse{} - queryfilter.ByResourceIdentity(testCase.identity).Filter(context.TODO(), req, resp) + queryfilter.ByResourceIdentity(testCase.identity).Filter(t.Context(), req, resp) if testCase.expectInclude != resp.Include { t.Fatalf("expected included: %t, but got %t", testCase.expectInclude, resp.Include) From 68c857d96da38589af899de31b36caaccfcad73d Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 11 Nov 2025 15:16:26 -0500 Subject: [PATCH 05/15] Rename `ExpectResourceDisplayName` to `ExpectResourceDisplayNameExact` --- ...o => expect_resource_display_name_exact.go} | 16 ++++++++-------- ...expect_resource_display_name_exact_test.go} | 18 +++++++++--------- 2 files changed, 17 insertions(+), 17 deletions(-) rename querycheck/{expect_resource_display_name.go => expect_resource_display_name_exact.go} (65%) rename querycheck/{expect_resource_display_name_test.go => expect_resource_display_name_exact_test.go} (87%) diff --git a/querycheck/expect_resource_display_name.go b/querycheck/expect_resource_display_name_exact.go similarity index 65% rename from querycheck/expect_resource_display_name.go rename to querycheck/expect_resource_display_name_exact.go index a4179f7a..cf6ce85a 100644 --- a/querycheck/expect_resource_display_name.go +++ b/querycheck/expect_resource_display_name_exact.go @@ -13,16 +13,16 @@ import ( "github.com/hashicorp/terraform-plugin-testing/querycheck/queryfilter" ) -var _ QueryResultCheck = expectResourceDisplayName{} -var _ QueryResultCheckWithFilters = expectResourceDisplayName{} +var _ QueryResultCheck = expectResourceDisplayNameExact{} +var _ QueryResultCheckWithFilters = expectResourceDisplayNameExact{} -type expectResourceDisplayName struct { +type expectResourceDisplayNameExact struct { listResourceAddress string filter queryfilter.QueryFilter displayName string } -func (e expectResourceDisplayName) QueryFilters(ctx context.Context) []queryfilter.QueryFilter { +func (e expectResourceDisplayNameExact) QueryFilters(ctx context.Context) []queryfilter.QueryFilter { if e.filter == nil { return []queryfilter.QueryFilter{} } @@ -32,7 +32,7 @@ func (e expectResourceDisplayName) QueryFilters(ctx context.Context) []queryfilt } } -func (e expectResourceDisplayName) CheckQuery(_ context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { +func (e expectResourceDisplayNameExact) CheckQuery(_ context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { listRes := make([]tfjson.ListResourceFoundData, 0) for _, result := range req.Query { if strings.TrimPrefix(result.Address, "list.") == e.listResourceAddress { @@ -57,11 +57,11 @@ func (e expectResourceDisplayName) CheckQuery(_ context.Context, req CheckQueryR resp.Error = fmt.Errorf("expected to find resource with display name %q in results but resource was not found", e.displayName) } -// ExpectResourceDisplayName returns a query check that asserts that a resource with a given display name exists within the returned results of the query. +// ExpectResourceDisplayNameExact returns a query check that asserts that a resource with a given display name exists within the returned results of the query. // // This query check can only be used with managed resources that support query. Query is only supported in Terraform v1.14+ -func ExpectResourceDisplayName(listResourceAddress string, filter queryfilter.QueryFilter, displayName string) QueryResultCheck { - return expectResourceDisplayName{ +func ExpectResourceDisplayNameExact(listResourceAddress string, filter queryfilter.QueryFilter, displayName string) QueryResultCheck { + return expectResourceDisplayNameExact{ listResourceAddress: listResourceAddress, filter: filter, displayName: displayName, diff --git a/querycheck/expect_resource_display_name_test.go b/querycheck/expect_resource_display_name_exact_test.go similarity index 87% rename from querycheck/expect_resource_display_name_test.go rename to querycheck/expect_resource_display_name_exact_test.go index 9f10c0c0..4a09d728 100644 --- a/querycheck/expect_resource_display_name_test.go +++ b/querycheck/expect_resource_display_name_exact_test.go @@ -17,7 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-testing/tfversion" ) -func TestExpectResourceDisplayName(t *testing.T) { +func TestExpectResourceDisplayNameExact(t *testing.T) { t.Parallel() r.UnitTest(t, r.TestCase{ @@ -61,8 +61,8 @@ func TestExpectResourceDisplayName(t *testing.T) { } `, QueryResultChecks: []querycheck.QueryResultCheck{ - querycheck.ExpectResourceDisplayName("examplecloud_containerette.test", queryfilter.ByDisplayNameExact("ananas"), "ananas"), - querycheck.ExpectResourceDisplayName("examplecloud_containerette.test", queryfilter.ByResourceIdentity(map[string]knownvalue.Check{ + querycheck.ExpectResourceDisplayNameExact("examplecloud_containerette.test", queryfilter.ByDisplayNameExact("ananas"), "ananas"), + querycheck.ExpectResourceDisplayNameExact("examplecloud_containerette.test", queryfilter.ByResourceIdentity(map[string]knownvalue.Check{ "name": knownvalue.StringExact("ananas"), "resource_group_name": knownvalue.StringExact("foo"), }), "ananas"), @@ -72,7 +72,7 @@ func TestExpectResourceDisplayName(t *testing.T) { }) } -func TestExpectResourceDisplayName_TooManyResults(t *testing.T) { +func TestExpectResourceDisplayNameExact_TooManyResults(t *testing.T) { t.Parallel() r.UnitTest(t, r.TestCase{ @@ -116,7 +116,7 @@ func TestExpectResourceDisplayName_TooManyResults(t *testing.T) { } `, QueryResultChecks: []querycheck.QueryResultCheck{ - querycheck.ExpectResourceDisplayName("examplecloud_containerette.test", nil, "ananas"), + querycheck.ExpectResourceDisplayNameExact("examplecloud_containerette.test", nil, "ananas"), }, ExpectError: regexp.MustCompile("examplecloud_containerette.test - more than 1 query result found after filtering"), }, @@ -124,7 +124,7 @@ func TestExpectResourceDisplayName_TooManyResults(t *testing.T) { }) } -func TestExpectResourceDisplayName_NoResults(t *testing.T) { +func TestExpectResourceDisplayNameExact_NoResults(t *testing.T) { t.Parallel() r.UnitTest(t, r.TestCase{ @@ -168,7 +168,7 @@ func TestExpectResourceDisplayName_NoResults(t *testing.T) { } `, QueryResultChecks: []querycheck.QueryResultCheck{ - querycheck.ExpectResourceDisplayName("examplecloud_containerette.test", queryfilter.ByResourceIdentity(map[string]knownvalue.Check{}), + querycheck.ExpectResourceDisplayNameExact("examplecloud_containerette.test", queryfilter.ByResourceIdentity(map[string]knownvalue.Check{}), "ananas"), }, ExpectError: regexp.MustCompile("examplecloud_containerette.test - no query results found after filtering"), @@ -177,7 +177,7 @@ func TestExpectResourceDisplayName_NoResults(t *testing.T) { }) } -func TestExpectResourceDisplayName_InvalidDisplayName(t *testing.T) { +func TestExpectResourceDisplayNameExact_InvalidDisplayName(t *testing.T) { t.Parallel() r.UnitTest(t, r.TestCase{ @@ -221,7 +221,7 @@ func TestExpectResourceDisplayName_InvalidDisplayName(t *testing.T) { } `, QueryResultChecks: []querycheck.QueryResultCheck{ - querycheck.ExpectResourceDisplayName("examplecloud_containerette.test", queryfilter.ByResourceIdentity(map[string]knownvalue.Check{ + querycheck.ExpectResourceDisplayNameExact("examplecloud_containerette.test", queryfilter.ByResourceIdentity(map[string]knownvalue.Check{ "name": knownvalue.StringExact("ananas"), "resource_group_name": knownvalue.StringExact("foo"), }), "invalid"), From a7e0ee9a0e2a057bc7fc354a8100dea81d94bbc9 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 11 Nov 2025 15:23:04 -0500 Subject: [PATCH 06/15] Add changelog entries --- .changes/unreleased/FEATURES-20251111-151917.yaml | 5 +++++ .changes/unreleased/FEATURES-20251111-152247.yaml | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changes/unreleased/FEATURES-20251111-151917.yaml create mode 100644 .changes/unreleased/FEATURES-20251111-152247.yaml diff --git a/.changes/unreleased/FEATURES-20251111-151917.yaml b/.changes/unreleased/FEATURES-20251111-151917.yaml new file mode 100644 index 00000000..b419a468 --- /dev/null +++ b/.changes/unreleased/FEATURES-20251111-151917.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'queryfilter: Introduced new `queryfilter` package with interface and built-in query check filtering functionality.' +time: 2025-11-11T15:19:17.237154-05:00 +custom: + Issue: "573" diff --git a/.changes/unreleased/FEATURES-20251111-152247.yaml b/.changes/unreleased/FEATURES-20251111-152247.yaml new file mode 100644 index 00000000..a13a29b5 --- /dev/null +++ b/.changes/unreleased/FEATURES-20251111-152247.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'querycheck: Added `ExpectResourceDisplayNameExact` query check to assert a specific display name value on a filtered query result.' +time: 2025-11-11T15:22:47.472876-05:00 +custom: + Issue: "573" From b45318540f689942855bc2c00e6ade3e917a0635 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 11 Nov 2025 16:50:38 -0500 Subject: [PATCH 07/15] Sort attributes in error message for `ExpectIdentity` query check --- querycheck/expect_identity.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/querycheck/expect_identity.go b/querycheck/expect_identity.go index 2d8777f7..39439099 100644 --- a/querycheck/expect_identity.go +++ b/querycheck/expect_identity.go @@ -73,10 +73,22 @@ func (e expectIdentity) CheckQuery(_ context.Context, req CheckQueryRequest, res var errCollection []error errCollection = append(errCollection, fmt.Errorf("an identity with the following attributes was not found")) + var keys []string + + for k := range e.check { + keys = append(keys, k) + } + + sort.SliceStable(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + // wrap errors for each check - for attr, check := range e.check { + for _, attr := range keys { + check := e.check[attr] errCollection = append(errCollection, fmt.Errorf("attribute %q: %s", attr, check)) } + errCollection = append(errCollection, fmt.Errorf("address: %s\n", e.listResourceAddress)) resp.Error = errors.Join(errCollection...) } From a3fb6a1468c69048824cb353ab2ac9effaccff45 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Mon, 24 Nov 2025 15:54:26 -0500 Subject: [PATCH 08/15] Run scope query filtering to individual query checks --- helper/resource/query/query_checks.go | 8 +++++--- .../expect_resource_display_name_exact_test.go | 17 +++++++++++++---- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/helper/resource/query/query_checks.go b/helper/resource/query/query_checks.go index 4e4c6b8d..79a5b981 100644 --- a/helper/resource/query/query_checks.go +++ b/helper/resource/query/query_checks.go @@ -39,17 +39,19 @@ func RunQueryChecks(ctx context.Context, t testing.T, query []tfjson.LogMsg, que } } + var reqQueryData []tfjson.ListResourceFoundData for _, queryCheck := range queryChecks { + reqQueryData = found if filterCheck, ok := queryCheck.(querycheck.QueryResultCheckWithFilters); ok { - var err error - found, err = runQueryFilters(ctx, filterCheck, found) + filtered, err := runQueryFilters(ctx, filterCheck, reqQueryData) if err != nil { return err } + reqQueryData = filtered } resp := querycheck.CheckQueryResponse{} queryCheck.CheckQuery(ctx, querycheck.CheckQueryRequest{ - Query: found, + Query: reqQueryData, QuerySummary: &summary, }, &resp) diff --git a/querycheck/expect_resource_display_name_exact_test.go b/querycheck/expect_resource_display_name_exact_test.go index 4a09d728..6a435f53 100644 --- a/querycheck/expect_resource_display_name_exact_test.go +++ b/querycheck/expect_resource_display_name_exact_test.go @@ -50,22 +50,31 @@ func TestExpectResourceDisplayNameExact(t *testing.T) { { Query: true, Config: ` - provider "examplecloud" {} - + 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.ExpectResourceDisplayNameExact("examplecloud_containerette.test", queryfilter.ByDisplayNameExact("ananas"), "ananas"), querycheck.ExpectResourceDisplayNameExact("examplecloud_containerette.test", queryfilter.ByResourceIdentity(map[string]knownvalue.Check{ "name": knownvalue.StringExact("ananas"), "resource_group_name": knownvalue.StringExact("foo"), }), "ananas"), + querycheck.ExpectResourceDisplayNameExact("examplecloud_containerette.test", queryfilter.ByResourceIdentity(map[string]knownvalue.Check{ + "name": knownvalue.StringExact("banane"), + "resource_group_name": knownvalue.StringExact("foo"), + }), "banane"), }, }, }, From ee0adc00b4ab125496865bded96c80d2ccc8a566 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 25 Nov 2025 17:27:26 -0500 Subject: [PATCH 09/15] Add `knownvalue.StringCheck` interface and implementations --- knownvalue/check.go | 10 ++++++++++ knownvalue/string.go | 5 +++++ knownvalue/string_func.go | 5 +++++ knownvalue/string_regexp.go | 5 +++++ 4 files changed, 25 insertions(+) diff --git a/knownvalue/check.go b/knownvalue/check.go index cef532c9..d771734d 100644 --- a/knownvalue/check.go +++ b/knownvalue/check.go @@ -12,3 +12,13 @@ type Check interface { // String should return a string representation of the type and value. String() string } + +// StringCheck defines an interface that is implemented to determine whether the string type and value match. Individual +// implementations determine how the match is performed (e.g., exact match, partial match). +type StringCheck interface { + // CheckString should assert the given known string value against any expectations. Use the error + // return to signal unexpected values or implementation errors. + CheckString(value string) error + // String should return a string representation of the type and value. + String() string +} diff --git a/knownvalue/string.go b/knownvalue/string.go index 63d03a50..d86c00ad 100644 --- a/knownvalue/string.go +++ b/knownvalue/string.go @@ -6,6 +6,7 @@ package knownvalue import "fmt" var _ Check = stringExact{} +var _ StringCheck = stringFunc{} type stringExact struct { value string @@ -27,6 +28,10 @@ func (v stringExact) CheckValue(other any) error { return nil } +func (v stringExact) CheckString(value string) error { + return v.CheckValue(value) +} + // String returns the string representation of the value. func (v stringExact) String() string { return v.value diff --git a/knownvalue/string_func.go b/knownvalue/string_func.go index e1dc3f0b..54736864 100644 --- a/knownvalue/string_func.go +++ b/knownvalue/string_func.go @@ -6,6 +6,7 @@ package knownvalue import "fmt" var _ Check = stringFunc{} +var _ StringCheck = stringFunc{} type stringFunc struct { checkFunc func(v string) error @@ -23,6 +24,10 @@ func (v stringFunc) CheckValue(value any) error { return v.checkFunc(val) } +func (v stringFunc) CheckString(value string) error { + return v.CheckValue(value) +} + // String returns the string representation of the value. func (v stringFunc) String() string { // Validation is up the the implementer of the function, so there are no diff --git a/knownvalue/string_regexp.go b/knownvalue/string_regexp.go index 782e2974..c6c811cf 100644 --- a/knownvalue/string_regexp.go +++ b/knownvalue/string_regexp.go @@ -9,6 +9,7 @@ import ( ) var _ Check = stringRegexp{} +var _ StringCheck = stringRegexp{} type stringRegexp struct { regex *regexp.Regexp @@ -31,6 +32,10 @@ func (v stringRegexp) CheckValue(other any) error { return nil } +func (v stringRegexp) CheckString(value string) error { + return v.CheckValue(value) +} + // String returns the string representation of the value. func (v stringRegexp) String() string { return v.regex.String() From 66e457b0052699a1b8e7599ca5b3b51777ed6172 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 25 Nov 2025 17:29:10 -0500 Subject: [PATCH 10/15] Add `ByDisplayName` query filter and remove `ByDisplayNameExact` and `ByDisplayNameRegexp` filters --- .../queryfilter/filter_by_display_name.go | 29 +++++++ .../filter_by_display_name_exact.go | 26 ------- .../filter_by_display_name_exact_test.go | 75 ------------------- .../filter_by_display_name_regexp.go | 27 ------- ...test.go => filter_by_display_name_test.go} | 45 ++++++++--- 5 files changed, 63 insertions(+), 139 deletions(-) create mode 100644 querycheck/queryfilter/filter_by_display_name.go delete mode 100644 querycheck/queryfilter/filter_by_display_name_exact.go delete mode 100644 querycheck/queryfilter/filter_by_display_name_exact_test.go delete mode 100644 querycheck/queryfilter/filter_by_display_name_regexp.go rename querycheck/queryfilter/{filter_by_display_name_regexp_test.go => filter_by_display_name_test.go} (54%) diff --git a/querycheck/queryfilter/filter_by_display_name.go b/querycheck/queryfilter/filter_by_display_name.go new file mode 100644 index 00000000..c37d1df6 --- /dev/null +++ b/querycheck/queryfilter/filter_by_display_name.go @@ -0,0 +1,29 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package queryfilter + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +type filterByDisplayName struct { + displayNameCheck knownvalue.StringCheck +} + +func (f filterByDisplayName) Filter(ctx context.Context, req FilterQueryRequest, resp *FilterQueryResponse) { + if err := f.displayNameCheck.CheckString(req.QueryItem.DisplayName); err == nil { + resp.Include = true + return + } +} + +// ByDisplayNameExact returns a query filter that only includes query items that match +// the specified display name. +func ByDisplayName(displayNameCheck knownvalue.StringCheck) QueryFilter { + return filterByDisplayName{ + displayNameCheck: displayNameCheck, + } +} diff --git a/querycheck/queryfilter/filter_by_display_name_exact.go b/querycheck/queryfilter/filter_by_display_name_exact.go deleted file mode 100644 index d48dd2dc..00000000 --- a/querycheck/queryfilter/filter_by_display_name_exact.go +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package queryfilter - -import ( - "context" -) - -type filterByDisplayNameExact struct { - displayName string -} - -func (f filterByDisplayNameExact) Filter(ctx context.Context, req FilterQueryRequest, resp *FilterQueryResponse) { - if req.QueryItem.DisplayName == f.displayName { - resp.Include = true - } -} - -// ByDisplayNameExact returns a query filter that only includes query items that match -// the specified display name. -func ByDisplayNameExact(displayName string) QueryFilter { - return filterByDisplayNameExact{ - displayName: displayName, - } -} diff --git a/querycheck/queryfilter/filter_by_display_name_exact_test.go b/querycheck/queryfilter/filter_by_display_name_exact_test.go deleted file mode 100644 index 4f932f4c..00000000 --- a/querycheck/queryfilter/filter_by_display_name_exact_test.go +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package queryfilter_test - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - tfjson "github.com/hashicorp/terraform-json" - - "github.com/hashicorp/terraform-plugin-testing/querycheck/queryfilter" -) - -func TestByDisplayNameExact(t *testing.T) { - t.Parallel() - - testCases := map[string]struct { - displayName string - queryItem tfjson.ListResourceFoundData - expectInclude bool - expectedError error - }{ - "nil-query-result": { - displayName: "test", - expectInclude: false, - }, - "empty-display-name": { - displayName: "", - expectInclude: true, - }, - "included": { - displayName: "test", - queryItem: tfjson.ListResourceFoundData{ - DisplayName: "test", - }, - expectInclude: true, - }, - "not-included": { - displayName: "test", - queryItem: tfjson.ListResourceFoundData{ - DisplayName: "testsss", - }, - expectInclude: false, - }, - } - - for name, testCase := range testCases { - t.Run(name, func(t *testing.T) { - t.Parallel() - - req := queryfilter.FilterQueryRequest{QueryItem: testCase.queryItem} - - resp := &queryfilter.FilterQueryResponse{} - - queryfilter.ByDisplayNameExact(testCase.displayName).Filter(t.Context(), req, resp) - - if testCase.expectInclude != resp.Include { - t.Fatalf("expected included: %t, but got %t", testCase.expectInclude, resp.Include) - } - - if testCase.expectedError == nil && resp.Error != nil { - t.Errorf("unexpected error %s", resp.Error) - } - - if testCase.expectedError != nil && resp.Error == nil { - t.Errorf("expected error but got none") - } - - if diff := cmp.Diff(resp.Error, testCase.expectedError); diff != "" { - t.Errorf("unexpected difference: %s", diff) - } - }) - } -} diff --git a/querycheck/queryfilter/filter_by_display_name_regexp.go b/querycheck/queryfilter/filter_by_display_name_regexp.go deleted file mode 100644 index ab26efcb..00000000 --- a/querycheck/queryfilter/filter_by_display_name_regexp.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package queryfilter - -import ( - "context" - "regexp" -) - -type filterByDisplayNameRegexp struct { - regexp *regexp.Regexp -} - -func (f filterByDisplayNameRegexp) Filter(ctx context.Context, req FilterQueryRequest, resp *FilterQueryResponse) { - if f.regexp.MatchString(req.QueryItem.DisplayName) { - resp.Include = true - } -} - -// ByDisplayNameRegexp returns a query filter that only includes query items that match -// the specified regular expression. -func ByDisplayNameRegexp(regexp *regexp.Regexp) QueryFilter { - return filterByDisplayNameRegexp{ - regexp: regexp, - } -} diff --git a/querycheck/queryfilter/filter_by_display_name_regexp_test.go b/querycheck/queryfilter/filter_by_display_name_test.go similarity index 54% rename from querycheck/queryfilter/filter_by_display_name_regexp_test.go rename to querycheck/queryfilter/filter_by_display_name_test.go index 8c7e7f1e..2df53779 100644 --- a/querycheck/queryfilter/filter_by_display_name_regexp_test.go +++ b/querycheck/queryfilter/filter_by_display_name_test.go @@ -10,35 +10,58 @@ import ( "github.com/google/go-cmp/cmp" tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" "github.com/hashicorp/terraform-plugin-testing/querycheck/queryfilter" ) -func TestByDisplayNameRegexp(t *testing.T) { +func TestByDisplayName(t *testing.T) { t.Parallel() testCases := map[string]struct { - regexp *regexp.Regexp + displayName knownvalue.StringCheck queryItem tfjson.ListResourceFoundData expectInclude bool expectedError error }{ - "nil-query-result": { - regexp: regexp.MustCompile("display"), + "nil-query-result-exact": { + displayName: knownvalue.StringExact("test"), expectInclude: false, }, - "empty-regexp": { - regexp: regexp.MustCompile(""), + "nil-query-result-regex": { + displayName: knownvalue.StringRegexp(regexp.MustCompile("display")), + expectInclude: false, + }, + "empty-display-name-exact": { + displayName: knownvalue.StringExact(""), + expectInclude: true, + }, + "empty-display-name-regexp": { + displayName: knownvalue.StringRegexp(regexp.MustCompile("")), + expectInclude: true, + }, + "included-exact": { + displayName: knownvalue.StringExact("test"), + queryItem: tfjson.ListResourceFoundData{ + DisplayName: "test", + }, expectInclude: true, }, - "included": { - regexp: regexp.MustCompile("test"), + "included-regex": { + displayName: knownvalue.StringRegexp(regexp.MustCompile("test")), queryItem: tfjson.ListResourceFoundData{ DisplayName: "test", }, expectInclude: true, }, - "not-included": { - regexp: regexp.MustCompile("invalid"), + "not-included-exact": { + displayName: knownvalue.StringExact("test"), + queryItem: tfjson.ListResourceFoundData{ + DisplayName: "testsss", + }, + expectInclude: false, + }, + "not-included-regex": { + displayName: knownvalue.StringRegexp(regexp.MustCompile("invalid")), queryItem: tfjson.ListResourceFoundData{ DisplayName: "testsss", }, @@ -54,7 +77,7 @@ func TestByDisplayNameRegexp(t *testing.T) { resp := &queryfilter.FilterQueryResponse{} - queryfilter.ByDisplayNameRegexp(testCase.regexp).Filter(t.Context(), req, resp) + queryfilter.ByDisplayName(testCase.displayName).Filter(t.Context(), req, resp) if testCase.expectInclude != resp.Include { t.Fatalf("expected included: %t, but got %t", testCase.expectInclude, resp.Include) From 79c446a7641931f43adb23667c6454992e1181a0 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 25 Nov 2025 17:30:35 -0500 Subject: [PATCH 11/15] Refactor `ExpectResourceDisplayNameExact` to use new `knownvalue.stringcheck` interface --- ...act.go => expect_resource_display_name.go} | 23 ++++++++++--------- ...o => expect_resource_display_name_test.go} | 20 ++++++++-------- 2 files changed, 22 insertions(+), 21 deletions(-) rename querycheck/{expect_resource_display_name_exact.go => expect_resource_display_name.go} (54%) rename querycheck/{expect_resource_display_name_exact_test.go => expect_resource_display_name_test.go} (87%) diff --git a/querycheck/expect_resource_display_name_exact.go b/querycheck/expect_resource_display_name.go similarity index 54% rename from querycheck/expect_resource_display_name_exact.go rename to querycheck/expect_resource_display_name.go index cf6ce85a..d38c774b 100644 --- a/querycheck/expect_resource_display_name_exact.go +++ b/querycheck/expect_resource_display_name.go @@ -10,19 +10,20 @@ import ( tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" "github.com/hashicorp/terraform-plugin-testing/querycheck/queryfilter" ) -var _ QueryResultCheck = expectResourceDisplayNameExact{} -var _ QueryResultCheckWithFilters = expectResourceDisplayNameExact{} +var _ QueryResultCheck = expectResourceDisplayName{} +var _ QueryResultCheckWithFilters = expectResourceDisplayName{} -type expectResourceDisplayNameExact struct { +type expectResourceDisplayName struct { listResourceAddress string filter queryfilter.QueryFilter - displayName string + displayName knownvalue.StringCheck } -func (e expectResourceDisplayNameExact) QueryFilters(ctx context.Context) []queryfilter.QueryFilter { +func (e expectResourceDisplayName) QueryFilters(ctx context.Context) []queryfilter.QueryFilter { if e.filter == nil { return []queryfilter.QueryFilter{} } @@ -32,7 +33,7 @@ func (e expectResourceDisplayNameExact) QueryFilters(ctx context.Context) []quer } } -func (e expectResourceDisplayNameExact) CheckQuery(_ context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { +func (e expectResourceDisplayName) CheckQuery(_ context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { listRes := make([]tfjson.ListResourceFoundData, 0) for _, result := range req.Query { if strings.TrimPrefix(result.Address, "list.") == e.listResourceAddress { @@ -50,18 +51,18 @@ func (e expectResourceDisplayNameExact) CheckQuery(_ context.Context, req CheckQ return } res := listRes[0] - if strings.EqualFold(e.displayName, res.DisplayName) { + if err := e.displayName.CheckString(res.DisplayName); err != nil { + resp.Error = fmt.Errorf("error checking value for display name %s, err: %s", e.displayName.String(), err) return } - resp.Error = fmt.Errorf("expected to find resource with display name %q in results but resource was not found", e.displayName) } -// ExpectResourceDisplayNameExact returns a query check that asserts that a resource with a given display name exists within the returned results of the query. +// ExpectResourceDisplayName returns a query check that asserts that a resource with a given display name exists within the returned results of the query. // // This query check can only be used with managed resources that support query. Query is only supported in Terraform v1.14+ -func ExpectResourceDisplayNameExact(listResourceAddress string, filter queryfilter.QueryFilter, displayName string) QueryResultCheck { - return expectResourceDisplayNameExact{ +func ExpectResourceDisplayName(listResourceAddress string, filter queryfilter.QueryFilter, displayName knownvalue.StringCheck) QueryResultCheck { + return expectResourceDisplayName{ listResourceAddress: listResourceAddress, filter: filter, displayName: displayName, diff --git a/querycheck/expect_resource_display_name_exact_test.go b/querycheck/expect_resource_display_name_test.go similarity index 87% rename from querycheck/expect_resource_display_name_exact_test.go rename to querycheck/expect_resource_display_name_test.go index 6a435f53..0a4d8696 100644 --- a/querycheck/expect_resource_display_name_exact_test.go +++ b/querycheck/expect_resource_display_name_test.go @@ -67,14 +67,14 @@ func TestExpectResourceDisplayNameExact(t *testing.T) { } `, QueryResultChecks: []querycheck.QueryResultCheck{ - querycheck.ExpectResourceDisplayNameExact("examplecloud_containerette.test", queryfilter.ByResourceIdentity(map[string]knownvalue.Check{ + querycheck.ExpectResourceDisplayName("examplecloud_containerette.test", queryfilter.ByResourceIdentity(map[string]knownvalue.Check{ "name": knownvalue.StringExact("ananas"), "resource_group_name": knownvalue.StringExact("foo"), - }), "ananas"), - querycheck.ExpectResourceDisplayNameExact("examplecloud_containerette.test", queryfilter.ByResourceIdentity(map[string]knownvalue.Check{ + }), knownvalue.StringExact("ananas")), + querycheck.ExpectResourceDisplayName("examplecloud_containerette.test", queryfilter.ByResourceIdentity(map[string]knownvalue.Check{ "name": knownvalue.StringExact("banane"), "resource_group_name": knownvalue.StringExact("foo"), - }), "banane"), + }), knownvalue.StringExact("banane")), }, }, }, @@ -125,7 +125,7 @@ func TestExpectResourceDisplayNameExact_TooManyResults(t *testing.T) { } `, QueryResultChecks: []querycheck.QueryResultCheck{ - querycheck.ExpectResourceDisplayNameExact("examplecloud_containerette.test", nil, "ananas"), + querycheck.ExpectResourceDisplayName("examplecloud_containerette.test", nil, knownvalue.StringExact("ananas")), }, ExpectError: regexp.MustCompile("examplecloud_containerette.test - more than 1 query result found after filtering"), }, @@ -177,8 +177,8 @@ func TestExpectResourceDisplayNameExact_NoResults(t *testing.T) { } `, QueryResultChecks: []querycheck.QueryResultCheck{ - querycheck.ExpectResourceDisplayNameExact("examplecloud_containerette.test", queryfilter.ByResourceIdentity(map[string]knownvalue.Check{}), - "ananas"), + querycheck.ExpectResourceDisplayName("examplecloud_containerette.test", queryfilter.ByResourceIdentity(map[string]knownvalue.Check{}), + knownvalue.StringExact("ananas")), }, ExpectError: regexp.MustCompile("examplecloud_containerette.test - no query results found after filtering"), }, @@ -230,12 +230,12 @@ func TestExpectResourceDisplayNameExact_InvalidDisplayName(t *testing.T) { } `, QueryResultChecks: []querycheck.QueryResultCheck{ - querycheck.ExpectResourceDisplayNameExact("examplecloud_containerette.test", queryfilter.ByResourceIdentity(map[string]knownvalue.Check{ + querycheck.ExpectResourceDisplayName("examplecloud_containerette.test", queryfilter.ByResourceIdentity(map[string]knownvalue.Check{ "name": knownvalue.StringExact("ananas"), "resource_group_name": knownvalue.StringExact("foo"), - }), "invalid"), + }), knownvalue.StringExact("invalid")), }, - ExpectError: regexp.MustCompile("expected to find resource with display name \"invalid\" in results but resource was not found"), + ExpectError: regexp.MustCompile("error checking value for display name invalid, err: expected value invalid for StringExact check, got: ananas"), }, }, }) From beec665f77af319ff1b89f5726ab3e7ff1e04491 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 25 Nov 2025 17:31:08 -0500 Subject: [PATCH 12/15] Remove `ContainsResourceWithName` query check --- querycheck/contains_name.go | 38 --------- querycheck/contains_name_test.go | 139 ------------------------------- 2 files changed, 177 deletions(-) delete mode 100644 querycheck/contains_name.go delete mode 100644 querycheck/contains_name_test.go diff --git a/querycheck/contains_name.go b/querycheck/contains_name.go deleted file mode 100644 index 6cef5701..00000000 --- a/querycheck/contains_name.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -import ( - "context" - "fmt" - "strings" -) - -var _ QueryResultCheck = contains{} - -type contains struct { - resourceAddress string - check string -} - -func (c contains) CheckQuery(_ context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { - for _, res := range req.Query { - if strings.EqualFold(c.check, res.DisplayName) { - return - } - } - - resp.Error = fmt.Errorf("expected to find resource with display name %q in results but resource was not found", c.check) - -} - -// ContainsResourceWithName returns a query check that asserts that a resource with a given display name exists within the returned results of the query. -// -// This query check can only be used with managed resources that support query. Query is only supported in Terraform v1.14+ -func ContainsResourceWithName(resourceAddress string, displayName string) QueryResultCheck { - return contains{ - resourceAddress: resourceAddress, - check: displayName, - } -} diff --git a/querycheck/contains_name_test.go b/querycheck/contains_name_test.go deleted file mode 100644 index 8ae703b1..00000000 --- a/querycheck/contains_name_test.go +++ /dev/null @@ -1,139 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -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" -) - -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{ - "examplecloud_containerette": examplecloudListResource(), - }, - Resources: map[string]testprovider.Resource{ - "examplecloud_containerette": examplecloudResource(), - }, - }), - }, - 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: ` - 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"), - }, - }, - }, - }) -} - -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{ - "examplecloud_containerette": examplecloudListResource(), - }, - Resources: map[string]testprovider.Resource{ - "examplecloud_containerette": examplecloudResource(), - }, - }), - }, - 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: ` - 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"), - }, - ExpectError: regexp.MustCompile("expected to find resource with display name \"pflaume\" in results but resource was not found"), - }, - }, - }) -} From b55eba7d288260c5ede03e1e5940c69e413ea82a Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 25 Nov 2025 17:32:29 -0500 Subject: [PATCH 13/15] Update changelogs --- .changes/unreleased/FEATURES-20251111-151917.yaml | 2 +- .changes/unreleased/FEATURES-20251111-152247.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.changes/unreleased/FEATURES-20251111-151917.yaml b/.changes/unreleased/FEATURES-20251111-151917.yaml index b419a468..9a3a0f6e 100644 --- a/.changes/unreleased/FEATURES-20251111-151917.yaml +++ b/.changes/unreleased/FEATURES-20251111-151917.yaml @@ -1,5 +1,5 @@ kind: FEATURES -body: 'queryfilter: Introduced new `queryfilter` package with interface and built-in query check filtering functionality.' +body: 'queryfilter: Introduces new `queryfilter` package with interface and built-in query check filtering functionality.' time: 2025-11-11T15:19:17.237154-05:00 custom: Issue: "573" diff --git a/.changes/unreleased/FEATURES-20251111-152247.yaml b/.changes/unreleased/FEATURES-20251111-152247.yaml index a13a29b5..7b807ec7 100644 --- a/.changes/unreleased/FEATURES-20251111-152247.yaml +++ b/.changes/unreleased/FEATURES-20251111-152247.yaml @@ -1,5 +1,5 @@ kind: FEATURES -body: 'querycheck: Added `ExpectResourceDisplayNameExact` query check to assert a specific display name value on a filtered query result.' +body: 'querycheck: Adds `ExpectResourceDisplayName` query check to assert a display name value on a filtered query result.' time: 2025-11-11T15:22:47.472876-05:00 custom: Issue: "573" From 18baf1d6c07c8f3c7029d2fc7f0508cc0c65a1a2 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Wed, 26 Nov 2025 16:10:30 -0500 Subject: [PATCH 14/15] Update querycheck/queryfilter/filter.go Co-authored-by: Austin Valle --- querycheck/queryfilter/filter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/querycheck/queryfilter/filter.go b/querycheck/queryfilter/filter.go index c0315c1e..716ccc2a 100644 --- a/querycheck/queryfilter/filter.go +++ b/querycheck/queryfilter/filter.go @@ -17,7 +17,7 @@ type QueryFilter interface { // FilterQueryRequest is a request to a filter function. type FilterQueryRequest struct { - // Query represents the parsed log messages relating to found resources returned by the `terraform query -json` command. + // QueryItem represents a single parsed log message relating to a found resource returned by the `terraform query -json` command. QueryItem tfjson.ListResourceFoundData } From e32297d2151958880b93614012c10fb03d77964b Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Wed, 26 Nov 2025 16:24:03 -0500 Subject: [PATCH 15/15] Remove `knownvalue.StringCheck` interface --- knownvalue/check.go | 10 ---------- knownvalue/string.go | 5 ----- knownvalue/string_func.go | 5 ----- knownvalue/string_regexp.go | 5 ----- querycheck/expect_resource_display_name.go | 6 +++--- querycheck/queryfilter/filter_by_display_name.go | 6 +++--- querycheck/queryfilter/filter_by_display_name_test.go | 2 +- 7 files changed, 7 insertions(+), 32 deletions(-) diff --git a/knownvalue/check.go b/knownvalue/check.go index d771734d..cef532c9 100644 --- a/knownvalue/check.go +++ b/knownvalue/check.go @@ -12,13 +12,3 @@ type Check interface { // String should return a string representation of the type and value. String() string } - -// StringCheck defines an interface that is implemented to determine whether the string type and value match. Individual -// implementations determine how the match is performed (e.g., exact match, partial match). -type StringCheck interface { - // CheckString should assert the given known string value against any expectations. Use the error - // return to signal unexpected values or implementation errors. - CheckString(value string) error - // String should return a string representation of the type and value. - String() string -} diff --git a/knownvalue/string.go b/knownvalue/string.go index d86c00ad..63d03a50 100644 --- a/knownvalue/string.go +++ b/knownvalue/string.go @@ -6,7 +6,6 @@ package knownvalue import "fmt" var _ Check = stringExact{} -var _ StringCheck = stringFunc{} type stringExact struct { value string @@ -28,10 +27,6 @@ func (v stringExact) CheckValue(other any) error { return nil } -func (v stringExact) CheckString(value string) error { - return v.CheckValue(value) -} - // String returns the string representation of the value. func (v stringExact) String() string { return v.value diff --git a/knownvalue/string_func.go b/knownvalue/string_func.go index 54736864..e1dc3f0b 100644 --- a/knownvalue/string_func.go +++ b/knownvalue/string_func.go @@ -6,7 +6,6 @@ package knownvalue import "fmt" var _ Check = stringFunc{} -var _ StringCheck = stringFunc{} type stringFunc struct { checkFunc func(v string) error @@ -24,10 +23,6 @@ func (v stringFunc) CheckValue(value any) error { return v.checkFunc(val) } -func (v stringFunc) CheckString(value string) error { - return v.CheckValue(value) -} - // String returns the string representation of the value. func (v stringFunc) String() string { // Validation is up the the implementer of the function, so there are no diff --git a/knownvalue/string_regexp.go b/knownvalue/string_regexp.go index c6c811cf..782e2974 100644 --- a/knownvalue/string_regexp.go +++ b/knownvalue/string_regexp.go @@ -9,7 +9,6 @@ import ( ) var _ Check = stringRegexp{} -var _ StringCheck = stringRegexp{} type stringRegexp struct { regex *regexp.Regexp @@ -32,10 +31,6 @@ func (v stringRegexp) CheckValue(other any) error { return nil } -func (v stringRegexp) CheckString(value string) error { - return v.CheckValue(value) -} - // String returns the string representation of the value. func (v stringRegexp) String() string { return v.regex.String() diff --git a/querycheck/expect_resource_display_name.go b/querycheck/expect_resource_display_name.go index d38c774b..89f3f00c 100644 --- a/querycheck/expect_resource_display_name.go +++ b/querycheck/expect_resource_display_name.go @@ -20,7 +20,7 @@ var _ QueryResultCheckWithFilters = expectResourceDisplayName{} type expectResourceDisplayName struct { listResourceAddress string filter queryfilter.QueryFilter - displayName knownvalue.StringCheck + displayName knownvalue.Check } func (e expectResourceDisplayName) QueryFilters(ctx context.Context) []queryfilter.QueryFilter { @@ -51,7 +51,7 @@ func (e expectResourceDisplayName) CheckQuery(_ context.Context, req CheckQueryR return } res := listRes[0] - if err := e.displayName.CheckString(res.DisplayName); err != nil { + if err := e.displayName.CheckValue(res.DisplayName); err != nil { resp.Error = fmt.Errorf("error checking value for display name %s, err: %s", e.displayName.String(), err) return } @@ -61,7 +61,7 @@ func (e expectResourceDisplayName) CheckQuery(_ context.Context, req CheckQueryR // ExpectResourceDisplayName returns a query check that asserts that a resource with a given display name exists within the returned results of the query. // // This query check can only be used with managed resources that support query. Query is only supported in Terraform v1.14+ -func ExpectResourceDisplayName(listResourceAddress string, filter queryfilter.QueryFilter, displayName knownvalue.StringCheck) QueryResultCheck { +func ExpectResourceDisplayName(listResourceAddress string, filter queryfilter.QueryFilter, displayName knownvalue.Check) QueryResultCheck { return expectResourceDisplayName{ listResourceAddress: listResourceAddress, filter: filter, diff --git a/querycheck/queryfilter/filter_by_display_name.go b/querycheck/queryfilter/filter_by_display_name.go index c37d1df6..025559b7 100644 --- a/querycheck/queryfilter/filter_by_display_name.go +++ b/querycheck/queryfilter/filter_by_display_name.go @@ -10,11 +10,11 @@ import ( ) type filterByDisplayName struct { - displayNameCheck knownvalue.StringCheck + displayNameCheck knownvalue.Check } func (f filterByDisplayName) Filter(ctx context.Context, req FilterQueryRequest, resp *FilterQueryResponse) { - if err := f.displayNameCheck.CheckString(req.QueryItem.DisplayName); err == nil { + if err := f.displayNameCheck.CheckValue(req.QueryItem.DisplayName); err == nil { resp.Include = true return } @@ -22,7 +22,7 @@ func (f filterByDisplayName) Filter(ctx context.Context, req FilterQueryRequest, // ByDisplayNameExact returns a query filter that only includes query items that match // the specified display name. -func ByDisplayName(displayNameCheck knownvalue.StringCheck) QueryFilter { +func ByDisplayName(displayNameCheck knownvalue.Check) QueryFilter { return filterByDisplayName{ displayNameCheck: displayNameCheck, } diff --git a/querycheck/queryfilter/filter_by_display_name_test.go b/querycheck/queryfilter/filter_by_display_name_test.go index 2df53779..a7ba1f29 100644 --- a/querycheck/queryfilter/filter_by_display_name_test.go +++ b/querycheck/queryfilter/filter_by_display_name_test.go @@ -18,7 +18,7 @@ func TestByDisplayName(t *testing.T) { t.Parallel() testCases := map[string]struct { - displayName knownvalue.StringCheck + displayName knownvalue.Check queryItem tfjson.ListResourceFoundData expectInclude bool expectedError error