Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/unreleased/FEATURES-20251111-151917.yaml
Original file line number Diff line number Diff line change
@@ -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"
5 changes: 5 additions & 0 deletions .changes/unreleased/FEATURES-20251111-152247.yaml
Original file line number Diff line number Diff line change
@@ -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"
42 changes: 42 additions & 0 deletions helper/resource/query/query_checks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand All @@ -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
}
14 changes: 13 additions & 1 deletion querycheck/expect_identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -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...)
}
Expand Down
69 changes: 69 additions & 0 deletions querycheck/expect_resource_display_name_exact.go
Original file line number Diff line number Diff line change
@@ -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,
}
}
233 changes: 233 additions & 0 deletions querycheck/expect_resource_display_name_exact_test.go
Original file line number Diff line number Diff line change
@@ -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"),
},
},
})
}
Loading