diff --git a/.changes/unreleased/FEATURES-20251202-113347.yaml b/.changes/unreleased/FEATURES-20251202-113347.yaml new file mode 100644 index 00000000..675925a5 --- /dev/null +++ b/.changes/unreleased/FEATURES-20251202-113347.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'querycheck: Adds `ExpectResourceKnownValues` query check to assert resource values on a filtered query result.' +time: 2025-12-02T11:33:47.606282+01:00 +custom: + Issue: "583" diff --git a/querycheck/expect_known_value.go b/querycheck/expect_known_value.go deleted file mode 100644 index faa046eb..00000000 --- a/querycheck/expect_known_value.go +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -import ( - "context" - "fmt" - "strings" - - "github.com/hashicorp/terraform-plugin-testing/knownvalue" - "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" -) - -var _ QueryResultCheck = expectKnownValue{} - -type expectKnownValue struct { - listResourceAddress string - resourceName string - attributePath tfjsonpath.Path - knownValue knownvalue.Check -} - -func (e expectKnownValue) CheckQuery(_ context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { - for _, res := range req.Query { - var diags []error - - if e.listResourceAddress == strings.TrimPrefix(res.Address, "list.") && e.resourceName == res.DisplayName { - if res.ResourceObject == nil { - resp.Error = fmt.Errorf("%s - no resource object was returned, ensure `include_resource` has been set to `true` in the list resource config`", e.listResourceAddress) - return - } - - resource, err := tfjsonpath.Traverse(res.ResourceObject, e.attributePath) - if err != nil { - resp.Error = err - return - } - - if err := e.knownValue.CheckValue(resource); err != nil { - diags = append(diags, fmt.Errorf("error checking value for attribute at path: %s for resource %s, err: %s", e.attributePath.String(), e.resourceName, err)) - } - - if diags == nil { - return - } - } - - if diags != nil { - var diagsStr string - for _, diag := range diags { - diagsStr += diag.Error() + "; " - } - resp.Error = fmt.Errorf("the following errors were found while checking values: %s", diagsStr) - return - } - } - - resp.Error = fmt.Errorf("%s - the resource %s was not found", e.listResourceAddress, e.resourceName) -} - -// ExpectKnownValue returns a query check that asserts the specified attribute values are present for a given resource object -// returned by a list query. The resource object can only be identified by providing the list resource address as well as the -// resource name (display name). -// -// This query check can only be used with managed resources that support resource identity and query. Query is only supported in Terraform v1.14+ -func ExpectKnownValue(listResourceAddress string, resourceName string, attributePath tfjsonpath.Path, knownValue knownvalue.Check) QueryResultCheck { - return expectKnownValue{ - listResourceAddress: listResourceAddress, - resourceName: resourceName, - attributePath: attributePath, - knownValue: knownValue, - } -} diff --git a/querycheck/expect_resource_known_values.go b/querycheck/expect_resource_known_values.go new file mode 100644 index 00000000..8185d8f0 --- /dev/null +++ b/querycheck/expect_resource_known_values.go @@ -0,0 +1,93 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "fmt" + tfjson "github.com/hashicorp/terraform-json" + "strings" + + "github.com/hashicorp/terraform-plugin-testing/querycheck/queryfilter" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +var _ QueryResultCheck = expectResourceKnownValues{} +var _ QueryResultCheckWithFilters = expectResourceKnownValues{} + +type expectResourceKnownValues struct { + listResourceAddress string + filter queryfilter.QueryFilter + knownValueChecks []KnownValueCheck +} + +func (e expectResourceKnownValues) QueryFilters(ctx context.Context) []queryfilter.QueryFilter { + if e.filter == nil { + return []queryfilter.QueryFilter{} + } + + return []queryfilter.QueryFilter{ + e.filter, + } +} + +func (e expectResourceKnownValues) CheckQuery(_ context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { + listRes := make([]tfjson.ListResourceFoundData, 0) + var diags []error + for _, res := range req.Query { + if e.listResourceAddress == strings.TrimPrefix(res.Address, "list.") { + listRes = append(listRes, res) + } + } + + 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 res.ResourceObject == nil { + resp.Error = fmt.Errorf("%s - no resource object was returned, ensure `include_resource` has been set to `true` in the list resource config`", e.listResourceAddress) + return + } + + for _, c := range e.knownValueChecks { + resource, err := tfjsonpath.Traverse(res.ResourceObject, c.Path) + if err != nil { + resp.Error = err + return + } + + if err := c.KnownValue.CheckValue(resource); err != nil { + diags = append(diags, fmt.Errorf("error checking value for attribute at path: %s for resource with identity %s, err: %s", c.Path.String(), e.filter, err)) + } + } + + if diags != nil { + var diagsStr string + for _, diag := range diags { + diagsStr += diag.Error() + "; " + } + resp.Error = fmt.Errorf("the following errors were found while checking values: %s", diagsStr) + return + } +} + +// ExpectResourceKnownValues returns a query check which asserts that a resource object identified by a query filter +// passes the given query checks. +// +// This query check can only be used with managed resources that support resource identity and query. Query is only supported in Terraform v1.14+ +func ExpectResourceKnownValues(listResourceAddress string, filter queryfilter.QueryFilter, knownValues []KnownValueCheck) QueryResultCheck { + return expectResourceKnownValues{ + listResourceAddress: listResourceAddress, + filter: filter, + knownValueChecks: knownValues, + } +} diff --git a/querycheck/expect_known_value_test.go b/querycheck/expect_resource_known_values_test.go similarity index 75% rename from querycheck/expect_known_value_test.go rename to querycheck/expect_resource_known_values_test.go index a12ab688..b780ef16 100644 --- a/querycheck/expect_known_value_test.go +++ b/querycheck/expect_resource_known_values_test.go @@ -14,11 +14,12 @@ import ( "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/tfjsonpath" "github.com/hashicorp/terraform-plugin-testing/tfversion" ) -func TestExpectKnownValue(t *testing.T) { +func TestExpectResourceKnownValues(t *testing.T) { t.Parallel() r.UnitTest(t, r.TestCase{ @@ -67,11 +68,20 @@ func TestExpectKnownValue(t *testing.T) { } `, QueryResultChecks: []querycheck.QueryResultCheck{ - querycheck.ExpectKnownValue( - "examplecloud_containerette.test", - "banane", - tfjsonpath.New("instances"), - knownvalue.NumberExact(big.NewFloat(5)), + querycheck.ExpectResourceKnownValues( + "examplecloud_containerette.test", queryfilter.ByResourceIdentity(map[string]knownvalue.Check{ + "name": knownvalue.StringExact("banane"), + "resource_group_name": knownvalue.StringExact("foo"), + }), []querycheck.KnownValueCheck{ + { + tfjsonpath.New("instances"), + knownvalue.NumberExact(big.NewFloat(5)), + }, + { + tfjsonpath.New("location"), + knownvalue.StringExact("westeurope"), + }, + }, ), }, }, @@ -79,7 +89,7 @@ func TestExpectKnownValue(t *testing.T) { }) } -func TestExpectKnownValue_ValueIncorrect(t *testing.T) { +func TestExpectResourceKnownValues_ValueIncorrect(t *testing.T) { t.Parallel() r.UnitTest(t, r.TestCase{ @@ -128,14 +138,23 @@ func TestExpectKnownValue_ValueIncorrect(t *testing.T) { } `, QueryResultChecks: []querycheck.QueryResultCheck{ - querycheck.ExpectKnownValue( - "examplecloud_containerette.test", - "banane", - tfjsonpath.New("instances"), - knownvalue.NumberExact(big.NewFloat(4)), + querycheck.ExpectResourceKnownValues( + "examplecloud_containerette.test", queryfilter.ByResourceIdentity(map[string]knownvalue.Check{ + "name": knownvalue.StringExact("banane"), + "resource_group_name": knownvalue.StringExact("foo"), + }), []querycheck.KnownValueCheck{ + { + tfjsonpath.New("location"), + knownvalue.StringExact("westeurope"), + }, + { + tfjsonpath.New("instances"), + knownvalue.NumberExact(big.NewFloat(4)), + }, + }, ), }, - ExpectError: regexp.MustCompile("the following errors were found while checking values: error checking value for attribute at path: instances for resource banane, err: expected value 4 for NumberExact check, got: 5;"), + ExpectError: regexp.MustCompile("the following errors were found while checking values: error checking value for attribute at path: instances for resource with identity .*, err: expected value 4 for NumberExact check, got: 5;"), }, }, }) diff --git a/querycheck/known_value.go b/querycheck/known_value.go new file mode 100644 index 00000000..67179ec2 --- /dev/null +++ b/querycheck/known_value.go @@ -0,0 +1,19 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +// KnownValueCheck represents a check of a known value at a specific JSON path +// and is used to specify multiple known value checks to assert against a +// single resource object returned by a query. +type KnownValueCheck struct { + // Path specifies the JSON path to check within the resource object. + Path tfjsonpath.Path + // KnownValue specifies the expected known value check to perform at the given path. + KnownValue knownvalue.Check +}