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" diff --git a/helper/resource/query/query_checks.go b/helper/resource/query/query_checks.go index 8ef9a3e0..4e4c6b8d 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,37 @@ 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) + + // If there are no filters, just return the original results + if len(filters) == 0 { + return queryResults, nil + } + + 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_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...) } diff --git a/querycheck/expect_resource_display_name_exact.go b/querycheck/expect_resource_display_name_exact.go new file mode 100644 index 00000000..cf6ce85a --- /dev/null +++ b/querycheck/expect_resource_display_name_exact.go @@ -0,0 +1,69 @@ +// 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 = expectResourceDisplayNameExact{} +var _ QueryResultCheckWithFilters = expectResourceDisplayNameExact{} + +type expectResourceDisplayNameExact struct { + listResourceAddress string + filter queryfilter.QueryFilter + displayName string +} + +func (e expectResourceDisplayNameExact) QueryFilters(ctx context.Context) []queryfilter.QueryFilter { + if e.filter == nil { + return []queryfilter.QueryFilter{} + } + + return []queryfilter.QueryFilter{ + e.filter, + } +} + +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 { + 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) +} + +// 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 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_exact_test.go b/querycheck/expect_resource_display_name_exact_test.go new file mode 100644 index 00000000..4a09d728 --- /dev/null +++ b/querycheck/expect_resource_display_name_exact_test.go @@ -0,0 +1,233 @@ +// 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/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/querycheck" + "github.com/hashicorp/terraform-plugin-testing/querycheck/queryfilter" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestExpectResourceDisplayNameExact(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.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"), + }, + }, + }, + }) +} + +func TestExpectResourceDisplayNameExact_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 = "foo" + } + } + `, + QueryResultChecks: []querycheck.QueryResultCheck{ + querycheck.ExpectResourceDisplayNameExact("examplecloud_containerette.test", nil, "ananas"), + }, + ExpectError: regexp.MustCompile("examplecloud_containerette.test - more than 1 query result found after filtering"), + }, + }, + }) +} + +func TestExpectResourceDisplayNameExact_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.ExpectResourceDisplayNameExact("examplecloud_containerette.test", queryfilter.ByResourceIdentity(map[string]knownvalue.Check{}), + "ananas"), + }, + ExpectError: regexp.MustCompile("examplecloud_containerette.test - no query results found after filtering"), + }, + }, + }) +} + +func TestExpectResourceDisplayNameExact_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.ExpectResourceDisplayNameExact("examplecloud_containerette.test", queryfilter.ByResourceIdentity(map[string]knownvalue.Check{ + "name": knownvalue.StringExact("ananas"), + "resource_group_name": knownvalue.StringExact("foo"), + }), "invalid"), + }, + ExpectError: regexp.MustCompile("expected to find resource with display name \"invalid\" in results but resource was not found"), + }, + }, + }) +} 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. diff --git a/querycheck/queryfilter/filter.go b/querycheck/queryfilter/filter.go new file mode 100644 index 00000000..c0315c1e --- /dev/null +++ b/querycheck/queryfilter/filter.go @@ -0,0 +1,32 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package queryfilter + +import ( + "context" + + 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) +} + +// 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..d48dd2dc --- /dev/null +++ b/querycheck/queryfilter/filter_by_display_name_exact.go @@ -0,0 +1,26 @@ +// 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 new file mode 100644 index 00000000..4f932f4c --- /dev/null +++ b/querycheck/queryfilter/filter_by_display_name_exact_test.go @@ -0,0 +1,75 @@ +// 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 new file mode 100644 index 00000000..ab26efcb --- /dev/null +++ b/querycheck/queryfilter/filter_by_display_name_regexp.go @@ -0,0 +1,27 @@ +// 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_regexp_test.go new file mode 100644 index 00000000..8c7e7f1e --- /dev/null +++ b/querycheck/queryfilter/filter_by_display_name_regexp_test.go @@ -0,0 +1,76 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package queryfilter_test + +import ( + "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) { + t.Parallel() + + 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(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_resource_identity.go b/querycheck/queryfilter/filter_by_resource_identity.go new file mode 100644 index 00000000..119da9b3 --- /dev/null +++ b/querycheck/queryfilter/filter_by_resource_identity.go @@ -0,0 +1,59 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +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 +} + +// 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..e2f1fb46 --- /dev/null +++ b/querycheck/queryfilter/filter_by_resource_identity_test.go @@ -0,0 +1,169 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package queryfilter_test + +import ( + "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) { + t.Parallel() + + 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(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) + } + }) + } +}