From 954b002427e049dccb1bdc8269b0a4429f72bc71 Mon Sep 17 00:00:00 2001 From: Rain Date: Mon, 21 Jul 2025 12:33:20 -0400 Subject: [PATCH 01/45] Initial work on Query command for List Resource --- go.mod | 10 +- go.sum | 40 +++---- helper/resource/testing.go | 3 + internal/plugintest/working_dir.go | 100 +++++++++++++++++- .../testing/testprovider/list_resource.go | 28 +++++ internal/testing/testprovider/provider.go | 12 +++ .../testing/testsdk/list/list_resource.go | 36 +++++++ internal/testing/testsdk/provider/provider.go | 2 + .../testsdk/providerserver/list_resources.go | 24 +++++ .../testsdk/providerserver/providerserver.go | 73 ++++++++++++- internal/testing/testsdk/resource/resource.go | 1 - internal/teststep/config.go | 6 +- internal/teststep/directory.go | 4 + internal/teststep/file.go | 4 + internal/teststep/string.go | 14 +++ tfversion/versions.go | 1 + 16 files changed, 325 insertions(+), 33 deletions(-) create mode 100644 internal/testing/testprovider/list_resource.go create mode 100644 internal/testing/testsdk/list/list_resource.go create mode 100644 internal/testing/testsdk/providerserver/list_resources.go diff --git a/go.mod b/go.mod index c0b835569..bc32ed306 100644 --- a/go.mod +++ b/go.mod @@ -13,9 +13,9 @@ require ( github.com/hashicorp/hc-install v0.9.2 github.com/hashicorp/hcl/v2 v2.24.0 github.com/hashicorp/logutils v1.0.0 - github.com/hashicorp/terraform-exec v0.23.0 + github.com/hashicorp/terraform-exec v0.23.1-0.20250717072919-061a850a52d2 github.com/hashicorp/terraform-json v0.25.0 - github.com/hashicorp/terraform-plugin-go v0.28.0 + github.com/hashicorp/terraform-plugin-go v0.29.0-alpha.1 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0 github.com/mitchellh/go-testing-interface v1.14.1 @@ -36,7 +36,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-plugin v1.6.3 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect - github.com/hashicorp/terraform-registry-address v0.2.5 // indirect + github.com/hashicorp/terraform-registry-address v0.3.0 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect @@ -56,7 +56,7 @@ require ( golang.org/x/text v0.27.0 // indirect golang.org/x/tools v0.34.0 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect - google.golang.org/grpc v1.72.1 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect + google.golang.org/grpc v1.73.0 // indirect google.golang.org/protobuf v1.36.6 // indirect ) diff --git a/go.sum b/go.sum index 2735878e2..5b943dc60 100644 --- a/go.sum +++ b/go.sum @@ -76,18 +76,18 @@ github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQx github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/terraform-exec v0.23.0 h1:MUiBM1s0CNlRFsCLJuM5wXZrzA3MnPYEsiXmzATMW/I= -github.com/hashicorp/terraform-exec v0.23.0/go.mod h1:mA+qnx1R8eePycfwKkCRk3Wy65mwInvlpAeOwmA7vlY= +github.com/hashicorp/terraform-exec v0.23.1-0.20250717072919-061a850a52d2 h1:90fcAqw0Qmv4vY7zL4jEKgKarHmOnNN6SjTY68eLKGA= +github.com/hashicorp/terraform-exec v0.23.1-0.20250717072919-061a850a52d2/go.mod h1:8D3RLLpzAZdhT9jvALYz1KHyGU4OvI73I1o0+01QJxA= github.com/hashicorp/terraform-json v0.25.0 h1:rmNqc/CIfcWawGiwXmRuiXJKEiJu1ntGoxseG1hLhoQ= github.com/hashicorp/terraform-json v0.25.0/go.mod h1:sMKS8fiRDX4rVlR6EJUMudg1WcanxCMoWwTLkgZP/vc= -github.com/hashicorp/terraform-plugin-go v0.28.0 h1:zJmu2UDwhVN0J+J20RE5huiF3XXlTYVIleaevHZgKPA= -github.com/hashicorp/terraform-plugin-go v0.28.0/go.mod h1:FDa2Bb3uumkTGSkTFpWSOwWJDwA7bf3vdP3ltLDTH6o= +github.com/hashicorp/terraform-plugin-go v0.29.0-alpha.1 h1:ZId6oWG8VTKhz207quE/Xh8a3HuoLtM/QkcSSypekIQ= +github.com/hashicorp/terraform-plugin-go v0.29.0-alpha.1/go.mod h1:hL//wLEfYo0YVt0TC/VLzia/ADQQto3HEm4/jX2gkdY= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0 h1:NFPMacTrY/IdcIcnUB+7hsore1ZaRWU9cnB6jFoBnIM= github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0/go.mod h1:QYmYnLfsosrxjCnGY1p9c7Zj6n9thnEE+7RObeYs3fA= -github.com/hashicorp/terraform-registry-address v0.2.5 h1:2GTftHqmUhVOeuu9CW3kwDkRe4pcBDq0uuK5VJngU1M= -github.com/hashicorp/terraform-registry-address v0.2.5/go.mod h1:PpzXWINwB5kuVS5CA7m1+eO2f1jKb5ZDIxrOPfpnGkg= +github.com/hashicorp/terraform-registry-address v0.3.0 h1:HMpK3nqaGFPS9VmgRXrJL/dzHNdheGVKk5k7VlFxzCo= +github.com/hashicorp/terraform-registry-address v0.3.0/go.mod h1:jRGCMiLaY9zii3GLC7hqpSnwhfnCN5yzvY0hh4iCGbM= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= @@ -152,16 +152,16 @@ github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6 github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= -go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= -go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= -go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= -go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= -go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= -go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= -go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= -go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= -go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= @@ -214,10 +214,10 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= -google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= -google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= +google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= diff --git a/helper/resource/testing.go b/helper/resource/testing.go index c3dd3bdc9..fda33bf7e 100644 --- a/helper/resource/testing.go +++ b/helper/resource/testing.go @@ -835,6 +835,9 @@ type TestStep struct { // for performing import testing where the prior TestStep configuration // contained a provider outside the one under test. ExternalProviders map[string]ExternalProvider + + // If true, the test step will run the query command + Query bool } // ConfigPlanChecks defines the different points in a Config TestStep when plan checks can be run. diff --git a/internal/plugintest/working_dir.go b/internal/plugintest/working_dir.go index d29425c32..29010e5e1 100644 --- a/internal/plugintest/working_dir.go +++ b/internal/plugintest/working_dir.go @@ -4,6 +4,7 @@ package plugintest import ( + "bytes" "context" "fmt" "io" @@ -21,6 +22,7 @@ import ( const ( ConfigFileName = "terraform_plugin_test.tf" PlanFileName = "tfplan" + QueryFileName = "terraform_plugin_test.tfquery.hcl" ) // WorkingDir represents a distinct working directory that can be used for @@ -37,6 +39,10 @@ type WorkingDir struct { // was stored; empty until SetConfig is called. configFilename string + // queryFilename is the full filename where the latest query configuration + // was stored; empty until SetQuery is called. + queryFilename string + // tf is the instance of tfexec.Terraform used for running Terraform commands tf *tfexec.Terraform @@ -101,7 +107,7 @@ func (wd *WorkingDir) SetConfig(ctx context.Context, cfg teststep.Config, vars c for _, file := range fi { if file.Mode().IsRegular() { - if filepath.Ext(file.Name()) == ".tf" || filepath.Ext(file.Name()) == ".json" { + if filepath.Ext(file.Name()) == ".tf" || filepath.Ext(file.Name()) == ".json" || filepath.Ext(file.Name()) == ".hcl" { err = os.Remove(filepath.Join(d.Name(), file.Name())) if err != nil && !os.IsNotExist(err) { @@ -151,6 +157,80 @@ func (wd *WorkingDir) SetConfig(ctx context.Context, cfg teststep.Config, vars c return nil } +// SetQuery sets a new query configuration for the working directory. +// +// This must be called at least once before any call to Init or Query Destroy +// to establish the query configuration. Any previously-set configuration is +// discarded and any saved plan is cleared. +func (wd *WorkingDir) SetQuery(ctx context.Context, cfg teststep.Config, vars config.Variables) error { + // Remove old config and variables files first + d, err := os.Open(wd.baseDir) + + if err != nil { + return err + } + + defer d.Close() + + fi, err := d.Readdir(-1) + + if err != nil { + return err + } + + for _, file := range fi { + if file.Mode().IsRegular() { + if filepath.Ext(file.Name()) == ".warioform" || filepath.Ext(file.Name()) == ".json" || filepath.Ext(file.Name()) == ".hcl" { + err = os.Remove(filepath.Join(d.Name(), file.Name())) + + if err != nil && !os.IsNotExist(err) { + return err + } + } + } + } + + logging.HelperResourceTrace(ctx, "Setting Terraform query configuration", map[string]any{logging.KeyTestTerraformConfiguration: cfg}) + + outFilename := filepath.Join(wd.baseDir, QueryFileName) + + // This file has to be written otherwise wd.Init() will return an error. + err = os.WriteFile(outFilename, nil, 0700) + + if err != nil { + return err + } + + // wd.configFilename must be set otherwise wd.Init() will return an error. + wd.queryFilename = outFilename + wd.configFilename = outFilename + + // Write configuration + if cfg != nil { + err = cfg.WriteQuery(ctx, wd.baseDir) + + if err != nil { + return err + } + } + + //Write configuration variables + err = vars.Write(wd.baseDir) + + if err != nil { + return err + } + + // Changing configuration invalidates any saved plan. + err = wd.ClearPlan(ctx) + + if err != nil { + return err + } + + return nil +} + // ClearState deletes any Terraform state present in the working directory. // // Any remote objects tracked by the state are not destroyed first, so this @@ -444,3 +524,21 @@ func (wd *WorkingDir) Schemas(ctx context.Context) (*tfjson.ProviderSchemas, err return providerSchemas, err } + +func (wd *WorkingDir) Query(ctx context.Context) ([]string, error) { + logging.HelperResourceTrace(ctx, "Calling Terraform CLI providers query command") + + // Query the provider using the Terraform CLI function + var buffer bytes.Buffer + err := wd.tf.QueryJSON(context.Background(), &buffer) + + if err != nil { + return nil, fmt.Errorf("error running terraform query command: %w", err) + } + + logging.HelperResourceTrace(ctx, "Called Terraform CLI providers query command") + + output := buffer.String() + + return []string{output}, nil +} diff --git a/internal/testing/testprovider/list_resource.go b/internal/testing/testprovider/list_resource.go new file mode 100644 index 000000000..f57c95a4a --- /dev/null +++ b/internal/testing/testprovider/list_resource.go @@ -0,0 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/list" +) + +var _ list.ListResource = ListResource{} + +type ListResource struct { + SchemaResponse *list.SchemaResponse + ListResultsStream *list.ListResultsStream + ValidateListConfigResponse *list.ValidateListConfigResponse +} + +func (r ListResource) Schema(ctx context.Context, req list.SchemaRequest, resp *list.SchemaResponse) { + if r.SchemaResponse != nil { + resp.Diagnostics = r.SchemaResponse.Diagnostics + resp.Schema = r.SchemaResponse.Schema + } +} +func (r ListResource) List(ctx context.Context, req list.ListRequest, stream *list.ListResultsStream) { + stream.Results = r.ListResultsStream.Results +} diff --git a/internal/testing/testprovider/provider.go b/internal/testing/testprovider/provider.go index 6e7f9d9a7..f30883bcc 100644 --- a/internal/testing/testprovider/provider.go +++ b/internal/testing/testprovider/provider.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/datasource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/list" "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/provider" "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" ) @@ -19,6 +20,7 @@ var _ provider.Provider = Provider{} type Provider struct { ConfigureResponse *provider.ConfigureResponse DataSources map[string]DataSource + ListResources map[string]ListResource Resources map[string]Resource SchemaResponse *provider.SchemaResponse StopResponse *provider.StopResponse @@ -41,6 +43,16 @@ func (p Provider) DataSourcesMap() map[string]datasource.DataSource { return datasources } +func (p Provider) ListResourcesMap() map[string]list.ListResource { + listResources := make(map[string]list.ListResource, len(p.ListResources)) + + for typeName, d := range p.ListResources { + listResources[typeName] = d + } + + return listResources +} + func (p Provider) ResourcesMap() map[string]resource.Resource { resources := make(map[string]resource.Resource, len(p.Resources)) diff --git a/internal/testing/testsdk/list/list_resource.go b/internal/testing/testsdk/list/list_resource.go new file mode 100644 index 000000000..fb5d7baf7 --- /dev/null +++ b/internal/testing/testsdk/list/list_resource.go @@ -0,0 +1,36 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package list + +import ( + "context" + "iter" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +type ListResource interface { + Schema(context.Context, SchemaRequest, *SchemaResponse) + List(context.Context, ListRequest, *ListResultsStream) +} + +type ListRequest struct { +} + +type ListResultsStream struct { + Results iter.Seq[ListResult] +} + +type ListResult struct { +} + +type ValidateListConfigResponse struct { +} + +type SchemaRequest struct{} + +type SchemaResponse struct { + Diagnostics []*tfprotov6.Diagnostic + Schema *tfprotov6.Schema +} diff --git a/internal/testing/testsdk/provider/provider.go b/internal/testing/testsdk/provider/provider.go index 82c65b9f4..4fcc6b4d3 100644 --- a/internal/testing/testsdk/provider/provider.go +++ b/internal/testing/testsdk/provider/provider.go @@ -9,12 +9,14 @@ import ( "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-go/tftypes" "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/datasource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/list" "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" ) type Provider interface { Configure(context.Context, ConfigureRequest, *ConfigureResponse) DataSourcesMap() map[string]datasource.DataSource + ListResourcesMap() map[string]list.ListResource ResourcesMap() map[string]resource.Resource Schema(context.Context, SchemaRequest, *SchemaResponse) Stop(context.Context, StopRequest, *StopResponse) diff --git a/internal/testing/testsdk/providerserver/list_resources.go b/internal/testing/testsdk/providerserver/list_resources.go new file mode 100644 index 000000000..cd31ef1e0 --- /dev/null +++ b/internal/testing/testsdk/providerserver/list_resources.go @@ -0,0 +1,24 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package providerserver + +import ( + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/list" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/provider" +) + +func ProviderListResource(p provider.Provider, typeName string) (list.ListResource, *tfprotov6.Diagnostic) { + r, ok := p.ListResourcesMap()[typeName] + + if !ok { + return nil, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Missing List Resource Type", + Detail: "The provider does not define the list resource type: " + typeName, + } + } + + return r, nil +} diff --git a/internal/testing/testsdk/providerserver/providerserver.go b/internal/testing/testsdk/providerserver/providerserver.go index 0855ff733..a55b5c5c1 100644 --- a/internal/testing/testsdk/providerserver/providerserver.go +++ b/internal/testing/testsdk/providerserver/providerserver.go @@ -12,12 +12,15 @@ import ( "github.com/hashicorp/terraform-plugin-go/tftypes" "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/datasource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/list" "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/provider" "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" ) var _ tfprotov6.ProviderServer = ProviderServer{} +// var _ tfprotov6.ProviderServerWithListResource = ProviderServer{} + // NewProviderServer returns a lightweight protocol version 6 provider server // for consumption with ProtoV6ProviderFactories. func NewProviderServer(p provider.Provider) func() (tfprotov6.ProviderServer, error) { @@ -86,6 +89,12 @@ func (s ProviderServer) GetMetadata(ctx context.Context, request *tfprotov6.GetM }) } + for typeName := range s.Provider.ListResourcesMap() { + resp.ListResources = append(resp.ListResources, tfprotov6.ListResourceMetadata{ + TypeName: typeName, + }) + } + for typeName := range s.Provider.ResourcesMap() { resp.Resources = append(resp.Resources, tfprotov6.ResourceMetadata{ TypeName: typeName, @@ -304,10 +313,11 @@ func (s ProviderServer) GetProviderSchema(ctx context.Context, req *tfprotov6.Ge Functions: map[string]*tfprotov6.Function{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Diagnostics: providerResp.Diagnostics, - Provider: providerResp.Schema, - ResourceSchemas: map[string]*tfprotov6.Schema{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Diagnostics: providerResp.Diagnostics, + ListResourceSchemas: map[string]*tfprotov6.Schema{}, + Provider: providerResp.Schema, + ResourceSchemas: map[string]*tfprotov6.Schema{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ PlanDestroy: true, }, @@ -324,6 +334,17 @@ func (s ProviderServer) GetProviderSchema(ctx context.Context, req *tfprotov6.Ge resp.DataSourceSchemas[typeName] = schemaResp.Schema } + for typeName, l := range s.Provider.ListResourcesMap() { + schemaReq := list.SchemaRequest{} + schemaResp := &list.SchemaResponse{} + + l.Schema(ctx, schemaReq, schemaResp) + + resp.Diagnostics = append(resp.Diagnostics, schemaResp.Diagnostics...) + + resp.ListResourceSchemas[typeName] = schemaResp.Schema + } + for typeName, r := range s.Provider.ResourcesMap() { schemaReq := resource.SchemaRequest{} schemaResp := &resource.SchemaResponse{} @@ -1017,3 +1038,47 @@ func (s ProviderServer) CloseEphemeralResource(ctx context.Context, req *tfproto func (s ProviderServer) ValidateEphemeralResourceConfig(ctx context.Context, req *tfprotov6.ValidateEphemeralResourceConfigRequest) (*tfprotov6.ValidateEphemeralResourceConfigResponse, error) { return &tfprotov6.ValidateEphemeralResourceConfigResponse{}, nil } + +func (s ProviderServer) ListResource(ctx context.Context, req *tfprotov6.ListResourceRequest) (*tfprotov6.ListResourceServerStream, error) { + resp := &tfprotov6.ListResourceServerStream{} + + // Copy over identity if it's supported + identitySchemaReq := resource.IdentitySchemaRequest{} + identitySchemaResp := &resource.IdentitySchemaResponse{} + + r, _ := ProviderResource(s.Provider, req.TypeName) + // TODO: diag + r.IdentitySchema(ctx, identitySchemaReq, identitySchemaResp) + + results := func(push func(tfprotov6.ListResourceResult) bool) { + _, diag := ProviderListResource(s.Provider, req.TypeName) + if diag != nil { + push(tfprotov6.ListResourceResult{Diagnostics: []*tfprotov6.Diagnostic{diag}}) + return + } + + identityData := tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + }, + ) + identity, _ := IdentityValuetoDynamicValue(identitySchemaResp.Schema, identityData) // TODO: diag + push(tfprotov6.ListResourceResult{ + Identity: &tfprotov6.ResourceIdentityData{ + IdentityData: identity, + }, + }) + } + + resp.Results = results + return resp, nil +} + +func (s ProviderServer) ValidateListResourceConfig(ctx context.Context, req *tfprotov6.ValidateListResourceConfigRequest) (*tfprotov6.ValidateListResourceConfigResponse, error) { + return &tfprotov6.ValidateListResourceConfigResponse{}, nil +} diff --git a/internal/testing/testsdk/resource/resource.go b/internal/testing/testsdk/resource/resource.go index 3fb3703ae..dc6412d55 100644 --- a/internal/testing/testsdk/resource/resource.go +++ b/internal/testing/testsdk/resource/resource.go @@ -5,7 +5,6 @@ package resource import ( "context" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-go/tftypes" ) diff --git a/internal/teststep/config.go b/internal/teststep/config.go index 91a708e26..5df477f08 100644 --- a/internal/teststep/config.go +++ b/internal/teststep/config.go @@ -16,8 +16,9 @@ import ( ) const ( - rawConfigFileName = "terraform_plugin_test.tf" - rawConfigFileNameJSON = rawConfigFileName + ".json" + rawConfigFileName = "terraform_plugin_test.tf" + rawConfigFileNameJSON = rawConfigFileName + ".json" + rawQueryConfigFileName = "terraform_plugin_test.tfquery.hcl" ) var ( @@ -46,6 +47,7 @@ type Config interface { HasTerraformBlock(context.Context) (bool, error) Write(context.Context, string) error Append(string) Config + WriteQuery(context.Context, string) error } // PrepareConfigurationRequest is used to simplify the generation of diff --git a/internal/teststep/directory.go b/internal/teststep/directory.go index 67ecc5ccd..1afc45a2d 100644 --- a/internal/teststep/directory.go +++ b/internal/teststep/directory.go @@ -75,6 +75,10 @@ func (c configurationDirectory) HasTerraformBlock(ctx context.Context) (bool, er return contains, nil } +func (c configurationDirectory) WriteQuery(ctx context.Context, dest string) error { + panic("WriteQuery not supported for configurationDirectory") +} + // Write copies all files from directory to destination. func (c configurationDirectory) Write(ctx context.Context, dest string) error { configDirectory := c.directory diff --git a/internal/teststep/file.go b/internal/teststep/file.go index 75ee6f7d6..99cefbd5b 100644 --- a/internal/teststep/file.go +++ b/internal/teststep/file.go @@ -71,6 +71,10 @@ func (c configurationFile) HasTerraformBlock(ctx context.Context) (bool, error) return contains, nil } +func (c configurationFile) WriteQuery(ctx context.Context, dest string) error { + panic("WriteQuery not supported for configurationDirectory") +} + // Write copies file from c.file to destination. func (c configurationFile) Write(ctx context.Context, dest string) error { configFile := c.file diff --git a/internal/teststep/string.go b/internal/teststep/string.go index 39028682a..ccb8fb3e8 100644 --- a/internal/teststep/string.go +++ b/internal/teststep/string.go @@ -61,6 +61,20 @@ func (c configurationString) Write(ctx context.Context, dest string) error { return nil } +// WriteQuery creates a file and writes c.raw into it. +func (c configurationString) WriteQuery(ctx context.Context, dest string) error { + outFilename := filepath.Join(dest, rawQueryConfigFileName) + + bCfg := []byte(c.raw) + + err := os.WriteFile(outFilename, bCfg, 0700) + if err != nil { + return err + } + + return nil +} + func (c configurationString) Append(config string) Config { return configurationString{ raw: strings.Join([]string{c.raw, config}, "\n"), diff --git a/tfversion/versions.go b/tfversion/versions.go index ffb625c8d..76130f650 100644 --- a/tfversion/versions.go +++ b/tfversion/versions.go @@ -39,4 +39,5 @@ var ( Version1_10_0 *version.Version = version.Must(version.NewVersion("1.10.0")) Version1_11_0 *version.Version = version.Must(version.NewVersion("1.11.0")) Version1_12_0 *version.Version = version.Must(version.NewVersion("1.12.0")) + Version1_13_0 *version.Version = version.Must(version.NewVersion("1.13.0")) ) From 99e9c884268c65ffd8d092e1df8f5c6c7c3b14bf Mon Sep 17 00:00:00 2001 From: Rain Date: Wed, 23 Jul 2025 14:03:45 -0400 Subject: [PATCH 02/45] Added helper/resource parts for query --- .../testdata/query/examplecloud_test.go | 118 ++++++++++++++++++ helper/resource/testdata/query/query_test.go | 63 ++++++++++ helper/resource/testdata/query/types_test.go | 66 ++++++++++ helper/resource/testing_new.go | 40 +++++- helper/resource/testing_new_config.go | 1 + 5 files changed, 287 insertions(+), 1 deletion(-) create mode 100644 helper/resource/testdata/query/examplecloud_test.go create mode 100644 helper/resource/testdata/query/query_test.go create mode 100644 helper/resource/testdata/query/types_test.go diff --git a/helper/resource/testdata/query/examplecloud_test.go b/helper/resource/testdata/query/examplecloud_test.go new file mode 100644 index 000000000..1cae8e95d --- /dev/null +++ b/helper/resource/testdata/query/examplecloud_test.go @@ -0,0 +1,118 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package query_test + +import ( + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" +) + +func examplecloudResource() testprovider.Resource { + return testprovider.Resource{ + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "somevalue"), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + }, + )), + }, + ReadResponse: &resource.ReadResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "somevalue"), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + }, + )), + }, + ImportStateResponse: &resource.ImportStateResponse{ + State: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "somevalue"), + }, + ), + Identity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + }, + )), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + ComputedStringAttribute("id"), + RequiredStringAttribute("location"), + RequiredStringAttribute("name"), + }, + }, + }, + }, + IdentitySchemaResponse: &resource.IdentitySchemaResponse{ + Schema: &tfprotov6.ResourceIdentitySchema{ + Version: 1, + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + RequiredForImport: true, + }, + }, + }, + }, + } +} diff --git a/helper/resource/testdata/query/query_test.go b/helper/resource/testdata/query/query_test.go new file mode 100644 index 000000000..e59e37768 --- /dev/null +++ b/helper/resource/testdata/query/query_test.go @@ -0,0 +1,63 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package query_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/list" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestQuery(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_13_0), // Query mode requires Terraform 1.13.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + ListResources: map[string]testprovider.ListResource{ + "examplecloud_containerette": { + SchemaResponse: &list.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + ComputedStringAttribute("id"), + }, + }, + }, + }, + ListResultsStream: &list.ListResultsStream{ + Results: func(push func(list.ListResult) bool) { + }, + }, + }, + }, + Resources: map[string]testprovider.Resource{ + "examplecloud_containerette": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + { + Query: true, + Config: ` + provider "examplecloud" {} + list "examplecloud_containerette" "test" { + provider = examplecloud + + config { + id = "bat" + } + }`, + }, + }, + }) +} diff --git a/helper/resource/testdata/query/types_test.go b/helper/resource/testdata/query/types_test.go new file mode 100644 index 000000000..7620d4d7d --- /dev/null +++ b/helper/resource/testdata/query/types_test.go @@ -0,0 +1,66 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package query_test + +import ( + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func RequiredBoolAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.Bool, + Required: true, + } +} + +func OptionalComputedListAttribute(name string, elementType tftypes.Type) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.List{ElementType: elementType}, + Optional: true, + Computed: true, + } +} + +func RequiredListAttribute(name string, elementType tftypes.Type) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.List{ElementType: elementType}, + Required: true, + } +} + +func RequiredNumberAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.Number, + Required: true, + } +} + +func ComputedStringAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.String, + Computed: true, + } +} + +func OptionalStringAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.String, + Optional: true, + } +} + +func RequiredStringAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.String, + Required: true, + } +} diff --git a/helper/resource/testing_new.go b/helper/resource/testing_new.go index 5ae7a5b4d..7e2158bbd 100644 --- a/helper/resource/testing_new.go +++ b/helper/resource/testing_new.go @@ -254,7 +254,10 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest testStepConfig = teststep.Configuration(confRequest) - err = wd.SetConfig(ctx, testStepConfig, step.ConfigVariables) + if !step.Query { + fmt.Println("Writing pre-switch configuration:", rawCfg) + err = wd.SetConfig(ctx, testStepConfig, step.ConfigVariables) + } if err != nil { logging.HelperResourceError(ctx, @@ -356,6 +359,39 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest continue } + if step.Query { + logging.HelperResourceTrace(ctx, "TestStep is Query mode") + + queryConfigRequest := teststep.ConfigurationRequest{ + Raw: &step.Config, + } + err := wd.SetQuery(ctx, teststep.Configuration(queryConfigRequest), step.ConfigVariables) + if err != nil { + t.Fatalf("Step %d/%d error setting query: %s", stepNumber, len(c.Steps), err) + } + + err = runProviderCommand(ctx, t, wd, providers, func() error { + return wd.Init(ctx) + }) + if err != nil { + t.Fatalf("Step %d/%d error running init: %s", stepNumber, len(c.Steps), err) + } + + var queryOut []string + err = runProviderCommand(ctx, t, wd, providers, func() error { + var err error + queryOut, err = wd.Query(ctx) + return err + }) + if err != nil { + fmt.Printf("Step %d/%d Query Output:\n%s\n", stepNumber, len(c.Steps), queryOut) + t.Fatalf("Step %d/%d error running query: %s", stepNumber, len(c.Steps), err) + } + + fmt.Printf("Step %d/%d Query Output:\n%s\n", stepNumber, len(c.Steps), queryOut) + continue + } + if cfg != nil { logging.HelperResourceTrace(ctx, "TestStep is Config mode") @@ -567,6 +603,8 @@ func testIDRefresh(ctx context.Context, t testing.T, c TestCase, wd *plugintest. testStepConfigDefer := teststep.Configuration(confRequest) + fmt.Println("Writing the reset to original configuration:", rawCfg) + err = wd.SetConfig(ctx, testStepConfigDefer, step.ConfigVariables) if err != nil { diff --git a/helper/resource/testing_new_config.go b/helper/resource/testing_new_config.go index babaf8410..7506e65f0 100644 --- a/helper/resource/testing_new_config.go +++ b/helper/resource/testing_new_config.go @@ -435,6 +435,7 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint // this fails. If refresh isn't read-only, then this will have // caught a different bug. if idRefreshCheck != nil { + fmt.Println("Not Writing by testing ID Refresh") if err := testIDRefresh(ctx, t, c, wd, step, idRefreshCheck, providers, stepIndex, helper); err != nil { return fmt.Errorf( "[ERROR] Test: ID-only test failed: %s", err) From 870fff03911b079100c48a166611daeb4e527634 Mon Sep 17 00:00:00 2001 From: Rain Date: Wed, 23 Jul 2025 14:12:10 -0400 Subject: [PATCH 03/45] Moved files --- helper/resource/{testdata => }/query/examplecloud_test.go | 0 helper/resource/{testdata => }/query/query_test.go | 0 helper/resource/{testdata => }/query/types_test.go | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename helper/resource/{testdata => }/query/examplecloud_test.go (100%) rename helper/resource/{testdata => }/query/query_test.go (100%) rename helper/resource/{testdata => }/query/types_test.go (100%) diff --git a/helper/resource/testdata/query/examplecloud_test.go b/helper/resource/query/examplecloud_test.go similarity index 100% rename from helper/resource/testdata/query/examplecloud_test.go rename to helper/resource/query/examplecloud_test.go diff --git a/helper/resource/testdata/query/query_test.go b/helper/resource/query/query_test.go similarity index 100% rename from helper/resource/testdata/query/query_test.go rename to helper/resource/query/query_test.go diff --git a/helper/resource/testdata/query/types_test.go b/helper/resource/query/types_test.go similarity index 100% rename from helper/resource/testdata/query/types_test.go rename to helper/resource/query/types_test.go From ede917cfdddd62de82e9a1524f8dab1fb4239165 Mon Sep 17 00:00:00 2001 From: Rain Date: Thu, 24 Jul 2025 14:36:15 -0400 Subject: [PATCH 04/45] But what if we comment out print statements? --- helper/resource/testing_new.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/helper/resource/testing_new.go b/helper/resource/testing_new.go index 7e2158bbd..6b000475d 100644 --- a/helper/resource/testing_new.go +++ b/helper/resource/testing_new.go @@ -255,7 +255,7 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest testStepConfig = teststep.Configuration(confRequest) if !step.Query { - fmt.Println("Writing pre-switch configuration:", rawCfg) + //fmt.Println("Writing pre-switch configuration:", rawCfg) err = wd.SetConfig(ctx, testStepConfig, step.ConfigVariables) } @@ -360,6 +360,7 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest } if step.Query { + print("yes, this is a query step\n") logging.HelperResourceTrace(ctx, "TestStep is Query mode") queryConfigRequest := teststep.ConfigurationRequest{ @@ -603,7 +604,7 @@ func testIDRefresh(ctx context.Context, t testing.T, c TestCase, wd *plugintest. testStepConfigDefer := teststep.Configuration(confRequest) - fmt.Println("Writing the reset to original configuration:", rawCfg) + //fmt.Println("Writing the reset to original configuration:", rawCfg) err = wd.SetConfig(ctx, testStepConfigDefer, step.ConfigVariables) From 06d257316d80138161c7216e6b370f9443f86457 Mon Sep 17 00:00:00 2001 From: Rain Date: Wed, 30 Jul 2025 12:50:25 -0400 Subject: [PATCH 05/45] What if we are more specific and say `.tfquery.hcl` --- helper/resource/testing_new.go | 1 - internal/plugintest/working_dir.go | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/helper/resource/testing_new.go b/helper/resource/testing_new.go index 6b000475d..1ff2c6eaf 100644 --- a/helper/resource/testing_new.go +++ b/helper/resource/testing_new.go @@ -360,7 +360,6 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest } if step.Query { - print("yes, this is a query step\n") logging.HelperResourceTrace(ctx, "TestStep is Query mode") queryConfigRequest := teststep.ConfigurationRequest{ diff --git a/internal/plugintest/working_dir.go b/internal/plugintest/working_dir.go index 29010e5e1..6b83de739 100644 --- a/internal/plugintest/working_dir.go +++ b/internal/plugintest/working_dir.go @@ -107,7 +107,7 @@ func (wd *WorkingDir) SetConfig(ctx context.Context, cfg teststep.Config, vars c for _, file := range fi { if file.Mode().IsRegular() { - if filepath.Ext(file.Name()) == ".tf" || filepath.Ext(file.Name()) == ".json" || filepath.Ext(file.Name()) == ".hcl" { + if filepath.Ext(file.Name()) == ".tf" || filepath.Ext(file.Name()) == ".json" || filepath.Ext(file.Name()) == ".tfquery.hcl" { err = os.Remove(filepath.Join(d.Name(), file.Name())) if err != nil && !os.IsNotExist(err) { From 236218d2d74684622406f63888fc91473791cfc1 Mon Sep 17 00:00:00 2001 From: Rain Date: Thu, 31 Jul 2025 10:44:50 -0400 Subject: [PATCH 06/45] Updating to version 1.14 for query_test.go --- helper/resource/query/query_test.go | 2 +- tfversion/versions.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/helper/resource/query/query_test.go b/helper/resource/query/query_test.go index e59e37768..5fe52e738 100644 --- a/helper/resource/query/query_test.go +++ b/helper/resource/query/query_test.go @@ -19,7 +19,7 @@ func TestQuery(t *testing.T) { r.UnitTest(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_13_0), // Query mode requires Terraform 1.13.0 or later + tfversion.SkipBelow(tfversion.Version1_14_0), // Query mode requires Terraform 1.13.0 or later }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ diff --git a/tfversion/versions.go b/tfversion/versions.go index 76130f650..dc5cc7dfe 100644 --- a/tfversion/versions.go +++ b/tfversion/versions.go @@ -40,4 +40,5 @@ var ( Version1_11_0 *version.Version = version.Must(version.NewVersion("1.11.0")) Version1_12_0 *version.Version = version.Must(version.NewVersion("1.12.0")) Version1_13_0 *version.Version = version.Must(version.NewVersion("1.13.0")) + Version1_14_0 *version.Version = version.Must(version.NewVersion("1.14.0")) ) From 49004cd0eaeb44737f353df4496a79429f54e045 Mon Sep 17 00:00:00 2001 From: Rain Date: Thu, 31 Jul 2025 13:55:11 -0400 Subject: [PATCH 07/45] Scaffolding for QueryCheck --- go.mod | 6 +- go.sum | 36 +- helper/resource/query/query_checks.go | 29 + helper/resource/query/query_checks_test.go | 22 + helper/resource/testing.go | 20 + internal/plugintest/working_dir.go | 2 + querycheck/compare_value.go | 114 + querycheck/compare_value_collection.go | 223 ++ querycheck/compare_value_collection_test.go | 1988 +++++++++++++++++ querycheck/compare_value_pairs.go | 111 + querycheck/compare_value_pairs_test.go | 142 ++ querycheck/compare_value_test.go | 241 ++ querycheck/doc.go | 5 + querycheck/expect_identity.go | 138 ++ querycheck/expect_identity_example_test.go | 41 + querycheck/expect_identity_test.go | 334 +++ querycheck/expect_identity_value.go | 91 + .../expect_identity_value_example_test.go | 40 + .../expect_identity_value_matches_query.go | 97 + ...ct_identity_value_matches_query_at_path.go | 106 + ...alue_matches_query_at_path_example_test.go | 42 + ...entity_value_matches_query_at_path_test.go | 344 +++ ...entity_value_matches_query_example_test.go | 38 + ...xpect_identity_value_matches_query_test.go | 337 +++ querycheck/expect_identity_value_test.go | 461 ++++ querycheck/expect_known_output_value.go | 76 + .../expect_known_output_value_at_path.go | 78 + ...known_output_value_at_path_example_test.go | 42 + .../expect_known_output_value_at_path_test.go | 1628 ++++++++++++++ .../expect_known_output_value_example_test.go | 40 + querycheck/expect_known_output_value_test.go | 1562 +++++++++++++ querycheck/expect_known_value.go | 84 + querycheck/expect_known_value_example_test.go | 38 + querycheck/expect_known_value_test.go | 1655 ++++++++++++++ querycheck/expect_sensitive_value.go | 101 + querycheck/expect_sensitive_value_test.go | 308 +++ querycheck/query_check.go | 30 + 37 files changed, 10629 insertions(+), 21 deletions(-) create mode 100644 helper/resource/query/query_checks.go create mode 100644 helper/resource/query/query_checks_test.go create mode 100644 querycheck/compare_value.go create mode 100644 querycheck/compare_value_collection.go create mode 100644 querycheck/compare_value_collection_test.go create mode 100644 querycheck/compare_value_pairs.go create mode 100644 querycheck/compare_value_pairs_test.go create mode 100644 querycheck/compare_value_test.go create mode 100644 querycheck/doc.go create mode 100644 querycheck/expect_identity.go create mode 100644 querycheck/expect_identity_example_test.go create mode 100644 querycheck/expect_identity_test.go create mode 100644 querycheck/expect_identity_value.go create mode 100644 querycheck/expect_identity_value_example_test.go create mode 100644 querycheck/expect_identity_value_matches_query.go create mode 100644 querycheck/expect_identity_value_matches_query_at_path.go create mode 100644 querycheck/expect_identity_value_matches_query_at_path_example_test.go create mode 100644 querycheck/expect_identity_value_matches_query_at_path_test.go create mode 100644 querycheck/expect_identity_value_matches_query_example_test.go create mode 100644 querycheck/expect_identity_value_matches_query_test.go create mode 100644 querycheck/expect_identity_value_test.go create mode 100644 querycheck/expect_known_output_value.go create mode 100644 querycheck/expect_known_output_value_at_path.go create mode 100644 querycheck/expect_known_output_value_at_path_example_test.go create mode 100644 querycheck/expect_known_output_value_at_path_test.go create mode 100644 querycheck/expect_known_output_value_example_test.go create mode 100644 querycheck/expect_known_output_value_test.go create mode 100644 querycheck/expect_known_value.go create mode 100644 querycheck/expect_known_value_example_test.go create mode 100644 querycheck/expect_known_value_test.go create mode 100644 querycheck/expect_sensitive_value.go create mode 100644 querycheck/expect_sensitive_value_test.go create mode 100644 querycheck/query_check.go diff --git a/go.mod b/go.mod index bc32ed306..e2b9abf9b 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/hashicorp/logutils v1.0.0 github.com/hashicorp/terraform-exec v0.23.1-0.20250717072919-061a850a52d2 github.com/hashicorp/terraform-json v0.25.0 - github.com/hashicorp/terraform-plugin-go v0.29.0-alpha.1 + github.com/hashicorp/terraform-plugin-go v0.29.0-beta.1 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0 github.com/mitchellh/go-testing-interface v1.14.1 @@ -56,7 +56,7 @@ require ( golang.org/x/text v0.27.0 // indirect golang.org/x/tools v0.34.0 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect - google.golang.org/grpc v1.73.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect + google.golang.org/grpc v1.74.2 // indirect google.golang.org/protobuf v1.36.6 // indirect ) diff --git a/go.sum b/go.sum index 5b943dc60..0b8071dd2 100644 --- a/go.sum +++ b/go.sum @@ -29,8 +29,8 @@ github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UN github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60= github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= @@ -80,8 +80,8 @@ github.com/hashicorp/terraform-exec v0.23.1-0.20250717072919-061a850a52d2 h1:90f github.com/hashicorp/terraform-exec v0.23.1-0.20250717072919-061a850a52d2/go.mod h1:8D3RLLpzAZdhT9jvALYz1KHyGU4OvI73I1o0+01QJxA= github.com/hashicorp/terraform-json v0.25.0 h1:rmNqc/CIfcWawGiwXmRuiXJKEiJu1ntGoxseG1hLhoQ= github.com/hashicorp/terraform-json v0.25.0/go.mod h1:sMKS8fiRDX4rVlR6EJUMudg1WcanxCMoWwTLkgZP/vc= -github.com/hashicorp/terraform-plugin-go v0.29.0-alpha.1 h1:ZId6oWG8VTKhz207quE/Xh8a3HuoLtM/QkcSSypekIQ= -github.com/hashicorp/terraform-plugin-go v0.29.0-alpha.1/go.mod h1:hL//wLEfYo0YVt0TC/VLzia/ADQQto3HEm4/jX2gkdY= +github.com/hashicorp/terraform-plugin-go v0.29.0-beta.1 h1:xeHlRQYev3iMXwX2W7+D1bSfLRBs9jojZXqE6hmNxMI= +github.com/hashicorp/terraform-plugin-go v0.29.0-beta.1/go.mod h1:5pww/UULn9C2tItq6o5sbScEkJxBUt9X9kI4DkeRsIw= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0 h1:NFPMacTrY/IdcIcnUB+7hsore1ZaRWU9cnB6jFoBnIM= @@ -152,16 +152,16 @@ github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6 github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= -go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= -go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= -go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= -go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= -go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= -go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= -go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= -go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= -go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= +go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= +go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= +go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= +go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= +go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= +go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= +go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= @@ -214,10 +214,10 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= -google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= +google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= diff --git a/helper/resource/query/query_checks.go b/helper/resource/query/query_checks.go new file mode 100644 index 000000000..6ff4eca42 --- /dev/null +++ b/helper/resource/query/query_checks.go @@ -0,0 +1,29 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package query + +import ( + "context" + "errors" + + tfjson "github.com/hashicorp/terraform-json" + "github.com/mitchellh/go-testing-interface" + + "github.com/hashicorp/terraform-plugin-testing/querycheck" +) + +func runQueryChecks(ctx context.Context, t testing.T, query *tfjson.Query, queryChecks []querycheck.QueryCheck) error { + t.Helper() + + var result []error + + for _, queryCheck := range queryChecks { + resp := querycheck.CheckQueryResponse{} + queryCheck.CheckQuery(ctx, querycheck.CheckQueryRequest{Query: query}, &resp) + + result = append(result, resp.Error) + } + + return errors.Join(result...) +} diff --git a/helper/resource/query/query_checks_test.go b/helper/resource/query/query_checks_test.go new file mode 100644 index 000000000..ee9242c97 --- /dev/null +++ b/helper/resource/query/query_checks_test.go @@ -0,0 +1,22 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package query + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-testing/querycheck" +) + +var _ querycheck.QueryCheck = &queryCheckSpy{} + +type queryCheckSpy struct { + err error + called bool +} + +func (s *queryCheckSpy) CheckQuery(ctx context.Context, req querycheck.CheckQueryRequest, resp *querycheck.CheckQueryResponse) { + s.called = true + resp.Error = s.err +} diff --git a/helper/resource/testing.go b/helper/resource/testing.go index fda33bf7e..162c17262 100644 --- a/helper/resource/testing.go +++ b/helper/resource/testing.go @@ -8,6 +8,7 @@ import ( "errors" "flag" "fmt" + "github.com/hashicorp/terraform-plugin-testing/querycheck" "log" "os" "regexp" @@ -640,6 +641,10 @@ type TestStep struct { // Custom state checks can be created by implementing the [statecheck.StateCheck] interface, or by using a StateCheck implementation from the provided [statecheck] package. ConfigStateChecks []statecheck.StateCheck + // ConfigQueryChecks allow assertions to be made against the query file during a Config test using a query check. + // Custom query checks can be created by implementing the [querycheck.QueryCheck] interface, or by using a QueryCheck implementation from the provided [querycheck] package. + ConfigQueryChecks []querycheck.QueryCheck + // PlanOnly can be set to only run `plan` with this configuration, and not // actually apply it. This is useful for ensuring config changes result in // no-op plans @@ -855,6 +860,21 @@ type ConfigPlanChecks struct { PostApplyPostRefresh []plancheck.PlanCheck } +// ConfigQueryChecks defines the different points in a Config TestStep when query checks can be run. +type ConfigQueryChecks struct { + // PreApply runs all query checks in the slice. This occurs before the apply of a Config test is run. This slice cannot be populated + // with TestStep.QueryOnly, as there is no PreApply query run with that flag set. All errors by query checks in this slice are aggregated, reported, and will result in a test failure. + PreApply []querycheck.QueryCheck + + // PostApplyPreRefresh runs all query checks in the slice. This occurs after the apply and before the refresh of a Config test is run. + // All errors by query checks in this slice are aggregated, reported, and will result in a test failure. + PostApplyPreRefresh []querycheck.QueryCheck + + // PostApplyPostRefresh runs all query checks in the slice. This occurs after the apply and refresh of a Config test are run. + // All errors by query checks in this slice are aggregated, reported, and will result in a test failure. + PostApplyPostRefresh []querycheck.QueryCheck +} + // ImportPlanChecks defines the different points in an Import TestStep when plan checks can be run. type ImportPlanChecks struct { // PreApply runs all plan checks in the slice. This occurs after the plan of an Import test is computed. This slice cannot be populated diff --git a/internal/plugintest/working_dir.go b/internal/plugintest/working_dir.go index 6b83de739..0af7d640e 100644 --- a/internal/plugintest/working_dir.go +++ b/internal/plugintest/working_dir.go @@ -532,6 +532,8 @@ func (wd *WorkingDir) Query(ctx context.Context) ([]string, error) { var buffer bytes.Buffer err := wd.tf.QueryJSON(context.Background(), &buffer) + // Marshall buffer? JSON.mashallto___ terraform-json.Query + if err != nil { return nil, fmt.Errorf("error running terraform query command: %w", err) } diff --git a/querycheck/compare_value.go b/querycheck/compare_value.go new file mode 100644 index 000000000..35f6777f4 --- /dev/null +++ b/querycheck/compare_value.go @@ -0,0 +1,114 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "fmt" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/compare" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +// Resource Query Check +var _ QueryCheck = &compareValue{} + +type compareValue struct { + resourceAddresses []string + attributePaths []tfjsonpath.Path + queryValues []any + comparer compare.ValueComparer +} + +func (e *compareValue) AddQueryValue(resourceAddress string, attributePath tfjsonpath.Path) QueryCheck { + e.resourceAddresses = append(e.resourceAddresses, resourceAddress) + e.attributePaths = append(e.attributePaths, attributePath) + + return e +} + +// CheckQuery implements the query check logic. +func (e *compareValue) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { + var resource *tfjson.QueryResource + + if req.Query == nil { + resp.Error = fmt.Errorf("query is nil") + + return + } + + if req.Query.Values == nil { + resp.Error = fmt.Errorf("query does not contain any query values") + + return + } + + if req.Query.Values.RootModule == nil { + resp.Error = fmt.Errorf("query does not contain a root module") + + return + } + + // All calls to AddQueryValue occur before any TestStep is run, populating the resourceAddresses + // and attributePaths slices. The queryValues slice is populated during execution of each TestStep. + // Each call to CheckQuery happens sequentially during each TestStep. + // The currentIndex is reflective of the current query value being checked. + currentIndex := len(e.queryValues) + + if len(e.resourceAddresses) <= currentIndex { + resp.Error = fmt.Errorf("resource addresses index out of bounds: %d", currentIndex) + + return + } + + resourceAddress := e.resourceAddresses[currentIndex] + + for _, r := range req.Query.Values.RootModule.Resources { + if resourceAddress == r.Address { + resource = r + + break + } + } + + if resource == nil { + resp.Error = fmt.Errorf("%s - Resource not found in query", resourceAddress) + + return + } + + if len(e.attributePaths) <= currentIndex { + resp.Error = fmt.Errorf("attribute paths index out of bounds: %d", currentIndex) + + return + } + + attributePath := e.attributePaths[currentIndex] + + result, err := tfjsonpath.Traverse(resource.AttributeValues, attributePath) + + if err != nil { + resp.Error = err + + return + } + + e.queryValues = append(e.queryValues, result) + + err = e.comparer.CompareValues(e.queryValues...) + + if err != nil { + resp.Error = err + } +} + +// CompareValue returns a query check that compares values retrieved from query using the +// supplied value comparer. +func CompareValue(comparer compare.ValueComparer) *compareValue { + return &compareValue{ + comparer: comparer, + } +} diff --git a/querycheck/compare_value_collection.go b/querycheck/compare_value_collection.go new file mode 100644 index 000000000..fd3a3b3b2 --- /dev/null +++ b/querycheck/compare_value_collection.go @@ -0,0 +1,223 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "errors" + "fmt" + "sort" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/compare" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +// Resource Query Check +var _ QueryCheck = &compareValueCollection{} + +type compareValueCollection struct { + resourceAddressOne string + collectionPath []tfjsonpath.Path + resourceAddressTwo string + attributePath tfjsonpath.Path + comparer compare.ValueComparer +} + +func walkCollectionPath(obj any, paths []tfjsonpath.Path, results []any) ([]any, error) { + switch t := obj.(type) { + case []any: + for _, v := range t { + if len(paths) == 0 { + results = append(results, v) + continue + } + + x, err := tfjsonpath.Traverse(v, paths[0]) + + if err != nil { + return results, err + } + + results, err = walkCollectionPath(x, paths[1:], results) + + if err != nil { + return results, err + } + } + case map[string]any: + keys := make([]string, 0, len(t)) + + for k := range t { + keys = append(keys, k) + } + + sort.Strings(keys) + + for _, key := range keys { + if len(paths) == 0 { + results = append(results, t[key]) + continue + } + + x, err := tfjsonpath.Traverse(t, paths[0]) + + if err != nil { + return results, err + } + + results, err = walkCollectionPath(x, paths[1:], results) + + if err != nil { + return results, err + } + } + default: + results = append(results, obj) + } + + return results, nil +} + +// CheckQuery implements the query check logic. +func (e *compareValueCollection) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { + var resourceOne *tfjson.QueryResource + var resourceTwo *tfjson.QueryResource + + if req.Query == nil { + resp.Error = fmt.Errorf("query is nil") + + return + } + + if req.Query.Values == nil { + resp.Error = fmt.Errorf("query does not contain any query values") + + return + } + + if req.Query.Values.RootModule == nil { + resp.Error = fmt.Errorf("query does not contain a root module") + + return + } + + for _, r := range req.Query.Values.RootModule.Resources { + if e.resourceAddressOne == r.Address { + resourceOne = r + + break + } + } + + if resourceOne == nil { + resp.Error = fmt.Errorf("%s - Resource not found in query", e.resourceAddressOne) + + return + } + + if len(e.collectionPath) == 0 { + resp.Error = fmt.Errorf("%s - No collection path was provided", e.resourceAddressOne) + + return + } + + resultOne, err := tfjsonpath.Traverse(resourceOne.AttributeValues, e.collectionPath[0]) + + if err != nil { + resp.Error = err + + return + } + + // Verify resultOne is a collection. + switch t := resultOne.(type) { + case []any, map[string]any: + // Collection found. + default: + var pathStr string + + for _, v := range e.collectionPath { + pathStr += fmt.Sprintf(".%s", v.String()) + } + + resp.Error = fmt.Errorf("%s%s is not a collection type: %T", e.resourceAddressOne, pathStr, t) + + return + } + + var results []any + + results, err = walkCollectionPath(resultOne, e.collectionPath[1:], results) + + if err != nil { + resp.Error = err + + return + } + + for _, r := range req.Query.Values.RootModule.Resources { + if e.resourceAddressTwo == r.Address { + resourceTwo = r + + break + } + } + + if resourceTwo == nil { + resp.Error = fmt.Errorf("%s - Resource not found in query", e.resourceAddressTwo) + + return + } + + resultTwo, err := tfjsonpath.Traverse(resourceTwo.AttributeValues, e.attributePath) + + if err != nil { + resp.Error = err + + return + } + + var errs []error + + for _, v := range results { + switch resultTwo.(type) { + case []any: + errs = append(errs, e.comparer.CompareValues([]any{v}, resultTwo)) + default: + errs = append(errs, e.comparer.CompareValues(v, resultTwo)) + } + } + + for _, err = range errs { + if err == nil { + return + } + } + + errMsgs := map[string]struct{}{} + + for _, err = range errs { + if _, ok := errMsgs[err.Error()]; ok { + continue + } + + resp.Error = errors.Join(resp.Error, err) + + errMsgs[err.Error()] = struct{}{} + } +} + +// CompareValueCollection returns a query check that iterates over each element in a collection and compares the value of each element +// with the value of an attribute using the given value comparer. +func CompareValueCollection(resourceAddressOne string, collectionPath []tfjsonpath.Path, resourceAddressTwo string, attributePath tfjsonpath.Path, comparer compare.ValueComparer) QueryCheck { + return &compareValueCollection{ + resourceAddressOne: resourceAddressOne, + collectionPath: collectionPath, + resourceAddressTwo: resourceAddressTwo, + attributePath: attributePath, + comparer: comparer, + } +} diff --git a/querycheck/compare_value_collection_test.go b/querycheck/compare_value_collection_test.go new file mode 100644 index 000000000..3fdb8095f --- /dev/null +++ b/querycheck/compare_value_collection_test.go @@ -0,0 +1,1988 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck_test + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-plugin-testing/compare" + 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/internal/testing/testsdk/resource" + "github.com/hashicorp/terraform-plugin-testing/querycheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestCompareValueCollection_CheckQuery_Bool_Error_NotCollection(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + resource "test_resource" "two" { + bool_attribute = true + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("bool_attribute"), + }, + "test_resource.one", + tfjsonpath.New("bool_attribute"), + compare.ValuesSame(), + ), + }, + ExpectError: regexp.MustCompile("test_resource.two.bool_attribute is not a collection type: bool"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_Float_Error_NotCollection(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + float_attribute = 1.234 + } + + resource "test_resource" "two" { + float_attribute = 1.234 + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("float_attribute"), + }, + "test_resource.one", + tfjsonpath.New("float_attribute"), + compare.ValuesSame(), + ), + }, + ExpectError: regexp.MustCompile("test_resource.two.float_attribute is not a collection type: json.Number"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_Int_Error_NotCollection(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 1234 + } + + resource "test_resource" "two" { + int_attribute = 1234 + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("int_attribute"), + }, + "test_resource.one", + tfjsonpath.New("int_attribute"), + compare.ValuesSame(), + ), + }, + ExpectError: regexp.MustCompile("test_resource.two.int_attribute is not a collection type: json.Number"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_List_ValuesSame_ErrorDiffer(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + list_attribute = [ + "str2", + "str3", + ] + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("list_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesSame(), + ), + }, + ExpectError: regexp.MustCompile("expected values to be the same, but they differ: str2 != str\nexpected values to be the same, but they differ: str3 != str"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_EmptyCollectionPath(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + list_attribute = [ + "str2", + "str", + ] + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + // Empty path is invalid + []tfjsonpath.Path{}, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesSame(), + ), + }, + ExpectError: regexp.MustCompile("test_resource.two - No collection path was provided"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_List_ValuesSame(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + list_attribute = [ + "str2", + "str", + ] + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("list_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesSame(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_List_ValuesDiffer_ErrorSame(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + list_attribute = [ + "str", + "str", + ] + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("list_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesDiffer(), + ), + }, + ExpectError: regexp.MustCompile("expected values to differ, but they are the same: str == str"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_List_ValuesDiffer(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + list_attribute = [ + "str", + "str2", + ] + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("list_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesDiffer(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_ListNestedBlock_ValuesSame_ErrorDiffer(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + list_nested_block { + list_nested_block_attribute = "str2" + } + list_nested_block { + list_nested_block_attribute = "str3" + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("list_nested_block"), + tfjsonpath.New("list_nested_block_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesSame(), + ), + }, + ExpectError: regexp.MustCompile("expected values to be the same, but they differ: str2 != str\nexpected values to be the same, but they differ: str3 != str"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_ListNestedBlock_ValuesSame(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + list_nested_block { + list_nested_block_attribute = "str2" + } + list_nested_block { + list_nested_block_attribute = "str" + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("list_nested_block"), + tfjsonpath.New("list_nested_block_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesSame(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_ListNestedBlock_ValuesDiffer_ErrorSame(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "str" + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("list_nested_block"), + tfjsonpath.New("list_nested_block_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesDiffer(), + ), + }, + ExpectError: regexp.MustCompile("expected values to differ, but they are the same: str == str"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_ListNestedBlock_ValuesDiffer(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + list_nested_block { + list_nested_block_attribute = "str2" + } + list_nested_block { + list_nested_block_attribute = "str3" + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("list_nested_block"), + tfjsonpath.New("list_nested_block_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesDiffer(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_Map_ValuesSame_ErrorDiffer(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + map_attribute = { + "a": "str2", + "b": "str3", + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("map_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesSame(), + ), + }, + ExpectError: regexp.MustCompile("expected values to be the same, but they differ: str2 != str\nexpected values to be the same, but they differ: str3 != str"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_Map_ValuesSame(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + map_attribute = { + "a": "str2", + "b": "str", + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("map_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesSame(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_Map_ValuesDiffer_ErrorSame(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + map_attribute = { + "a": "str", + "b": "str", + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("map_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesDiffer(), + ), + }, + ExpectError: regexp.MustCompile("expected values to differ, but they are the same: str == str"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_Map_ValuesDiffer(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + map_attribute = { + "a": "str", + "b": "str2", + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("map_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesDiffer(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_Set_ValuesSame_ErrorDiffer(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + set_attribute = [ + "str2", + "str3" + ] + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesSame(), + ), + }, + ExpectError: regexp.MustCompile("expected values to be the same, but they differ: str2 != str\nexpected values to be the same, but they differ: str3 != str"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_Set_ValuesSame(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + set_attribute = [ + "str2", + "str" + ] + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesSame(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_Set_ValuesDiffer_ErrorSame(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + set_attribute = [ + "str", + "str", + ] + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesDiffer(), + ), + }, + ExpectError: regexp.MustCompile("expected values to differ, but they are the same: str == str"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_Set_ValuesDiffer(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + set_attribute = [ + "str", + "str2" + ] + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesDiffer(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_SetNestedBlock_ValuesSame_ErrorDiffer(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + set_nested_block { + set_nested_block_attribute = "str2" + } + set_nested_block { + set_nested_block_attribute = "str3" + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_nested_block"), + tfjsonpath.New("set_nested_block_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesSame(), + ), + }, + ExpectError: regexp.MustCompile("expected values to be the same, but they differ: str2 != str\nexpected values to be the same, but they differ: str3 != str"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_SetNestedBlock_ValuesSame(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + set_nested_block { + set_nested_block_attribute = "str2" + } + set_nested_block { + set_nested_block_attribute = "str" + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_nested_block"), + tfjsonpath.New("set_nested_block_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesSame(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_SetNestedBlock_ValuesDiffer_ErrorSame(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + set_nested_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "str" + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_nested_block"), + tfjsonpath.New("set_nested_block_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesDiffer(), + ), + }, + ExpectError: regexp.MustCompile("expected values to differ, but they are the same: str == str"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_SetNestedBlock_ValuesDiffer(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + set_nested_block { + set_nested_block_attribute = "str2" + } + set_nested_block { + set_nested_block_attribute = "str3" + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_nested_block"), + tfjsonpath.New("set_nested_block_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesDiffer(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_SetNestedNestedBlock_ValuesDiffer_ErrorSameAttribute(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str" + } + } + } + + resource "test_resource" "two" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str" + } + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_nested_nested_block"), + tfjsonpath.New("set_nested_block"), + tfjsonpath.New("set_nested_block_attribute"), + }, + "test_resource.one", + tfjsonpath.New("set_nested_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block_attribute"), + compare.ValuesDiffer(), + ), + }, + ExpectError: regexp.MustCompile("expected values to differ, but they are the same: str == str"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_SetNestedNestedBlock_ValuesDiffer_ErrorSameNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str" + } + } + } + + resource "test_resource" "two" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str" + } + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_nested_nested_block"), + tfjsonpath.New("set_nested_block"), + }, + "test_resource.one", + tfjsonpath.New("set_nested_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block").AtSliceIndex(0), + compare.ValuesDiffer(), + ), + }, + ExpectError: regexp.MustCompile(`expected values to differ, but they are the same: map\[set_nested_block_attribute:str\] == map\[set_nested_block_attribute:str\]`), + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_SetNestedNestedBlock_ValuesDiffer_ErrorSameNestedNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str" + } + } + } + + resource "test_resource" "two" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str" + } + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_nested_nested_block"), + }, + "test_resource.one", + tfjsonpath.New("set_nested_nested_block"), + compare.ValuesDiffer(), + ), + }, + ExpectError: regexp.MustCompile(`expected values to differ, but they are the same: \[map\[set_nested_block:\[map\[set_nested_block_attribute:str\]\]\]\] == \[map\[set_nested_block:\[map\[set_nested_block_attribute:str\]\]\]\]`), + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_SetNestedNestedBlock_ValuesDifferAttribute(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str1" + } + set_nested_block { + set_nested_block_attribute = "str2" + } + } + } + + resource "test_resource" "two" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str3" + } + set_nested_block { + set_nested_block_attribute = "str4" + } + } + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str5" + } + set_nested_block { + set_nested_block_attribute = "str6" + } + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_nested_nested_block"), + tfjsonpath.New("set_nested_block"), + tfjsonpath.New("set_nested_block_attribute"), + }, + "test_resource.one", + tfjsonpath.New("set_nested_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block_attribute"), + compare.ValuesDiffer(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_SetNestedNestedBlock_ValuesDifferNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str1" + } + set_nested_block { + set_nested_block_attribute = "str2" + } + } + } + + resource "test_resource" "two" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str3" + } + set_nested_block { + set_nested_block_attribute = "str4" + } + } + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str5" + } + set_nested_block { + set_nested_block_attribute = "str6" + } + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_nested_nested_block"), + tfjsonpath.New("set_nested_block"), + }, + "test_resource.one", + tfjsonpath.New("set_nested_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block").AtSliceIndex(0), + compare.ValuesDiffer(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_SetNestedNestedBlock_ValuesDifferNestedNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str1" + } + set_nested_block { + set_nested_block_attribute = "str2" + } + } + } + + resource "test_resource" "two" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str3" + } + set_nested_block { + set_nested_block_attribute = "str4" + } + } + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str5" + } + set_nested_block { + set_nested_block_attribute = "str6" + } + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_nested_nested_block"), + }, + "test_resource.one", + tfjsonpath.New("set_nested_nested_block"), + compare.ValuesDiffer(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_SetNested_ValuesSame_ErrorAttribute(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_a" + } + set_nested_block { + set_nested_block_attribute = "str_b" + } + } + } + + resource "test_resource" "two" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_c" + } + set_nested_block { + set_nested_block_attribute = "str_d" + } + } + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_e" + } + set_nested_block { + set_nested_block_attribute = "str_f" + } + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_nested_nested_block"), + tfjsonpath.New("set_nested_block"), + tfjsonpath.New("set_nested_block_attribute"), + }, + "test_resource.one", + tfjsonpath.New("set_nested_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block_attribute"), + compare.ValuesSame(), + ), + }, + ExpectError: regexp.MustCompile("expected values to be the same, but they differ: str_c != str_a\nexpected values to be the same, but they differ: str_d != str_a\nexpected values to be the same, but they differ: str_e != str_a\nexpected values to be the same, but they differ: str_f != str_a"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_SetNested_ValuesSame_ErrorNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_a" + } + set_nested_block { + set_nested_block_attribute = "str_b" + } + } + } + + resource "test_resource" "two" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_c" + } + set_nested_block { + set_nested_block_attribute = "str_d" + } + } + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_e" + } + set_nested_block { + set_nested_block_attribute = "str_f" + } + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_nested_nested_block"), + tfjsonpath.New("set_nested_block"), + }, + "test_resource.one", + tfjsonpath.New("set_nested_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block").AtSliceIndex(0), + compare.ValuesSame(), + ), + }, + ExpectError: regexp.MustCompile(`expected values to be the same, but they differ: map\[set_nested_block_attribute:str_c\] != map\[set_nested_block_attribute:str_a\]\nexpected values to be the same, but they differ: map\[set_nested_block_attribute:str_d\] != map\[set_nested_block_attribute:str_a\]\nexpected values to be the same, but they differ: map\[set_nested_block_attribute:str_e\] != map\[set_nested_block_attribute:str_a\]\nexpected values to be the same, but they differ: map\[set_nested_block_attribute:str_f\] != map\[set_nested_block_attribute:str_a\]`), + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_SetNested_ValuesSame_ErrorNestedNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_a" + } + set_nested_block { + set_nested_block_attribute = "str_b" + } + } + } + + resource "test_resource" "two" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_c" + } + set_nested_block { + set_nested_block_attribute = "str_d" + } + } + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_e" + } + set_nested_block { + set_nested_block_attribute = "str_f" + } + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_nested_nested_block"), + }, + "test_resource.one", + tfjsonpath.New("set_nested_nested_block"), + compare.ValuesSame(), + ), + }, + ExpectError: regexp.MustCompile(`expected values to be the same, but they differ: \[map\[set_nested_block:\[map\[set_nested_block_attribute:str_c\] map\[set_nested_block_attribute:str_d\]\]\]\] != \[map\[set_nested_block:\[map\[set_nested_block_attribute:str_a\] map\[set_nested_block_attribute:str_b\]\]\]\]\nexpected values to be the same, but they differ: \[map\[set_nested_block:\[map\[set_nested_block_attribute:str_e\] map\[set_nested_block_attribute:str_f\]\]\]\] != \[map\[set_nested_block:\[map\[set_nested_block_attribute:str_a\] map\[set_nested_block_attribute:str_b\]\]\]\]`), + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_SetNested_ValuesSameAttribute(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_a" + } + set_nested_block { + set_nested_block_attribute = "str_b" + } + } + } + + resource "test_resource" "two" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_c" + } + set_nested_block { + set_nested_block_attribute = "str_d" + } + } + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_a" + } + set_nested_block { + set_nested_block_attribute = "str_b" + } + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_nested_nested_block"), + tfjsonpath.New("set_nested_block"), + tfjsonpath.New("set_nested_block_attribute"), + }, + "test_resource.one", + tfjsonpath.New("set_nested_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block_attribute"), + compare.ValuesSame(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_SetNested_ValuesSameNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_a" + } + set_nested_block { + set_nested_block_attribute = "str_b" + } + } + } + + resource "test_resource" "two" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_c" + } + set_nested_block { + set_nested_block_attribute = "str_d" + } + } + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_a" + } + set_nested_block { + set_nested_block_attribute = "str_b" + } + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_nested_nested_block"), + tfjsonpath.New("set_nested_block"), + }, + "test_resource.one", + tfjsonpath.New("set_nested_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block").AtSliceIndex(0), + compare.ValuesSame(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_SetNested_ValuesSameNestedNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_a" + } + set_nested_block { + set_nested_block_attribute = "str_b" + } + } + } + + resource "test_resource" "two" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_c" + } + set_nested_block { + set_nested_block_attribute = "str_d" + } + } + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_a" + } + set_nested_block { + set_nested_block_attribute = "str_b" + } + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_nested_nested_block"), + }, + "test_resource.one", + tfjsonpath.New("set_nested_nested_block"), + compare.ValuesSame(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_String_Error_NotCollection(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + string_attribute = "str" + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("string_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesSame(), + ), + }, + ExpectError: regexp.MustCompile("test_resource.two.string_attribute is not a collection type: string"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_ListNestedAttribute_ValuesSame(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // Nested attributes only available in protocol version 6 + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "str_attr", + Type: tftypes.String, + Optional: true, + }, + { + Name: "nested_attr", + NestedType: &tfprotov6.SchemaObject{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "str_attr", + Type: tftypes.String, + Optional: true, + }, + }, + Nesting: tfprotov6.SchemaObjectNestingModeList, + }, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + str_attr = "str2" + } + resource "test_resource" "two" { + nested_attr = [ + { + str_attr = "str1" + }, + { + str_attr = "str2" + } + ] + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("nested_attr"), + tfjsonpath.New("str_attr"), + }, + "test_resource.one", + tfjsonpath.New("str_attr"), + compare.ValuesSame(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_ListNestedAttribute_ValuesSame_ErrorDiff(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // Nested attributes only available in protocol version 6 + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "str_attr", + Type: tftypes.String, + Optional: true, + }, + { + Name: "nested_attr", + NestedType: &tfprotov6.SchemaObject{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "str_attr", + Type: tftypes.String, + Optional: true, + }, + }, + Nesting: tfprotov6.SchemaObjectNestingModeList, + }, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + str_attr = "str1" + } + resource "test_resource" "two" { + nested_attr = [ + { + str_attr = "str2" + }, + { + str_attr = "str3" + } + ] + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("nested_attr"), + tfjsonpath.New("str_attr"), + }, + "test_resource.one", + tfjsonpath.New("str_attr"), + compare.ValuesSame(), + ), + }, + ExpectError: regexp.MustCompile("expected values to be the same, but they differ: str2 != str1\nexpected values to be the same, but they differ: str3 != str1"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_DoubleListNestedAttribute_ValuesSame(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // Nested attributes only available in protocol version 6 + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "str_attr", + Type: tftypes.String, + Optional: true, + }, + { + Name: "nested_attr", + NestedType: &tfprotov6.SchemaObject{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "double_nested_attr", + NestedType: &tfprotov6.SchemaObject{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "str_attr", + Type: tftypes.String, + Optional: true, + }, + }, + Nesting: tfprotov6.SchemaObjectNestingModeSingle, + }, + Optional: true, + }, + }, + Nesting: tfprotov6.SchemaObjectNestingModeList, + }, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + str_attr = "str2" + } + resource "test_resource" "two" { + nested_attr = [ + { + double_nested_attr = { + str_attr = "str1" + } + }, + { + double_nested_attr = { + str_attr = "str2" + } + } + ] + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("nested_attr"), + tfjsonpath.New("double_nested_attr"), + tfjsonpath.New("str_attr"), + }, + "test_resource.one", + tfjsonpath.New("str_attr"), + compare.ValuesSame(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckQuery_DoubleListNestedAttribute_ValuesSame_ErrorDiff(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // Nested attributes only available in protocol version 6 + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "str_attr", + Type: tftypes.String, + Optional: true, + }, + { + Name: "nested_attr", + NestedType: &tfprotov6.SchemaObject{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "double_nested_attr", + NestedType: &tfprotov6.SchemaObject{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "str_attr", + Type: tftypes.String, + Optional: true, + }, + }, + Nesting: tfprotov6.SchemaObjectNestingModeSingle, + }, + Optional: true, + }, + }, + Nesting: tfprotov6.SchemaObjectNestingModeList, + }, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + str_attr = "str1" + } + resource "test_resource" "two" { + nested_attr = [ + { + double_nested_attr = { + str_attr = "str2" + } + }, + { + double_nested_attr = { + str_attr = "str3" + } + } + ] + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("nested_attr"), + tfjsonpath.New("double_nested_attr"), + tfjsonpath.New("str_attr"), + }, + "test_resource.one", + tfjsonpath.New("str_attr"), + compare.ValuesSame(), + ), + }, + ExpectError: regexp.MustCompile("expected values to be the same, but they differ: str2 != str1\nexpected values to be the same, but they differ: str3 != str1"), + }, + }, + }) +} diff --git a/querycheck/compare_value_pairs.go b/querycheck/compare_value_pairs.go new file mode 100644 index 000000000..7ce055bc4 --- /dev/null +++ b/querycheck/compare_value_pairs.go @@ -0,0 +1,111 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "fmt" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/compare" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +// Resource Query Check +var _ QueryCheck = &compareValuePairs{} + +type compareValuePairs struct { + resourceAddressOne string + attributePathOne tfjsonpath.Path + resourceAddressTwo string + attributePathTwo tfjsonpath.Path + comparer compare.ValueComparer +} + +// CheckQuery implements the query check logic. +func (e *compareValuePairs) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { + var resourceOne *tfjson.QueryResource + var resourceTwo *tfjson.QueryResource + + if req.Query == nil { + resp.Error = fmt.Errorf("query is nil") + + return + } + + if req.Query.Values == nil { + resp.Error = fmt.Errorf("query does not contain any query values") + + return + } + + if req.Query.Values.RootModule == nil { + resp.Error = fmt.Errorf("query does not contain a root module") + + return + } + + for _, r := range req.Query.Values.RootModule.Resources { + if e.resourceAddressOne == r.Address { + resourceOne = r + + break + } + } + + if resourceOne == nil { + resp.Error = fmt.Errorf("%s - Resource not found in query", e.resourceAddressOne) + + return + } + + resultOne, err := tfjsonpath.Traverse(resourceOne.AttributeValues, e.attributePathOne) + + if err != nil { + resp.Error = err + + return + } + + for _, r := range req.Query.Values.RootModule.Resources { + if e.resourceAddressTwo == r.Address { + resourceTwo = r + + break + } + } + + if resourceTwo == nil { + resp.Error = fmt.Errorf("%s - Resource not found in query", e.resourceAddressTwo) + + return + } + + resultTwo, err := tfjsonpath.Traverse(resourceTwo.AttributeValues, e.attributePathTwo) + + if err != nil { + resp.Error = err + + return + } + + err = e.comparer.CompareValues(resultOne, resultTwo) + + if err != nil { + resp.Error = err + } +} + +// CompareValuePairs returns a query check that compares the value in query for the first given resource address and +// path with the value in query for the second given resource address and path using the supplied value comparer. +func CompareValuePairs(resourceAddressOne string, attributePathOne tfjsonpath.Path, resourceAddressTwo string, attributePathTwo tfjsonpath.Path, comparer compare.ValueComparer) QueryCheck { + return &compareValuePairs{ + resourceAddressOne: resourceAddressOne, + attributePathOne: attributePathOne, + resourceAddressTwo: resourceAddressTwo, + attributePathTwo: attributePathTwo, + comparer: comparer, + } +} diff --git a/querycheck/compare_value_pairs_test.go b/querycheck/compare_value_pairs_test.go new file mode 100644 index 000000000..e65c4420f --- /dev/null +++ b/querycheck/compare_value_pairs_test.go @@ -0,0 +1,142 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck_test + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-plugin-testing/compare" + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/querycheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestCompareValuePairs_CheckQuery_ValuesSame_DifferError(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + float_attribute = 1.234 + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValuePairs( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + "test_resource.one", + tfjsonpath.New("float_attribute"), + compare.ValuesSame(), + ), + }, + ExpectError: regexp.MustCompile("expected values to be the same, but they differ: true != 1.234"), + }, + }, + }) +} + +func TestCompareValuePairs_CheckQuery_ValuesSame(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + resource "test_resource" "two" { + bool_attribute = true + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValuePairs( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + "test_resource.two", + tfjsonpath.New("bool_attribute"), + compare.ValuesSame(), + ), + }, + }, + }, + }) +} + +func TestCompareValuePairs_CheckQuery_ValuesDiffer_SameError(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + resource "test_resource" "two" { + bool_attribute = true + }`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValuePairs( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + "test_resource.two", + tfjsonpath.New("bool_attribute"), + compare.ValuesDiffer(), + ), + }, + ExpectError: regexp.MustCompile("expected values to differ, but they are the same: true == true"), + }, + }, + }) +} + +func TestCompareValuePairs_CheckQuery_ValuesDiffer(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + float_attribute = 1.234 + }`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.CompareValuePairs( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + "test_resource.one", + tfjsonpath.New("float_attribute"), + compare.ValuesDiffer(), + ), + }, + }, + }, + }) +} diff --git a/querycheck/compare_value_test.go b/querycheck/compare_value_test.go new file mode 100644 index 000000000..16ced6aed --- /dev/null +++ b/querycheck/compare_value_test.go @@ -0,0 +1,241 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck_test + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-plugin-testing/compare" + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/querycheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestCompareValue_CheckQuery_NoQueryValues(t *testing.T) { + t.Parallel() + + boolValuesDiffer := querycheck.CompareValue(compare.ValuesSame()) + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + // No query values have been added + boolValuesDiffer, + }, + ExpectError: regexp.MustCompile(`resource addresses index out of bounds: 0`), + }, + }, + }) +} + +func TestCompareValue_CheckQuery_ValuesSame_ValueDiffersError(t *testing.T) { + t.Parallel() + + boolValuesDiffer := querycheck.CompareValue(compare.ValuesSame()) + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + boolValuesDiffer.AddQueryValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + ), + }, + }, + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + boolValuesDiffer.AddQueryValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + ), + }, + }, + { + Config: `resource "test_resource" "one" { + bool_attribute = false + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + boolValuesDiffer.AddQueryValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + ), + }, + ExpectError: regexp.MustCompile(`expected values to be the same, but they differ: true != false`), + }, + }, + }) +} + +func TestCompareValue_CheckQuery_ValuesSame(t *testing.T) { + t.Parallel() + + boolValuesDiffer := querycheck.CompareValue(compare.ValuesSame()) + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + boolValuesDiffer.AddQueryValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + ), + }, + }, + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + boolValuesDiffer.AddQueryValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + ), + }, + }, + }, + }) +} + +func TestCompareValue_CheckQuery_ValuesDiffer_ValueSameError(t *testing.T) { + t.Parallel() + + boolValuesDiffer := querycheck.CompareValue(compare.ValuesDiffer()) + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + boolValuesDiffer.AddQueryValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + ), + }, + }, + { + Config: `resource "test_resource" "one" { + bool_attribute = false + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + boolValuesDiffer.AddQueryValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + ), + }, + }, + { + Config: `resource "test_resource" "one" { + bool_attribute = false + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + boolValuesDiffer.AddQueryValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + ), + }, + ExpectError: regexp.MustCompile(`expected values to differ, but they are the same: false == false`), + }, + }, + }) +} + +func TestCompareValue_CheckQuery_ValuesDiffer(t *testing.T) { + t.Parallel() + + boolValuesDiffer := querycheck.CompareValue(compare.ValuesDiffer()) + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + boolValuesDiffer.AddQueryValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + ), + }, + }, + { + Config: `resource "test_resource" "one" { + bool_attribute = false + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + boolValuesDiffer.AddQueryValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + ), + }, + }, + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + boolValuesDiffer.AddQueryValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + ), + }, + }, + }, + }) +} diff --git a/querycheck/doc.go b/querycheck/doc.go new file mode 100644 index 000000000..67aa0cede --- /dev/null +++ b/querycheck/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package querycheck contains the query check interface, request/response structs, and common query check implementations. +package querycheck diff --git a/querycheck/expect_identity.go b/querycheck/expect_identity.go new file mode 100644 index 000000000..4b695bea8 --- /dev/null +++ b/querycheck/expect_identity.go @@ -0,0 +1,138 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "fmt" + "maps" + "slices" + "sort" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +var _ QueryCheck = expectIdentity{} + +type expectIdentity struct { + resourceAddress string + identity map[string]knownvalue.Check +} + +// CheckQuery implements the query check logic. +func (e expectIdentity) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { + var resource *tfjson.QueryResource + + if req.Query == nil { + resp.Error = fmt.Errorf("query is nil") + + return + } + + if req.Query.Values == nil { + resp.Error = fmt.Errorf("query does not contain any query values") + + return + } + + if req.Query.Values.RootModule == nil { + resp.Error = fmt.Errorf("query does not contain a root module") + + return + } + + for _, r := range req.Query.Values.RootModule.Resources { + if e.resourceAddress == r.Address { + resource = r + + break + } + } + + if resource == nil { + resp.Error = fmt.Errorf("%s - Resource not found in query", e.resourceAddress) + + return + } + + if resource.IdentitySchemaVersion == nil || len(resource.IdentityValues) == 0 { + resp.Error = fmt.Errorf("%s - Identity not found in query. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.12+)", e.resourceAddress) + + return + } + + if len(resource.IdentityValues) != len(e.identity) { + deltaMsg := "" + if len(resource.IdentityValues) > len(e.identity) { + deltaMsg = createDeltaString(resource.IdentityValues, e.identity, "actual identity has extra attribute(s): ") + } else { + deltaMsg = createDeltaString(e.identity, resource.IdentityValues, "actual identity is missing attribute(s): ") + } + + resp.Error = fmt.Errorf("%s - Expected %d attribute(s) in the actual identity object, got %d attribute(s): %s", e.resourceAddress, len(e.identity), len(resource.IdentityValues), deltaMsg) + return + } + + var keys []string + + for k := range e.identity { + keys = append(keys, k) + } + + sort.SliceStable(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + for _, k := range keys { + actualIdentityVal, ok := resource.IdentityValues[k] + + if !ok { + resp.Error = fmt.Errorf("%s - missing attribute %q in actual identity object", e.resourceAddress, k) + return + } + + if err := e.identity[k].CheckValue(actualIdentityVal); err != nil { + resp.Error = fmt.Errorf("%s - %q identity attribute: %s", e.resourceAddress, k, err) + return + } + } +} + +// ExpectIdentity returns a query check that asserts that the identity at the given resource matches a known object, where each +// map key represents an identity attribute name. The identity in query must exactly match the given object and any missing/extra +// attributes will raise a diagnostic. +// +// This query check can only be used with managed resources that support resource identity. Resource identity is only supported in Terraform v1.12+ +func ExpectIdentity(resourceAddress string, identity map[string]knownvalue.Check) QueryCheck { + return expectIdentity{ + resourceAddress: resourceAddress, + identity: identity, + } +} + +// createDeltaString prints the map keys that are present in mapA and not present in mapB +func createDeltaString[T any, V any](mapA map[string]T, mapB map[string]V, msgPrefix string) string { + deltaMsg := "" + + deltaMap := make(map[string]T, len(mapA)) + maps.Copy(deltaMap, mapA) + for key := range mapB { + delete(deltaMap, key) + } + + deltaKeys := slices.Sorted(maps.Keys(deltaMap)) + + for i, k := range deltaKeys { + if i == 0 { + deltaMsg += msgPrefix + } else { + deltaMsg += ", " + } + deltaMsg += fmt.Sprintf("%q", k) + } + + return deltaMsg +} diff --git a/querycheck/expect_identity_example_test.go b/querycheck/expect_identity_example_test.go new file mode 100644 index 000000000..b625afc45 --- /dev/null +++ b/querycheck/expect_identity_example_test.go @@ -0,0 +1,41 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/querycheck" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func ExampleExpectIdentity() { + // A typical test would accept *testing.T as a function parameter, for instance `func TestSomething(t *testing.T) { ... }`. + t := &testing.T{} + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Resource identity support is only available in Terraform v1.12+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + // Provider definition omitted. Assuming "test_resource" has an identity schema with "id" and "name" string attributes + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentity( + "test_resource.one", + map[string]knownvalue.Check{ + "id": knownvalue.StringExact("id-123"), + "name": knownvalue.StringExact("John Doe"), + }, + ), + }, + }, + }, + }) +} diff --git a/querycheck/expect_identity_test.go b/querycheck/expect_identity_test.go new file mode 100644 index 000000000..cf7fa8969 --- /dev/null +++ b/querycheck/expect_identity_test.go @@ -0,0 +1,334 @@ +// Copyright (c) HashiCorp, Inc. +// 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/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/querycheck" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestExpectIdentity_CheckQuery_ResourceNotFound(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentity( + "examplecloud_thing.two", + 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), + }, + ), + }, + ), + }, + ExpectError: regexp.MustCompile("examplecloud_thing.two - Resource not found in query"), + }, + }, + }) +} + +func TestExpectIdentity_CheckQuery_No_Terraform_Identity_Support(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + tfversion.SkipAbove(tfversion.Version1_11_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + // Resource support identity, but the Terraform versions running will not. + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentity( + "examplecloud_thing.one", + 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), + }, + ), + }, + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in query. Either the resource ` + + `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, + ), + }, + }, + }) +} + +func TestExpectIdentity_CheckQuery_No_Identity(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + // Resource does not support identity + "examplecloud": examplecloudProviderNoIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentity( + "examplecloud_thing.one", + 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), + }, + ), + }, + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in query. Either the resource ` + + `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, + ), + }, + }, + }) +} + +func TestExpectIdentity_CheckQuery(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentity( + "examplecloud_thing.one", + 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), + }, + ), + }, + ), + }, + }, + }, + }) +} + +func TestExpectIdentity_CheckQuery_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + + querycheck.ExpectIdentity( + "examplecloud_thing.one", + map[string]knownvalue.Check{ + "id": knownvalue.Bool(true), + "list_of_numbers": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Int64Exact(1), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(4), + }, + ), + }, + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - "id" identity attribute: expected bool value for Bool check, got: string`), + }, + }, + }) +} + +func TestExpectIdentity_CheckQuery_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + + querycheck.ExpectIdentity( + "examplecloud_thing.one", + map[string]knownvalue.Check{ + "id": knownvalue.StringExact("321-id"), + "list_of_numbers": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Int64Exact(1), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(4), + }, + ), + }, + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - "id" identity attribute: expected value 321-id for StringExact check, got: id-123`), + }, + }, + }) +} + +func TestExpectIdentity_CheckQuery_ExtraAttribute(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + + querycheck.ExpectIdentity( + "examplecloud_thing.one", + map[string]knownvalue.Check{ + "id": knownvalue.StringExact("321-id"), + }, + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Expected 1 attribute\(s\) in the actual identity object, got 2 attribute\(s\): actual identity has extra attribute\(s\): "list_of_numbers"`), + }, + }, + }) +} + +func TestExpectIdentity_CheckQuery_MissingAttribute(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + + querycheck.ExpectIdentity( + "examplecloud_thing.one", + 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), + }, + ), + }, + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Expected 3 attribute\(s\) in the actual identity object, got 2 attribute\(s\): actual identity is missing attribute\(s\): "nonexistent_attr"`), + }, + }, + }) +} + +func TestExpectIdentity_CheckQuery_MismatchedAttribute(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentity( + "examplecloud_thing.one", + map[string]knownvalue.Check{ + "not_id": knownvalue.StringExact("id-123"), + "list_of_numbers": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Int64Exact(1), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(4), + }, + ), + }, + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - missing attribute "not_id" in actual identity object`), + }, + }, + }) +} diff --git a/querycheck/expect_identity_value.go b/querycheck/expect_identity_value.go new file mode 100644 index 000000000..468264f5b --- /dev/null +++ b/querycheck/expect_identity_value.go @@ -0,0 +1,91 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "fmt" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +var _ QueryCheck = expectIdentityValue{} + +type expectIdentityValue struct { + resourceAddress string + attributePath tfjsonpath.Path + identityValue knownvalue.Check +} + +// CheckQuery implements the query check logic. +func (e expectIdentityValue) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { + var resource *tfjson.QueryResource + + if req.Query == nil { + resp.Error = fmt.Errorf("query is nil") + + return + } + + if req.Query.Values == nil { + resp.Error = fmt.Errorf("query does not contain any query values") + + return + } + + if req.Query.Values.RootModule == nil { + resp.Error = fmt.Errorf("query does not contain a root module") + + return + } + + for _, r := range req.Query.Values.RootModule.Resources { + if e.resourceAddress == r.Address { + resource = r + + break + } + } + + if resource == nil { + resp.Error = fmt.Errorf("%s - Resource not found in query", e.resourceAddress) + + return + } + + if resource.IdentitySchemaVersion == nil || len(resource.IdentityValues) == 0 { + resp.Error = fmt.Errorf("%s - Identity not found in query. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.12+)", e.resourceAddress) + + return + } + + result, err := tfjsonpath.Traverse(resource.IdentityValues, e.attributePath) + + if err != nil { + resp.Error = err + + return + } + + if err := e.identityValue.CheckValue(result); err != nil { + resp.Error = fmt.Errorf("error checking identity value for attribute at path: %s.%s, err: %s", e.resourceAddress, e.attributePath.String(), err) + + return + } +} + +// ExpectIdentityValue returns a query check that asserts that the specified identity attribute at the given resource +// matches a known value. This query check can only be used with managed resources that support resource identity. +// +// Resource identity is only supported in Terraform v1.12+ +func ExpectIdentityValue(resourceAddress string, attributePath tfjsonpath.Path, identityValue knownvalue.Check) QueryCheck { + return expectIdentityValue{ + resourceAddress: resourceAddress, + attributePath: attributePath, + identityValue: identityValue, + } +} diff --git a/querycheck/expect_identity_value_example_test.go b/querycheck/expect_identity_value_example_test.go new file mode 100644 index 000000000..776632d68 --- /dev/null +++ b/querycheck/expect_identity_value_example_test.go @@ -0,0 +1,40 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/querycheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func ExampleExpectIdentityValue() { + // A typical test would accept *testing.T as a function parameter, for instance `func TestSomething(t *testing.T) { ... }`. + t := &testing.T{} + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Resource identity support is only available in Terraform v1.12+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + // Provider definition omitted. Assuming "test_resource" has an identity schema with an "id" string attribute + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentityValue( + "test_resource.one", + tfjsonpath.New("id"), + knownvalue.StringExact("id-123"), + ), + }, + }, + }, + }) +} diff --git a/querycheck/expect_identity_value_matches_query.go b/querycheck/expect_identity_value_matches_query.go new file mode 100644 index 000000000..ed4e3d40c --- /dev/null +++ b/querycheck/expect_identity_value_matches_query.go @@ -0,0 +1,97 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "fmt" + "reflect" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +var _ QueryCheck = expectIdentityValueMatchesQuery{} + +type expectIdentityValueMatchesQuery struct { + resourceAddress string + attributePath tfjsonpath.Path +} + +// CheckQuery implements the query check logic. +func (e expectIdentityValueMatchesQuery) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { + var resource *tfjson.QueryResource + + if req.Query == nil { + resp.Error = fmt.Errorf("query is nil") + + return + } + + if req.Query.Values == nil { + resp.Error = fmt.Errorf("query does not contain any query values") + + return + } + + if req.Query.Values.RootModule == nil { + resp.Error = fmt.Errorf("query does not contain a root module") + + return + } + + for _, r := range req.Query.Values.RootModule.Resources { + if e.resourceAddress == r.Address { + resource = r + + break + } + } + + if resource == nil { + resp.Error = fmt.Errorf("%s - Resource not found in query", e.resourceAddress) + + return + } + + if resource.IdentitySchemaVersion == nil || len(resource.IdentityValues) == 0 { + resp.Error = fmt.Errorf("%s - Identity not found in query. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.12+)", e.resourceAddress) + + return + } + + identityResult, err := tfjsonpath.Traverse(resource.IdentityValues, e.attributePath) + + if err != nil { + resp.Error = err + + return + } + + queryResult, err := tfjsonpath.Traverse(resource.AttributeValues, e.attributePath) + + if err != nil { + resp.Error = err + + return + } + + if !reflect.DeepEqual(identityResult, queryResult) { + resp.Error = fmt.Errorf("expected identity and query value at path to match, but they differ: %s.%s, identity value: %v, query value: %v", e.resourceAddress, e.attributePath.String(), identityResult, queryResult) + + return + } +} + +// ExpectIdentityValueMatchesQuery returns a query check that asserts that the specified identity attribute at the given resource +// matches the same attribute in query. This is useful when an identity attribute is in sync with a query attribute of the same path. +// +// This query check can only be used with managed resources that support resource identity. Resource identity is only supported in Terraform v1.12+ +func ExpectIdentityValueMatchesQuery(resourceAddress string, attributePath tfjsonpath.Path) QueryCheck { + return expectIdentityValueMatchesQuery{ + resourceAddress: resourceAddress, + attributePath: attributePath, + } +} diff --git a/querycheck/expect_identity_value_matches_query_at_path.go b/querycheck/expect_identity_value_matches_query_at_path.go new file mode 100644 index 000000000..a638b5c7d --- /dev/null +++ b/querycheck/expect_identity_value_matches_query_at_path.go @@ -0,0 +1,106 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "fmt" + "reflect" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +var _ QueryCheck = expectIdentityValueMatchesQueryAtPath{} + +type expectIdentityValueMatchesQueryAtPath struct { + resourceAddress string + identityAttrPath tfjsonpath.Path + queryAttrPath tfjsonpath.Path +} + +// CheckQuery implements the query check logic. +func (e expectIdentityValueMatchesQueryAtPath) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { + var resource *tfjson.QueryResource + + if req.Query == nil { + resp.Error = fmt.Errorf("query is nil") + + return + } + + if req.Query.Values == nil { + resp.Error = fmt.Errorf("query does not contain any query values") + + return + } + + if req.Query.Values.RootModule == nil { + resp.Error = fmt.Errorf("query does not contain a root module") + + return + } + + for _, r := range req.Query.Values.RootModule.Resources { + if e.resourceAddress == r.Address { + resource = r + + break + } + } + + if resource == nil { + resp.Error = fmt.Errorf("%s - Resource not found in query", e.resourceAddress) + + return + } + + if resource.IdentitySchemaVersion == nil || len(resource.IdentityValues) == 0 { + resp.Error = fmt.Errorf("%s - Identity not found in query. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.12+)", e.resourceAddress) + + return + } + + identityResult, err := tfjsonpath.Traverse(resource.IdentityValues, e.identityAttrPath) + + if err != nil { + resp.Error = err + + return + } + + queryResult, err := tfjsonpath.Traverse(resource.AttributeValues, e.queryAttrPath) + + if err != nil { + resp.Error = err + + return + } + + if !reflect.DeepEqual(identityResult, queryResult) { + resp.Error = fmt.Errorf( + "expected identity (%[1]s.%[2]s) and query value (%[1]s.%[3]s) to match, but they differ: identity value: %[4]v, query value: %[5]v", + e.resourceAddress, + e.identityAttrPath.String(), + e.queryAttrPath.String(), + identityResult, + queryResult, + ) + + return + } +} + +// ExpectIdentityValueMatchesQueryAtPath returns a query check that asserts that the specified identity attribute at the given resource +// matches the specified attribute in query. This is useful when an identity attribute is in sync with a query attribute of a different path. +// +// This query check can only be used with managed resources that support resource identity. Resource identity is only supported in Terraform v1.12+ +func ExpectIdentityValueMatchesQueryAtPath(resourceAddress string, identityAttrPath, queryAttrPath tfjsonpath.Path) QueryCheck { + return expectIdentityValueMatchesQueryAtPath{ + resourceAddress: resourceAddress, + identityAttrPath: identityAttrPath, + queryAttrPath: queryAttrPath, + } +} diff --git a/querycheck/expect_identity_value_matches_query_at_path_example_test.go b/querycheck/expect_identity_value_matches_query_at_path_example_test.go new file mode 100644 index 000000000..4824cf8d6 --- /dev/null +++ b/querycheck/expect_identity_value_matches_query_at_path_example_test.go @@ -0,0 +1,42 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/querycheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func ExampleExpectIdentityValueMatchesQueryAtPath() { + // A typical test would accept *testing.T as a function parameter, for instance `func TestSomething(t *testing.T) { ... }`. + t := &testing.T{} + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Resource identity support is only available in Terraform v1.12+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + // Provider definition omitted. Assuming "test_resource": + // - Has an identity schema with an "identity_id" string attribute + // - Has a resource schema with an "query_id" string attribute + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + // The identity attribute at "identity_id" and query attribute at "query_id" must match + querycheck.ExpectIdentityValueMatchesQueryAtPath( + "test_resource.one", + tfjsonpath.New("identity_id"), + tfjsonpath.New("query_id"), + ), + }, + }, + }, + }) +} diff --git a/querycheck/expect_identity_value_matches_query_at_path_test.go b/querycheck/expect_identity_value_matches_query_at_path_test.go new file mode 100644 index 000000000..4dbb5e921 --- /dev/null +++ b/querycheck/expect_identity_value_matches_query_at_path_test.go @@ -0,0 +1,344 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck_test + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" + "github.com/hashicorp/terraform-plugin-testing/querycheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestExpectIdentityValueMatchesQueryAtPath_CheckQuery_ResourceNotFound(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentityValueMatchesQueryAtPath( + "examplecloud_thing.two", + tfjsonpath.New("id"), + tfjsonpath.New("id"), + ), + }, + ExpectError: regexp.MustCompile("examplecloud_thing.two - Resource not found in query"), + }, + }, + }) +} + +func TestExpectIdentityValueMatchesQueryAtPath_CheckQuery_No_Terraform_Identity_Support(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + tfversion.SkipAbove(tfversion.Version1_11_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + // Resource support identity, but the Terraform versions running will not. + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentityValueMatchesQueryAtPath( + "examplecloud_thing.one", + tfjsonpath.New("id"), + tfjsonpath.New("id"), + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in query. Either the resource ` + + `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, + ), + }, + }, + }) +} + +func TestExpectIdentityValueMatchesQueryAtPath_CheckQuery_No_Identity(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + // Resource does not support identity + "examplecloud": examplecloudProviderNoIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentityValueMatchesQueryAtPath( + "examplecloud_thing.one", + tfjsonpath.New("id"), + tfjsonpath.New("id"), + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in query. Either the resource ` + + `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, + ), + }, + }, + }) +} + +func TestExpectIdentityValueMatchesQueryAtPath_CheckQuery_String_Matches(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentityDifferentPaths(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentityValueMatchesQueryAtPath( + "examplecloud_thing.one", + tfjsonpath.New("identity_id"), + tfjsonpath.New("query_id"), + ), + }, + }, + }, + }) +} + +func TestExpectIdentityValueMatchesQueryAtPath_CheckQuery_String_DoesntMatch(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithMismatchedResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentityValueMatchesQueryAtPath( + "examplecloud_thing.one", + tfjsonpath.New("id"), + tfjsonpath.New("id"), + ), + }, + ExpectError: regexp.MustCompile(`expected identity \(examplecloud_thing.one.id\) and query value \(examplecloud_thing.one.id\) to match, but they differ: identity value: id-123, query value: 321-di`), + }, + }, + }) +} + +func TestExpectIdentityValueMatchesQueryAtPath_CheckQuery_List(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentityDifferentPaths(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentityValueMatchesQueryAtPath( + "examplecloud_thing.one", + tfjsonpath.New("identity_list_of_numbers"), + tfjsonpath.New("query_list_of_numbers"), + ), + }, + }, + }, + }) +} + +func TestExpectIdentityValueMatchesQueryAtPath_CheckQuery_List_DoesntMatch(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithMismatchedResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentityValueMatchesQueryAtPath( + "examplecloud_thing.one", + tfjsonpath.New("list_of_numbers"), + tfjsonpath.New("list_of_numbers"), + ), + }, + ExpectError: regexp.MustCompile(`expected identity \(examplecloud_thing.one.list_of_numbers\) and query value \(examplecloud_thing.one.list_of_numbers\) to match, but they differ: identity value: \[1 2 3 4\], query value: \[4 3 2 1\]`), + }, + }, + }) +} + +func examplecloudProviderWithResourceIdentityDifferentPaths() func() (tfprotov6.ProviderServer, error) { + return providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_thing": { + CreateResponse: &resource.CreateResponse{ + NewQuery: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + "query_id": tftypes.String, + "query_list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "test value"), + "query_id": tftypes.NewValue(tftypes.String, "id-123"), + "query_list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 1), + tftypes.NewValue(tftypes.Number, 2), + tftypes.NewValue(tftypes.Number, 3), + tftypes.NewValue(tftypes.Number, 4), + }, + ), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "identity_id": tftypes.String, + "identity_list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "identity_id": tftypes.NewValue(tftypes.String, "id-123"), + "identity_list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 1), + tftypes.NewValue(tftypes.Number, 2), + tftypes.NewValue(tftypes.Number, 3), + tftypes.NewValue(tftypes.Number, 4), + }, + ), + }, + )), + }, + ReadResponse: &resource.ReadResponse{ + NewQuery: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + "query_id": tftypes.String, + "query_list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "test value"), + "query_id": tftypes.NewValue(tftypes.String, "id-123"), + "query_list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 1), + tftypes.NewValue(tftypes.Number, 2), + tftypes.NewValue(tftypes.Number, 3), + tftypes.NewValue(tftypes.Number, 4), + }, + ), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "identity_id": tftypes.String, + "identity_list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "identity_id": tftypes.NewValue(tftypes.String, "id-123"), + "identity_list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 1), + tftypes.NewValue(tftypes.Number, 2), + tftypes.NewValue(tftypes.Number, 3), + tftypes.NewValue(tftypes.Number, 4), + }, + ), + }, + )), + }, + IdentitySchemaResponse: &resource.IdentitySchemaResponse{ + Schema: &tfprotov6.ResourceIdentitySchema{ + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "identity_id", + Type: tftypes.String, + RequiredForImport: true, + }, + { + Name: "identity_list_of_numbers", + Type: tftypes.List{ElementType: tftypes.Number}, + OptionalForImport: true, + }, + }, + }, + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "name", + Type: tftypes.String, + Computed: true, + }, + { + Name: "query_id", + Type: tftypes.String, + Computed: true, + }, + { + Name: "query_list_of_numbers", + Type: tftypes.List{ElementType: tftypes.Number}, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }) +} diff --git a/querycheck/expect_identity_value_matches_query_example_test.go b/querycheck/expect_identity_value_matches_query_example_test.go new file mode 100644 index 000000000..d0ae5d600 --- /dev/null +++ b/querycheck/expect_identity_value_matches_query_example_test.go @@ -0,0 +1,38 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/querycheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func ExampleExpectIdentityValueMatchesQuery() { + // A typical test would accept *testing.T as a function parameter, for instance `func TestSomething(t *testing.T) { ... }`. + t := &testing.T{} + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Resource identity support is only available in Terraform v1.12+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + // Provider definition omitted. Assuming "test_resource": + // - Has an identity schema with an "id" string attribute + // - Has a resource schema with an "id" string attribute + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + // The identity attribute and query attribute at "id" must match + querycheck.ExpectIdentityValueMatchesQuery("test_resource.one", tfjsonpath.New("id")), + }, + }, + }, + }) +} diff --git a/querycheck/expect_identity_value_matches_query_test.go b/querycheck/expect_identity_value_matches_query_test.go new file mode 100644 index 000000000..58d40fb35 --- /dev/null +++ b/querycheck/expect_identity_value_matches_query_test.go @@ -0,0 +1,337 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck_test + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" + "github.com/hashicorp/terraform-plugin-testing/querycheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestExpectIdentityValueMatchesQuery_CheckQuery_ResourceNotFound(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentityValueMatchesQuery( + "examplecloud_thing.two", + tfjsonpath.New("id"), + ), + }, + ExpectError: regexp.MustCompile("examplecloud_thing.two - Resource not found in query"), + }, + }, + }) +} + +func TestExpectIdentityValueMatchesQuery_CheckQuery_No_Terraform_Identity_Support(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + tfversion.SkipAbove(tfversion.Version1_11_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + // Resource support identity, but the Terraform versions running will not. + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentityValueMatchesQuery( + "examplecloud_thing.one", + tfjsonpath.New("id"), + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in query. Either the resource ` + + `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, + ), + }, + }, + }) +} + +func TestExpectIdentityValueMatchesQuery_CheckQuery_No_Identity(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + // Resource does not support identity + "examplecloud": examplecloudProviderNoIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentityValueMatchesQuery( + "examplecloud_thing.one", + tfjsonpath.New("id"), + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in query. Either the resource ` + + `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, + ), + }, + }, + }) +} + +func TestExpectIdentityValueMatchesQuery_CheckQuery_String_Matches(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentityValueMatchesQuery( + "examplecloud_thing.one", + tfjsonpath.New("id"), + ), + }, + }, + }, + }) +} + +func TestExpectIdentityValueMatchesQuery_CheckQuery_String_DoesntMatch(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithMismatchedResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentityValueMatchesQuery( + "examplecloud_thing.one", + tfjsonpath.New("id"), + ), + }, + ExpectError: regexp.MustCompile("expected identity and query value at path to match, but they differ: examplecloud_thing.one.id, identity value: id-123, query value: 321-di"), + }, + }, + }) +} + +func TestExpectIdentityValueMatchesQuery_CheckQuery_List(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentityValueMatchesQuery( + "examplecloud_thing.one", + tfjsonpath.New("list_of_numbers"), + ), + }, + }, + }, + }) +} + +func TestExpectIdentityValueMatchesQuery_CheckQuery_List_DoesntMatch(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithMismatchedResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentityValueMatchesQuery( + "examplecloud_thing.one", + tfjsonpath.New("list_of_numbers"), + ), + }, + ExpectError: regexp.MustCompile(`expected identity and query value at path to match, but they differ: examplecloud_thing.one.list_of_numbers, identity value: \[1 2 3 4\], query value: \[4 3 2 1\]`), + }, + }, + }) +} + +func examplecloudProviderWithMismatchedResourceIdentity() func() (tfprotov6.ProviderServer, error) { + return providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_thing": { + CreateResponse: &resource.CreateResponse{ + NewQuery: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + "id": tftypes.String, + "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "test value"), + "id": tftypes.NewValue(tftypes.String, "321-di"), // doesn't match identity -> id + "list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 4), // doesn't match identity -> list_of_numbers[0] + tftypes.NewValue(tftypes.Number, 3), // doesn't match identity -> list_of_numbers[1] + tftypes.NewValue(tftypes.Number, 2), // doesn't match identity -> list_of_numbers[2] + tftypes.NewValue(tftypes.Number, 1), // doesn't match identity -> list_of_numbers[3] + }, + ), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "id-123"), + "list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 1), + tftypes.NewValue(tftypes.Number, 2), + tftypes.NewValue(tftypes.Number, 3), + tftypes.NewValue(tftypes.Number, 4), + }, + ), + }, + )), + }, + ReadResponse: &resource.ReadResponse{ + NewQuery: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + "id": tftypes.String, + "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "test value"), + "id": tftypes.NewValue(tftypes.String, "321-di"), // doesn't match identity -> id + "list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 4), // doesn't match identity -> list_of_numbers[0] + tftypes.NewValue(tftypes.Number, 3), // doesn't match identity -> list_of_numbers[1] + tftypes.NewValue(tftypes.Number, 2), // doesn't match identity -> list_of_numbers[2] + tftypes.NewValue(tftypes.Number, 1), // doesn't match identity -> list_of_numbers[3] + }, + ), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "id-123"), + "list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 1), + tftypes.NewValue(tftypes.Number, 2), + tftypes.NewValue(tftypes.Number, 3), + tftypes.NewValue(tftypes.Number, 4), + }, + ), + }, + )), + }, + IdentitySchemaResponse: &resource.IdentitySchemaResponse{ + Schema: &tfprotov6.ResourceIdentitySchema{ + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + RequiredForImport: true, + }, + { + Name: "list_of_numbers", + Type: tftypes.List{ElementType: tftypes.Number}, + OptionalForImport: true, + }, + }, + }, + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "name", + Type: tftypes.String, + Computed: true, + }, + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + { + Name: "list_of_numbers", + Type: tftypes.List{ElementType: tftypes.Number}, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }) +} diff --git a/querycheck/expect_identity_value_test.go b/querycheck/expect_identity_value_test.go new file mode 100644 index 000000000..5569f6496 --- /dev/null +++ b/querycheck/expect_identity_value_test.go @@ -0,0 +1,461 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck_test + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/querycheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestExpectIdentityValue_CheckQuery_ResourceNotFound(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentityValue( + "examplecloud_thing.two", + tfjsonpath.New("id"), + knownvalue.StringExact("id-123"), + ), + }, + ExpectError: regexp.MustCompile("examplecloud_thing.two - Resource not found in query"), + }, + }, + }) +} + +func TestExpectIdentityValue_CheckQuery_No_Terraform_Identity_Support(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + tfversion.SkipAbove(tfversion.Version1_11_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + // Resource support identity, but the Terraform versions running will not. + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("id"), + knownvalue.StringExact("id-123"), + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in query. Either the resource ` + + `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, + ), + }, + }, + }) +} + +func TestExpectIdentityValue_CheckQuery_No_Identity(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + // Resource does not support identity + "examplecloud": examplecloudProviderNoIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("id"), + knownvalue.StringExact("id-123"), + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in query. Either the resource ` + + `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, + ), + }, + }, + }) +} + +func TestExpectIdentityValue_CheckQuery_String(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("id"), + knownvalue.StringExact("id-123")), + }, + }, + }, + }) +} + +func TestExpectIdentityValue_CheckQuery_String_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("id"), + knownvalue.Bool(true)), + }, + ExpectError: regexp.MustCompile("expected bool value for Bool check, got: string"), + }, + }, + }) +} + +func TestExpectIdentityValue_CheckQuery_String_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("id"), + knownvalue.StringExact("321-id")), + }, + ExpectError: regexp.MustCompile("expected value 321-id for StringExact check, got: id-123"), + }, + }, + }) +} + +func TestExpectIdentityValue_CheckQuery_List(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("list_of_numbers").AtSliceIndex(0), + knownvalue.Int64Exact(1), + ), + querycheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("list_of_numbers").AtSliceIndex(1), + knownvalue.Int64Exact(2), + ), + querycheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("list_of_numbers").AtSliceIndex(2), + knownvalue.Int64Exact(3), + ), + querycheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("list_of_numbers").AtSliceIndex(3), + knownvalue.Int64Exact(4), + ), + }, + }, + }, + }) +} + +func TestExpectIdentityValue_CheckQuery_List_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {} + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("list_of_numbers"), + knownvalue.MapExact(map[string]knownvalue.Check{}), + ), + }, + ExpectError: regexp.MustCompile(`expected map\[string\]any value for MapExact check, got: \[\]interface {}`), + }, + }, + }) +} + +func TestExpectIdentityValue_CheckQuery_List_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("list_of_numbers"), + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.Int64Exact(4), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(1), + }), + ), + }, + ExpectError: regexp.MustCompile(`list element index 0: expected value 4 for Int64Exact check, got: 1`), + }, + }, + }) +} + +func examplecloudProviderWithResourceIdentity() func() (tfprotov6.ProviderServer, error) { + return providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_thing": { + CreateResponse: &resource.CreateResponse{ + NewQuery: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + "id": tftypes.String, + "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "test value"), + "id": tftypes.NewValue(tftypes.String, "id-123"), + "list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 1), + tftypes.NewValue(tftypes.Number, 2), + tftypes.NewValue(tftypes.Number, 3), + tftypes.NewValue(tftypes.Number, 4), + }, + ), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "id-123"), + "list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 1), + tftypes.NewValue(tftypes.Number, 2), + tftypes.NewValue(tftypes.Number, 3), + tftypes.NewValue(tftypes.Number, 4), + }, + ), + }, + )), + }, + ReadResponse: &resource.ReadResponse{ + NewQuery: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + "id": tftypes.String, + "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "test value"), + "id": tftypes.NewValue(tftypes.String, "id-123"), + "list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 1), + tftypes.NewValue(tftypes.Number, 2), + tftypes.NewValue(tftypes.Number, 3), + tftypes.NewValue(tftypes.Number, 4), + }, + ), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "id-123"), + "list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 1), + tftypes.NewValue(tftypes.Number, 2), + tftypes.NewValue(tftypes.Number, 3), + tftypes.NewValue(tftypes.Number, 4), + }, + ), + }, + )), + }, + IdentitySchemaResponse: &resource.IdentitySchemaResponse{ + Schema: &tfprotov6.ResourceIdentitySchema{ + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + RequiredForImport: true, + }, + { + Name: "list_of_numbers", + Type: tftypes.List{ElementType: tftypes.Number}, + OptionalForImport: true, + }, + }, + }, + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "name", + Type: tftypes.String, + Computed: true, + }, + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + { + Name: "list_of_numbers", + Type: tftypes.List{ElementType: tftypes.Number}, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }) +} + +func examplecloudProviderNoIdentity() func() (tfprotov6.ProviderServer, error) { + return providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_thing": { + CreateResponse: &resource.CreateResponse{ + NewQuery: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "test value"), + }, + ), + }, + ReadResponse: &resource.ReadResponse{ + NewQuery: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "test value"), + }, + ), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "name", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }) +} diff --git a/querycheck/expect_known_output_value.go b/querycheck/expect_known_output_value.go new file mode 100644 index 000000000..d63cafabd --- /dev/null +++ b/querycheck/expect_known_output_value.go @@ -0,0 +1,76 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "fmt" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +// Resource Query Check +var _ QueryCheck = expectKnownOutputValue{} + +type expectKnownOutputValue struct { + outputAddress string + knownValue knownvalue.Check +} + +// CheckQuery implements the query check logic. +func (e expectKnownOutputValue) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { + var output *tfjson.QueryOutput + + if req.Query == nil { + resp.Error = fmt.Errorf("query is nil") + + return + } + + if req.Query.Values == nil { + resp.Error = fmt.Errorf("query does not contain any query values") + + return + } + + for address, oc := range req.Query.Values.Outputs { + if e.outputAddress == address { + output = oc + + break + } + } + + if output == nil { + resp.Error = fmt.Errorf("%s - Output not found in query", e.outputAddress) + + return + } + + result, err := tfjsonpath.Traverse(output.Value, tfjsonpath.Path{}) + + if err != nil { + resp.Error = err + + return + } + + if err := e.knownValue.CheckValue(result); err != nil { + resp.Error = fmt.Errorf("error checking value for output at path: %s, err: %s", e.outputAddress, err) + + return + } +} + +// ExpectKnownOutputValue returns a query check that asserts that the specified value +// has a known type, and value. +func ExpectKnownOutputValue(outputAddress string, knownValue knownvalue.Check) QueryCheck { + return expectKnownOutputValue{ + outputAddress: outputAddress, + knownValue: knownValue, + } +} diff --git a/querycheck/expect_known_output_value_at_path.go b/querycheck/expect_known_output_value_at_path.go new file mode 100644 index 000000000..5ad5c66e2 --- /dev/null +++ b/querycheck/expect_known_output_value_at_path.go @@ -0,0 +1,78 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "fmt" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +// Resource Query Check +var _ QueryCheck = expectKnownOutputValueAtPath{} + +type expectKnownOutputValueAtPath struct { + outputAddress string + outputPath tfjsonpath.Path + knownValue knownvalue.Check +} + +// CheckQuery implements the query check logic. +func (e expectKnownOutputValueAtPath) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { + var output *tfjson.QueryOutput + + if req.Query == nil { + resp.Error = fmt.Errorf("query is nil") + + return + } + + if req.Query.Values == nil { + resp.Error = fmt.Errorf("query does not contain any query values") + + return + } + + for address, oc := range req.Query.Values.Outputs { + if e.outputAddress == address { + output = oc + + break + } + } + + if output == nil { + resp.Error = fmt.Errorf("%s - Output not found in query", e.outputAddress) + + return + } + + result, err := tfjsonpath.Traverse(output.Value, e.outputPath) + + if err != nil { + resp.Error = err + + return + } + + if err := e.knownValue.CheckValue(result); err != nil { + resp.Error = fmt.Errorf("error checking value for output at path: %s.%s, err: %s", e.outputAddress, e.outputPath.String(), err) + + return + } +} + +// ExpectKnownOutputValueAtPath returns a query check that asserts that the specified output at the given path +// has a known type and value. +func ExpectKnownOutputValueAtPath(outputAddress string, outputPath tfjsonpath.Path, knownValue knownvalue.Check) QueryCheck { + return expectKnownOutputValueAtPath{ + outputAddress: outputAddress, + outputPath: outputPath, + knownValue: knownValue, + } +} diff --git a/querycheck/expect_known_output_value_at_path_example_test.go b/querycheck/expect_known_output_value_at_path_example_test.go new file mode 100644 index 000000000..423cb6ddf --- /dev/null +++ b/querycheck/expect_known_output_value_at_path_example_test.go @@ -0,0 +1,42 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/querycheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func ExampleExpectKnownOutputValueAtPath() { + // A typical test would accept *testing.T as a function parameter, for instance `func TestSomething(t *testing.T) { ... }`. + t := &testing.T{} + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("bool_attribute"), + knownvalue.Bool(true), + ), + }, + }, + }, + }) +} diff --git a/querycheck/expect_known_output_value_at_path_test.go b/querycheck/expect_known_output_value_at_path_test.go new file mode 100644 index 000000000..caf174d44 --- /dev/null +++ b/querycheck/expect_known_output_value_at_path_test.go @@ -0,0 +1,1628 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck_test + +import ( + "context" + "fmt" + "math/big" + "regexp" + "testing" + + "github.com/google/go-cmp/cmp" + tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/querycheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestExpectKnownOutputValueAtPath_CheckQuery_ResourceNotFound(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_two_output", + tfjsonpath.New("bool_attribute"), + knownvalue.Bool(true), + ), + }, + ExpectError: regexp.MustCompile("test_resource_two_output - Output not found in query"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_AttributeValueNull(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" {} + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("bool_attribute"), + knownvalue.Null(), + ), + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("float_attribute"), + knownvalue.Null(), + ), + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("int_attribute"), + knownvalue.Null(), + ), + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_attribute"), + knownvalue.Null(), + ), + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_nested_block"), + knownvalue.ListExact([]knownvalue.Check{}), + ), + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("map_attribute"), + knownvalue.Null(), + ), + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_attribute"), + knownvalue.Null(), + ), + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_nested_block"), + knownvalue.SetExact([]knownvalue.Check{}), + ), + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("string_attribute"), + knownvalue.Null(), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_AttributeValueNotNull(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + float_attribute = 1.23 + int_attribute = 123 + list_attribute = ["value1", "value2"] + list_nested_block { + list_nested_block_attribute = "str" + } + map_attribute = { + key1 = "value1" + } + set_attribute = ["value1", "value2"] + set_nested_block { + set_nested_block_attribute = "str" + } + string_attribute = "str" + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("bool_attribute"), + knownvalue.NotNull(), + ), + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("float_attribute"), + knownvalue.NotNull(), + ), + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("int_attribute"), + knownvalue.NotNull(), + ), + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_attribute"), + knownvalue.NotNull(), + ), + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_nested_block"), + knownvalue.ListSizeExact(1), + ), + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("map_attribute"), + knownvalue.NotNull(), + ), + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_attribute"), + knownvalue.NotNull(), + ), + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_nested_block"), + knownvalue.SetSizeExact(1), + ), + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("string_attribute"), + knownvalue.NotNull(), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_Bool(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("bool_attribute"), + knownvalue.Bool(true), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_Bool_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("bool_attribute"), + knownvalue.Float64Exact(1.23), + ), + }, + ExpectError: regexp.MustCompile(`expected json\.Number value for Float64Exact check, got: bool`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_Bool_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("bool_attribute"), + knownvalue.Bool(false), + ), + }, + ExpectError: regexp.MustCompile("expected value false for Bool check, got: true"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_Float64(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + float_attribute = 1.23 + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("float_attribute"), + knownvalue.Float64Exact(1.23), + ), + }, + }, + }, + }) +} + +// We do not need equivalent tests for Int64 and Number as they all test the same logic. +func TestExpectKnownOutputValueAtPath_CheckQuery_Float64_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + float_attribute = 1.23 + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("float_attribute"), + knownvalue.StringExact("str"), + ), + }, + ExpectError: regexp.MustCompile(`expected string value for StringExact check, got: json\.Number`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_Float64_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + float_attribute = 1.23 + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("float_attribute"), + knownvalue.Float64Exact(3.21), + ), + }, + ExpectError: regexp.MustCompile("expected value 3.21 for Float64Exact check, got: 1.23"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_Int64(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("int_attribute"), + knownvalue.Int64Exact(123), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_Int64_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("int_attribute"), + knownvalue.Int64Exact(321), + ), + }, + ExpectError: regexp.MustCompile("expected value 321 for Int64Exact check, got: 123"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_List(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_attribute"), + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("value1"), + knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_List_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_attribute"), + knownvalue.MapExact(map[string]knownvalue.Check{}), + ), + }, + ExpectError: regexp.MustCompile(`expected map\[string\]any value for MapExact check, got: \[\]interface {}`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_List_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_attribute"), + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("value3"), + knownvalue.StringExact("value4"), + }), + ), + }, + ExpectError: regexp.MustCompile(`list element index 0: expected value value3 for StringExact check, got: value1`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_ListPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_attribute"), + knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.StringExact("value1"), + }), + ), + }, + }, + }, + }) +} + +// No need to check KnownValueWrongType for ListPartial as all lists, and sets are []any in +// tfjson.Query. +func TestExpectKnownOutputValueAtPath_CheckQuery_ListPartial_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_attribute"), + knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.StringExact("value3"), + }), + ), + }, + ExpectError: regexp.MustCompile(`list element 0: expected value value3 for StringExact check, got: value1`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_ListElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_attribute"), + knownvalue.ListSizeExact(2), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_ListElements_WrongNum(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_attribute"), + knownvalue.ListSizeExact(3), + ), + }, + ExpectError: regexp.MustCompile("expected 3 elements for ListSizeExact check, got 2 elements"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_ListNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_nested_block"), + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("str"), + }), + knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_ListNestedBlockPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_nested_block"), + knownvalue.ListPartial(map[int]knownvalue.Check{ + 1: knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_ListNestedBlockElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_nested_block"), + knownvalue.ListSizeExact(2), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_Map(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("map_attribute"), + knownvalue.MapExact(map[string]knownvalue.Check{ + "key1": knownvalue.StringExact("value1"), + "key2": knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_Map_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("map_attribute"), + knownvalue.ListExact([]knownvalue.Check{}), + ), + }, + ExpectError: regexp.MustCompile(`expected \[\]any value for ListExact check, got: map\[string\]interface {}`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_Map_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("map_attribute"), + knownvalue.MapExact(map[string]knownvalue.Check{ + "key3": knownvalue.StringExact("value3"), + "key4": knownvalue.StringExact("value4"), + }), + ), + }, + ExpectError: regexp.MustCompile(`missing element key3 for MapExact check`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_MapPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("map_attribute"), + knownvalue.MapPartial(map[string]knownvalue.Check{ + "key1": knownvalue.StringExact("value1"), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_MapPartial_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("map_attribute"), + knownvalue.MapPartial(map[string]knownvalue.Check{ + "key3": knownvalue.StringExact("value1"), + }), + ), + }, + ExpectError: regexp.MustCompile(`missing element key3 for MapPartial check`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_MapElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("map_attribute"), + knownvalue.MapSizeExact(2), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_MapElements_WrongNum(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("map_attribute"), + knownvalue.MapSizeExact(3), + ), + }, + ExpectError: regexp.MustCompile("expected 3 elements for MapSizeExact check, got 2 elements"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_Number(t *testing.T) { + t.Parallel() + + f, _, err := big.ParseFloat("123", 10, 512, big.ToNearestEven) + + if err != nil { + t.Errorf("%s", err) + } + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("int_attribute"), + knownvalue.NumberExact(f), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_Number_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + f, _, err := big.ParseFloat("321", 10, 512, big.ToNearestEven) + + if err != nil { + t.Errorf("%s", err) + } + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("int_attribute"), + knownvalue.NumberExact(f), + ), + }, + ExpectError: regexp.MustCompile("expected value 321 for NumberExact check, got: 123"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_Set(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_attribute"), + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.StringExact("value1"), + knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_Set_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_attribute"), + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.StringExact("value1"), + knownvalue.StringExact("value3"), + }), + ), + }, + ExpectError: regexp.MustCompile(`missing value value3 for SetExact check`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_SetPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_attribute"), + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_SetPartial_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_attribute"), + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.StringExact("value3"), + }), + ), + }, + ExpectError: regexp.MustCompile(`missing value value3 for SetPartial check`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_SetElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_attribute"), + knownvalue.SetSizeExact(2), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_SetNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_nested_block"), + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("str"), + }), + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_SetNestedBlockPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_nested_block"), + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_SetNestedBlockElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_nested_block"), + knownvalue.SetSizeExact(2), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_String(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("string_attribute"), + knownvalue.StringExact("str")), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_String_Custom(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "string" + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("string_attribute"), + StringContains("str")), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_String_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("string_attribute"), + knownvalue.Bool(true)), + }, + ExpectError: regexp.MustCompile("expected bool value for Bool check, got: string"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_String_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("string_attribute"), + knownvalue.StringExact("rts")), + }, + ExpectError: regexp.MustCompile("expected value rts for StringExact check, got: str"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckQuery_UnknownAttributeType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + knownValue knownvalue.Check + req querycheck.CheckQueryRequest + expectedErr error + }{ + "unrecognised-type": { + knownValue: knownvalue.Int64Exact(123), + req: querycheck.CheckQueryRequest{ + Query: &tfjson.Query{ + Values: &tfjson.QueryValues{ + Outputs: map[string]*tfjson.QueryOutput{ + "obj": { + Value: map[string]any{ + "float32_output": float32(123), + }, + }, + }, + }, + }, + }, + expectedErr: fmt.Errorf("error checking value for output at path: obj.float32_output, err: expected json.Number value for Int64Exact check, got: float32"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + e := querycheck.ExpectKnownOutputValueAtPath("obj", tfjsonpath.New("float32_output"), testCase.knownValue) + + resp := querycheck.CheckQueryResponse{} + + e.CheckQuery(context.Background(), testCase.req, &resp) + + if diff := cmp.Diff(resp.Error, testCase.expectedErr, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/querycheck/expect_known_output_value_example_test.go b/querycheck/expect_known_output_value_example_test.go new file mode 100644 index 000000000..b3107a16d --- /dev/null +++ b/querycheck/expect_known_output_value_example_test.go @@ -0,0 +1,40 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/querycheck" +) + +func ExampleExpectKnownOutputValue() { + // A typical test would accept *testing.T as a function parameter, for instance `func TestSomething(t *testing.T) { ... }`. + t := &testing.T{} + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + output bool_output { + value = test_resource.one.bool_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "bool_output", + knownvalue.Bool(true), + ), + }, + }, + }, + }) +} diff --git a/querycheck/expect_known_output_value_test.go b/querycheck/expect_known_output_value_test.go new file mode 100644 index 000000000..d8c2bcb3a --- /dev/null +++ b/querycheck/expect_known_output_value_test.go @@ -0,0 +1,1562 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck_test + +import ( + "context" + "fmt" + "math/big" + "regexp" + "testing" + + "github.com/google/go-cmp/cmp" + tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/querycheck" +) + +func TestExpectKnownOutputValue_CheckQuery_OutputNotFound(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + output bool_output { + value = test_resource.one.bool_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "bool_not_found", + knownvalue.Bool(true), + ), + }, + ExpectError: regexp.MustCompile("bool_not_found - Output not found in query"), + }, + }, + }) +} + +// TestExpectKnownOutputValue_CheckQuery_AttributeValueNull shows that outputs that reference +// null values do not appear in query. Indicating that there is no way to discriminate +// between null outputs and non-existent outputs. +// Reference: https://github.com/hashicorp/terraform/issues/34080 +func TestExpectKnownOutputValue_CheckQuery_AttributeValueNull(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" {} + output bool_output { + value = test_resource.one.bool_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "bool_output", + knownvalue.Bool(true), + ), + }, + ExpectError: regexp.MustCompile("bool_output - Output not found in query"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_AttributeValueNotNull(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + float_attribute = 1.23 + int_attribute = 123 + list_attribute = ["value1", "value2"] + list_nested_block { + list_nested_block_attribute = "str" + } + map_attribute = { + key1 = "value1" + } + set_attribute = ["value1", "value2"] + set_nested_block { + set_nested_block_attribute = "str" + } + string_attribute = "str" + } + output bool_output { + value = test_resource.one.bool_attribute + } + output float64_output { + value = test_resource.one.float_attribute + } + output int64_output { + value = test_resource.one.int_attribute + } + output list_output { + value = test_resource.one.list_attribute + } + output list_nested_block_output { + value = test_resource.one.list_nested_block + } + output map_output { + value = test_resource.one.map_attribute + } + output set_output { + value = test_resource.one.set_attribute + } + output set_nested_block_output { + value = test_resource.one.set_nested_block + } + output string_output { + value = test_resource.one.string_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "bool_output", + knownvalue.NotNull(), + ), + querycheck.ExpectKnownOutputValue( + "float64_output", + knownvalue.NotNull(), + ), + querycheck.ExpectKnownOutputValue( + "int64_output", + knownvalue.NotNull(), + ), + querycheck.ExpectKnownOutputValue( + "list_output", + knownvalue.NotNull(), + ), + querycheck.ExpectKnownOutputValue( + "list_nested_block_output", + knownvalue.ListSizeExact(1), + ), + querycheck.ExpectKnownOutputValue( + "map_output", + knownvalue.NotNull(), + ), + querycheck.ExpectKnownOutputValue( + "set_output", + knownvalue.NotNull(), + ), + querycheck.ExpectKnownOutputValue( + "set_nested_block_output", + knownvalue.SetSizeExact(1), + ), + querycheck.ExpectKnownOutputValue( + "string_output", + knownvalue.NotNull(), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_Bool(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + output bool_output { + value = test_resource.one.bool_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "bool_output", + knownvalue.Bool(true), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_Bool_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + output bool_output { + value = test_resource.one.bool_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "bool_output", + knownvalue.Float64Exact(1.23), + ), + }, + ExpectError: regexp.MustCompile(`expected json\.Number value for Float64Exact check, got: bool`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_Bool_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + output bool_output { + value = test_resource.one.bool_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "bool_output", + knownvalue.Bool(false), + ), + }, + ExpectError: regexp.MustCompile("expected value false for Bool check, got: true"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_Float64(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + float_attribute = 1.23 + } + + output float64_output { + value = test_resource.one.float_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "float64_output", + knownvalue.Float64Exact(1.23), + ), + }, + }, + }, + }) +} + +// We do not need equivalent tests for Int64 and Number as they all test the same logic. +func TestExpectKnownOutputValue_CheckQuery_Float64_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + float_attribute = 1.23 + } + + output float64_output { + value = test_resource.one.float_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "float64_output", + knownvalue.StringExact("str"), + ), + }, + ExpectError: regexp.MustCompile(`expected string value for StringExact check, got: json\.Number`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_Float64_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + float_attribute = 1.23 + } + + output float64_output { + value = test_resource.one.float_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "float64_output", + knownvalue.Float64Exact(3.21), + ), + }, + ExpectError: regexp.MustCompile("expected value 3.21 for Float64Exact check, got: 1.23"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_Int64(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + + output int64_output { + value = test_resource.one.int_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "int64_output", + knownvalue.Int64Exact(123), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_Int64_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + + output int64_output { + value = test_resource.one.int_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "int64_output", + knownvalue.Int64Exact(321), + ), + }, + ExpectError: regexp.MustCompile("expected value 321 for Int64Exact check, got: 123"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_List(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output list_output { + value = test_resource.one.list_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "list_output", + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("value1"), + knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_List_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output list_output { + value = test_resource.one.list_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "list_output", + knownvalue.MapExact(map[string]knownvalue.Check{}), + ), + }, + ExpectError: regexp.MustCompile(`expected map\[string\]any value for MapExact check, got: \[\]interface {}`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_List_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output list_output { + value = test_resource.one.list_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "list_output", + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("value3"), + knownvalue.StringExact("value4"), + }), + ), + }, + ExpectError: regexp.MustCompile(`list element index 0: expected value value3 for StringExact check, got: value1`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_ListPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output list_output { + value = test_resource.one.list_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "list_output", + knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.StringExact("value1"), + }), + ), + }, + }, + }, + }) +} + +// No need to check KnownValueWrongType for ListPartial as all lists, and sets are []any in +// tfjson.Query. +func TestExpectKnownOutputValue_CheckQuery_ListPartial_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output list_output { + value = test_resource.one.list_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "list_output", + knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.StringExact("value3"), + }), + ), + }, + ExpectError: regexp.MustCompile(`list element 0: expected value value3 for StringExact check, got: value1`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_ListElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output list_output { + value = test_resource.one.list_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "list_output", + knownvalue.ListSizeExact(2), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_ListElements_WrongNum(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output list_output { + value = test_resource.one.list_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "list_output", + knownvalue.ListSizeExact(3), + ), + }, + ExpectError: regexp.MustCompile("expected 3 elements for ListSizeExact check, got 2 elements"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_ListNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + + output list_nested_block_output { + value = test_resource.one.list_nested_block + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "list_nested_block_output", + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("str"), + }), + knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_ListNestedBlockPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + + output list_nested_block_output { + value = test_resource.one.list_nested_block + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "list_nested_block_output", + knownvalue.ListPartial(map[int]knownvalue.Check{ + 1: knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_ListNestedBlockElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + + output list_nested_block_output { + value = test_resource.one.list_nested_block + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "list_nested_block_output", + knownvalue.ListSizeExact(2), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_Map(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output map_output { + value = test_resource.one.map_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "map_output", + knownvalue.MapExact(map[string]knownvalue.Check{ + "key1": knownvalue.StringExact("value1"), + "key2": knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_Map_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output map_output { + value = test_resource.one.map_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "map_output", + knownvalue.ListExact([]knownvalue.Check{}), + ), + }, + ExpectError: regexp.MustCompile(`expected \[\]any value for ListExact check, got: map\[string\]interface {}`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_Map_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output map_output { + value = test_resource.one.map_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "map_output", + knownvalue.MapExact(map[string]knownvalue.Check{ + "key3": knownvalue.StringExact("value3"), + "key4": knownvalue.StringExact("value4"), + }), + ), + }, + ExpectError: regexp.MustCompile(`missing element key3 for MapExact check`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_MapPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output map_output { + value = test_resource.one.map_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "map_output", + knownvalue.MapPartial(map[string]knownvalue.Check{ + "key1": knownvalue.StringExact("value1"), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_MapPartial_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output map_output { + value = test_resource.one.map_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "map_output", + knownvalue.MapPartial(map[string]knownvalue.Check{ + "key3": knownvalue.StringExact("value1"), + }), + ), + }, + ExpectError: regexp.MustCompile(`missing element key3 for MapPartial check`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_MapElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output map_output { + value = test_resource.one.map_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "map_output", + knownvalue.MapSizeExact(2), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_MapElements_WrongNum(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output map_output { + value = test_resource.one.map_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "map_output", + knownvalue.MapSizeExact(3), + ), + }, + ExpectError: regexp.MustCompile("expected 3 elements for MapSizeExact check, got 2 elements"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_Number(t *testing.T) { + t.Parallel() + + f, _, err := big.ParseFloat("123", 10, 512, big.ToNearestEven) + + if err != nil { + t.Errorf("%s", err) + } + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + + output int64_output { + value = test_resource.one.int_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "int64_output", + knownvalue.NumberExact(f), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_Number_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + f, _, err := big.ParseFloat("321", 10, 512, big.ToNearestEven) + + if err != nil { + t.Errorf("%s", err) + } + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + + output int64_output { + value = test_resource.one.int_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "int64_output", + knownvalue.NumberExact(f), + ), + }, + ExpectError: regexp.MustCompile("expected value 321 for NumberExact check, got: 123"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_Set(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + + output set_output { + value = test_resource.one.set_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "set_output", + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.StringExact("value1"), + knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_Set_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + + output set_output { + value = test_resource.one.set_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "set_output", + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.StringExact("value1"), + knownvalue.StringExact("value3"), + }), + ), + }, + ExpectError: regexp.MustCompile(`missing value value3 for SetExact check`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_SetPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + + output set_output { + value = test_resource.one.set_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "set_output", + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_SetPartial_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + + output set_output { + value = test_resource.one.set_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "set_output", + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.StringExact("value3"), + }), + ), + }, + ExpectError: regexp.MustCompile(`missing value value3 for SetPartial check`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_SetElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + + output set_output { + value = test_resource.one.set_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "set_output", + knownvalue.SetSizeExact(2), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_SetNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + + output set_nested_block_output { + value = test_resource.one.set_nested_block + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "set_nested_block_output", + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("str"), + }), + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_SetNestedBlockPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + + output set_nested_block_output { + value = test_resource.one.set_nested_block + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "set_nested_block_output", + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_SetNestedBlockElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + + output set_nested_block_output { + value = test_resource.one.set_nested_block + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "set_nested_block_output", + knownvalue.SetSizeExact(2), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_String(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + output string_output { + value = test_resource.one.string_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "string_output", + knownvalue.StringExact("str")), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_String_Custom(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "string" + } + + output string_output { + value = test_resource.one.string_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "string_output", + StringContains("str")), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_String_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + output string_output { + value = test_resource.one.string_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "string_output", + knownvalue.Bool(true)), + }, + ExpectError: regexp.MustCompile("expected bool value for Bool check, got: string"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_String_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + output string_output { + value = test_resource.one.string_attribute + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownOutputValue( + "string_output", + knownvalue.StringExact("rts")), + }, + ExpectError: regexp.MustCompile("expected value rts for StringExact check, got: str"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckQuery_UnknownAttributeType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + knownValue knownvalue.Check + req querycheck.CheckQueryRequest + expectedErr error + }{ + "unrecognised-type": { + knownValue: knownvalue.Int64Exact(123), + req: querycheck.CheckQueryRequest{ + Query: &tfjson.Query{ + Values: &tfjson.QueryValues{ + Outputs: map[string]*tfjson.QueryOutput{ + "float32_output": { + Value: float32(123), + }, + }, + }, + }, + }, + expectedErr: fmt.Errorf("error checking value for output at path: float32_output, err: expected json.Number value for Int64Exact check, got: float32"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + e := querycheck.ExpectKnownOutputValue("float32_output", testCase.knownValue) + + resp := querycheck.CheckQueryResponse{} + + e.CheckQuery(context.Background(), testCase.req, &resp) + + if diff := cmp.Diff(resp.Error, testCase.expectedErr, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/querycheck/expect_known_value.go b/querycheck/expect_known_value.go new file mode 100644 index 000000000..4ea8ba91c --- /dev/null +++ b/querycheck/expect_known_value.go @@ -0,0 +1,84 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "fmt" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +// Resource Query Check +var _ QueryCheck = expectKnownValue{} + +type expectKnownValue struct { + resourceAddress string + attributePath tfjsonpath.Path + knownValue knownvalue.Check +} + +// CheckQuery implements the query check logic. +func (e expectKnownValue) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { + var resource *tfjson.QueryResource + + if req.Query == nil { + resp.Error = fmt.Errorf("query is nil") + + return + } + + if req.Query.Values == nil { + resp.Error = fmt.Errorf("query does not contain any query values") + + return + } + + if req.Query.Values.RootModule == nil { + resp.Error = fmt.Errorf("query does not contain a root module") + + return + } + + for _, r := range req.Query.Values.RootModule.Resources { + if e.resourceAddress == r.Address { + resource = r + + break + } + } + + if resource == nil { + resp.Error = fmt.Errorf("%s - Resource not found in query", e.resourceAddress) + + return + } + + result, err := tfjsonpath.Traverse(resource.AttributeValues, e.attributePath) + + if err != nil { + resp.Error = err + + return + } + + if err := e.knownValue.CheckValue(result); err != nil { + resp.Error = fmt.Errorf("error checking value for attribute at path: %s.%s, err: %s", e.resourceAddress, e.attributePath.String(), err) + + return + } +} + +// ExpectKnownValue returns a query check that asserts that the specified attribute at the given resource +// has a known type and value. +func ExpectKnownValue(resourceAddress string, attributePath tfjsonpath.Path, knownValue knownvalue.Check) QueryCheck { + return expectKnownValue{ + resourceAddress: resourceAddress, + attributePath: attributePath, + knownValue: knownValue, + } +} diff --git a/querycheck/expect_known_value_example_test.go b/querycheck/expect_known_value_example_test.go new file mode 100644 index 000000000..3c8d078e3 --- /dev/null +++ b/querycheck/expect_known_value_example_test.go @@ -0,0 +1,38 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/querycheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func ExampleExpectKnownValue() { + // A typical test would accept *testing.T as a function parameter, for instance `func TestSomething(t *testing.T) { ... }`. + t := &testing.T{} + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + knownvalue.Bool(true), + ), + }, + }, + }, + }) +} diff --git a/querycheck/expect_known_value_test.go b/querycheck/expect_known_value_test.go new file mode 100644 index 000000000..e31648f9b --- /dev/null +++ b/querycheck/expect_known_value_test.go @@ -0,0 +1,1655 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck_test + +import ( + "context" + "fmt" + "math/big" + "regexp" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/querycheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestExpectKnownValue_CheckQuery_ResourceNotFound(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.two", + tfjsonpath.New("bool_attribute"), + knownvalue.Bool(true), + ), + }, + ExpectError: regexp.MustCompile("test_resource.two - Resource not found in query"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_AttributeValueNull(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + knownvalue.Null(), + ), + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("float_attribute"), + knownvalue.Null(), + ), + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("int_attribute"), + knownvalue.Null(), + ), + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.Null(), + ), + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_nested_block"), + knownvalue.ListExact([]knownvalue.Check{}), + ), + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.Null(), + ), + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_attribute"), + knownvalue.Null(), + ), + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_nested_block"), + knownvalue.SetExact([]knownvalue.Check{}), + ), + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("string_attribute"), + knownvalue.Null(), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_AttributeValueNotNull(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + float_attribute = 1.23 + int_attribute = 123 + list_attribute = ["value1", "value2"] + list_nested_block { + list_nested_block_attribute = "str" + } + map_attribute = { + key1 = "value1" + } + set_attribute = ["value1", "value2"] + set_nested_block { + set_nested_block_attribute = "str" + } + string_attribute = "str" + }`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + knownvalue.NotNull(), + ), + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("float_attribute"), + knownvalue.NotNull(), + ), + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("int_attribute"), + knownvalue.NotNull(), + ), + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.NotNull(), + ), + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_nested_block"), + knownvalue.ListSizeExact(1), + ), + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.NotNull(), + ), + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_attribute"), + knownvalue.NotNull(), + ), + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_nested_block"), + knownvalue.SetSizeExact(1), + ), + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("string_attribute"), + knownvalue.NotNull(), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_Bool(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + knownvalue.Bool(true), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_Bool_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + knownvalue.Float64Exact(1.23), + ), + }, + ExpectError: regexp.MustCompile(`expected json\.Number value for Float64Exact check, got: bool`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_Bool_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + knownvalue.Bool(false), + ), + }, + ExpectError: regexp.MustCompile("expected value false for Bool check, got: true"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_Float64(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + float_attribute = 1.23 + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("float_attribute"), + knownvalue.Float64Exact(1.23), + ), + }, + }, + }, + }) +} + +// We do not need equivalent tests for Int64 and Number as they all test the same logic. +func TestExpectKnownValue_CheckQuery_Float64_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + float_attribute = 1.23 + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("float_attribute"), + knownvalue.StringExact("str"), + ), + }, + ExpectError: regexp.MustCompile(`expected string value for StringExact check, got: json\.Number`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_Float64_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + float_attribute = 1.23 + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("float_attribute"), + knownvalue.Float64Exact(3.21), + ), + }, + ExpectError: regexp.MustCompile("expected value 3.21 for Float64Exact check, got: 1.23"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_Int64(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("int_attribute"), + knownvalue.Int64Exact(123), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_Int64_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("int_attribute"), + knownvalue.Int64Exact(321), + ), + }, + ExpectError: regexp.MustCompile("expected value 321 for Int64Exact check, got: 123"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_List(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("value1"), + knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_List_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.MapExact(map[string]knownvalue.Check{}), + ), + }, + ExpectError: regexp.MustCompile(`expected map\[string\]any value for MapExact check, got: \[\]interface {}`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_List_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("value3"), + knownvalue.StringExact("value4"), + }), + ), + }, + ExpectError: regexp.MustCompile(`list element index 0: expected value value3 for StringExact check, got: value1`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_ListPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.StringExact("value1"), + }), + ), + }, + }, + }, + }) +} + +// No need to check KnownValueWrongType for ListPartial as all lists, and sets are []any in +// tfjson.Query. +func TestExpectKnownValue_CheckQuery_ListPartial_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.StringExact("value3"), + }), + ), + }, + ExpectError: regexp.MustCompile(`list element 0: expected value value3 for StringExact check, got: value1`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_ListElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.ListSizeExact(2), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_ListElements_WrongNum(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.ListSizeExact(3), + ), + }, + ExpectError: regexp.MustCompile("expected 3 elements for ListSizeExact check, got 2 elements"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_ListNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_nested_block"), + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("str"), + }), + knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_ListNestedBlockPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_nested_block"), + knownvalue.ListPartial(map[int]knownvalue.Check{ + 1: knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_ListNestedBlockElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_nested_block"), + knownvalue.ListSizeExact(2), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_Map(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.MapExact(map[string]knownvalue.Check{ + "key1": knownvalue.StringExact("value1"), + "key2": knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_Map_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.ListExact([]knownvalue.Check{}), + ), + }, + ExpectError: regexp.MustCompile(`expected \[\]any value for ListExact check, got: map\[string\]interface {}`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_Map_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.MapExact(map[string]knownvalue.Check{ + "key3": knownvalue.StringExact("value3"), + "key4": knownvalue.StringExact("value4"), + }), + ), + }, + ExpectError: regexp.MustCompile(`missing element key3 for MapExact check`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_MapPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.MapPartial(map[string]knownvalue.Check{ + "key1": knownvalue.StringExact("value1"), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_MapPartial_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.MapPartial(map[string]knownvalue.Check{ + "key3": knownvalue.StringExact("value1"), + }), + ), + }, + ExpectError: regexp.MustCompile(`missing element key3 for MapPartial check`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_MapElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.MapSizeExact(2), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_MapElements_WrongNum(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.MapSizeExact(3), + ), + }, + ExpectError: regexp.MustCompile("expected 3 elements for MapSizeExact check, got 2 elements"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_Number(t *testing.T) { + t.Parallel() + + f, _, err := big.ParseFloat("123", 10, 512, big.ToNearestEven) + + if err != nil { + t.Errorf("%s", err) + } + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("int_attribute"), + knownvalue.NumberExact(f), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_Number_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + f, _, err := big.ParseFloat("321", 10, 512, big.ToNearestEven) + + if err != nil { + t.Errorf("%s", err) + } + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("int_attribute"), + knownvalue.NumberExact(f), + ), + }, + ExpectError: regexp.MustCompile("expected value 321 for NumberExact check, got: 123"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_Set(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_attribute"), + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.StringExact("value2"), + knownvalue.StringExact("value1"), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_Set_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_attribute"), + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.StringExact("value1"), + knownvalue.StringExact("value3"), + }), + ), + }, + ExpectError: regexp.MustCompile(`missing value value3 for SetExact check`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_SetPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_attribute"), + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_SetPartial_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_attribute"), + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.StringExact("value3"), + }), + ), + }, + ExpectError: regexp.MustCompile(`missing value value3 for SetPartial check`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_SetElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_attribute"), + knownvalue.SetSizeExact(2), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_SetNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_nested_block"), + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("str"), + }), + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_SetNestedBlock_Custom(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_block { + set_nested_block_attribute = "string" + } + set_nested_block { + set_nested_block_attribute = "girts" + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_nested_block"), + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": StringContains("str"), + }), + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": StringContains("rts"), + }), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_SetNestedBlockPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_nested_block"), + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_SetNestedBlockElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_nested_block"), + knownvalue.SetSizeExact(2), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_String(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("string_attribute"), + knownvalue.StringExact("str")), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_String_Custom(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "string" + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("string_attribute"), + StringContains("tri")), + }, + }, + }, + }) +} + +var _ knownvalue.Check = stringContains{} + +type stringContains struct { + value string +} + +func (v stringContains) CheckValue(other any) error { + otherVal, ok := other.(string) + + if !ok { + return fmt.Errorf("expected string value for StringContains check, got: %T", other) + } + + if !strings.Contains(otherVal, v.value) { + return fmt.Errorf("expected string %q to contain %q for StringContains check", otherVal, v.value) + } + + return nil +} + +func (v stringContains) String() string { + return v.value +} + +func StringContains(value string) stringContains { + return stringContains{ + value: value, + } +} + +func TestExpectKnownValue_CheckQuery_String_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("string_attribute"), + knownvalue.Bool(true)), + }, + ExpectError: regexp.MustCompile("expected bool value for Bool check, got: string"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_String_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("string_attribute"), + knownvalue.StringExact("rts")), + }, + ExpectError: regexp.MustCompile("expected value rts for StringExact check, got: str"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckQuery_UnknownAttributeType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + knownValue knownvalue.Check + req querycheck.CheckQueryRequest + expectedErr error + }{ + "unrecognised-type": { + knownValue: knownvalue.Int64Exact(123), + req: querycheck.CheckQueryRequest{ + Query: &tfjson.Query{ + Values: &tfjson.QueryValues{ + RootModule: &tfjson.QueryModule{ + Resources: []*tfjson.QueryResource{ + { + Address: "example_resource.test", + AttributeValues: map[string]any{ + "attribute": float32(123), + }, + }, + }, + }, + }, + }, + }, + expectedErr: fmt.Errorf("error checking value for attribute at path: example_resource.test.attribute, err: expected json.Number value for Int64Exact check, got: float32"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + e := querycheck.ExpectKnownValue("example_resource.test", tfjsonpath.New("attribute"), testCase.knownValue) + + resp := querycheck.CheckQueryResponse{} + + e.CheckQuery(context.Background(), testCase.req, &resp) + + if diff := cmp.Diff(resp.Error, testCase.expectedErr, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +var equateErrorMessage = cmp.Comparer(func(x, y error) bool { + if x == nil || y == nil { + return x == nil && y == nil + } + + return x.Error() == y.Error() +}) + +func testProvider() *schema.Provider { + return &schema.Provider{ + ResourcesMap: map[string]*schema.Resource{ + "test_resource": { + CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + d.SetId("test") + + err := d.Set("string_computed_attribute", "computed") + if err != nil { + return diag.Errorf("error setting string_computed_attribute: %s", err) + } + + return nil + }, + UpdateContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + Schema: map[string]*schema.Schema{ + "bool_attribute": { + Optional: true, + Type: schema.TypeBool, + }, + "float_attribute": { + Optional: true, + Type: schema.TypeFloat, + }, + "int_attribute": { + Optional: true, + Type: schema.TypeInt, + }, + "list_attribute": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + }, + "list_nested_block": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "list_nested_block_attribute": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "map_attribute": { + Type: schema.TypeMap, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + }, + "set_attribute": { + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + }, + "set_nested_block": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "set_nested_block_attribute": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "set_nested_nested_block": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "set_nested_block": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "set_nested_block_attribute": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + "string_attribute": { + Optional: true, + Type: schema.TypeString, + }, + "string_computed_attribute": { + Computed: true, + Type: schema.TypeString, + }, + }, + }, + }, + } +} diff --git a/querycheck/expect_sensitive_value.go b/querycheck/expect_sensitive_value.go new file mode 100644 index 000000000..b0837c5d8 --- /dev/null +++ b/querycheck/expect_sensitive_value.go @@ -0,0 +1,101 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "encoding/json" + "fmt" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +var _ QueryCheck = expectSensitiveValue{} + +type expectSensitiveValue struct { + resourceAddress string + attributePath tfjsonpath.Path +} + +// CheckQuery implements the query check logic. +func (e expectSensitiveValue) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { + var resource *tfjson.QueryResource + + if req.Query == nil { + resp.Error = fmt.Errorf("query is nil") + + return + } + + if req.Query.Values == nil { + resp.Error = fmt.Errorf("query does not contain any query values") + + return + } + + if req.Query.Values.RootModule == nil { + resp.Error = fmt.Errorf("query does not contain a root module") + + return + } + + for _, r := range req.Query.Values.RootModule.Resources { + if e.resourceAddress == r.Address { + resource = r + + break + } + } + + if resource == nil { + resp.Error = fmt.Errorf("%s - Resource not found in query", e.resourceAddress) + + return + } + + var data map[string]any + + err := json.Unmarshal(resource.SensitiveValues, &data) + + if err != nil { + resp.Error = fmt.Errorf("could not unmarshal SensitiveValues: %s", err) + + return + } + + result, err := tfjsonpath.Traverse(data, e.attributePath) + + if err != nil { + resp.Error = err + + return + } + + isSensitive, ok := result.(bool) + if !ok { + resp.Error = fmt.Errorf("invalid path: the path value cannot be asserted as bool") + + return + } + + if !isSensitive { + resp.Error = fmt.Errorf("attribute at path is not sensitive") + + return + } +} + +// ExpectSensitiveValue returns a query check that asserts that the specified attribute at the given resource has a sensitive value. +// +// Due to implementation differences between the terraform-plugin-sdk and the terraform-plugin-framework, representation of sensitive +// values may differ. For example, terraform-plugin-sdk based providers may have less precise representations of sensitive values, such +// as marking whole maps as sensitive rather than individual element values. +func ExpectSensitiveValue(resourceAddress string, attributePath tfjsonpath.Path) QueryCheck { + return expectSensitiveValue{ + resourceAddress: resourceAddress, + attributePath: attributePath, + } +} diff --git a/querycheck/expect_sensitive_value_test.go b/querycheck/expect_sensitive_value_test.go new file mode 100644 index 000000000..4b575571c --- /dev/null +++ b/querycheck/expect_sensitive_value_test.go @@ -0,0 +1,308 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck_test + +import ( + "context" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/querycheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func Test_ExpectSensitiveValue_SensitiveStringAttribute(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_4_6), // QueryResource.SensitiveValues + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProviderSensitive(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "one" { + sensitive_string_attribute = "test" + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectSensitiveValue("test_resource.one", + tfjsonpath.New("sensitive_string_attribute")), + }, + }, + }, + }) +} + +func Test_ExpectSensitiveValue_SensitiveListAttribute(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_4_6), // QueryResource.SensitiveValues + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProviderSensitive(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "one" { + sensitive_list_attribute = ["value1"] + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectSensitiveValue("test_resource.one", + tfjsonpath.New("sensitive_list_attribute")), + }, + }, + }, + }) +} + +func Test_ExpectSensitiveValue_SensitiveSetAttribute(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_4_6), // QueryResource.SensitiveValues + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProviderSensitive(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "one" { + sensitive_set_attribute = ["value1"] + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectSensitiveValue("test_resource.one", + tfjsonpath.New("sensitive_set_attribute")), + }, + }, + }, + }) +} + +func Test_ExpectSensitiveValue_SensitiveMapAttribute(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_4_6), // QueryResource.SensitiveValues + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProviderSensitive(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "one" { + sensitive_map_attribute = { + key1 = "value1", + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectSensitiveValue("test_resource.one", + tfjsonpath.New("sensitive_map_attribute")), + }, + }, + }, + }) +} + +func Test_ExpectSensitiveValue_ListNestedBlock_SensitiveAttribute(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_4_6), // QueryResource.SensitiveValues + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProviderSensitive(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "one" { + list_nested_block_sensitive_attribute { + sensitive_list_nested_block_attribute = "sensitive-test" + list_nested_block_attribute = "test" + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectSensitiveValue("test_resource.one", + tfjsonpath.New("list_nested_block_sensitive_attribute").AtSliceIndex(0). + AtMapKey("sensitive_list_nested_block_attribute")), + }, + }, + }, + }) +} + +func Test_ExpectSensitiveValue_SetNestedBlock_SensitiveAttribute(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_4_6), // QueryResource.SensitiveValues + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProviderSensitive(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "one" { + set_nested_block_sensitive_attribute { + sensitive_set_nested_block_attribute = "sensitive-test" + set_nested_block_attribute = "test" + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectSensitiveValue("test_resource.one", + tfjsonpath.New("set_nested_block_sensitive_attribute")), + }, + }, + }, + }) +} + +func Test_ExpectSensitiveValue_ExpectError_ResourceNotFound(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_4_6), // QueryResource.SensitiveValues + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProviderSensitive(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "one" {} + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectSensitiveValue("test_resource.two", tfjsonpath.New("set_attribute")), + }, + ExpectError: regexp.MustCompile(`test_resource.two - Resource not found in query`), + }, + }, + }) +} + +func testProviderSensitive() *schema.Provider { + return &schema.Provider{ + ResourcesMap: map[string]*schema.Resource{ + "test_resource": { + CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + d.SetId("test") + return nil + }, + UpdateContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + Schema: map[string]*schema.Schema{ + "sensitive_string_attribute": { + Sensitive: true, + Optional: true, + Type: schema.TypeString, + }, + "sensitive_list_attribute": { + Sensitive: true, + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + }, + "sensitive_set_attribute": { + Sensitive: true, + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + }, + "sensitive_map_attribute": { + Sensitive: true, + Type: schema.TypeMap, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + }, + "list_nested_block_sensitive_attribute": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "list_nested_block_attribute": { + Type: schema.TypeString, + Optional: true, + }, + "sensitive_list_nested_block_attribute": { + Sensitive: true, + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "set_nested_block_sensitive_attribute": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "set_nested_block_attribute": { + Type: schema.TypeString, + Optional: true, + }, + "sensitive_set_nested_block_attribute": { + Sensitive: true, + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + } +} diff --git a/querycheck/query_check.go b/querycheck/query_check.go new file mode 100644 index 000000000..289c28e55 --- /dev/null +++ b/querycheck/query_check.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + + tfjson "github.com/hashicorp/terraform-json" +) + +// QueryCheck defines an interface for implementing test logic that checks a query file and then returns an error +// if the query file does not match what is expected. +type QueryCheck interface { + // CheckQuery should perform the query check. + CheckQuery(context.Context, CheckQueryRequest, *CheckQueryResponse) +} + +// CheckQueryRequest is a request for an invoke of the CheckQuery function. +type CheckQueryRequest struct { + // Query represents a parsed query file, retrieved via the `terraform show -json` command. + Query *tfjson.Query +} + +// CheckQueryResponse is a response to an invoke of the CheckQuery function. +type CheckQueryResponse struct { + // Error is used to report the failure of a query check assertion and is combined with other QueryCheck errors + // to be reported as a test failure. + Error error +} From 086d38cfcb2462718790e4eec681657f99bd7228 Mon Sep 17 00:00:00 2001 From: Rain Date: Mon, 4 Aug 2025 09:58:36 -0400 Subject: [PATCH 08/45] Outlining the marshalling for filling in terraform-json.Query --- internal/plugintest/working_dir.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/internal/plugintest/working_dir.go b/internal/plugintest/working_dir.go index 0af7d640e..af680f9cb 100644 --- a/internal/plugintest/working_dir.go +++ b/internal/plugintest/working_dir.go @@ -532,7 +532,19 @@ func (wd *WorkingDir) Query(ctx context.Context) ([]string, error) { var buffer bytes.Buffer err := wd.tf.QueryJSON(context.Background(), &buffer) - // Marshall buffer? JSON.mashallto___ terraform-json.Query + // terraform-exec also only exposes the raw json writer for now + // So we have to Marshall buffer something like JSON.mashallto___ for terraform-json.Query + /* The struct Terraform is using to encode a query result: + type QueryResult struct { + Address string `json:"address"` + DisplayName string `json:"display_name"` + Identity map[string]json.RawMessage `json:"identity"` + ResourceType string `json:"resource_type"` + ResourceObject map[string]json.RawMessage `json:"resource_object,omitempty"` + Config string `json:"config,omitempty"` + ImportConfig string `json:"import_config,omitempty"` + } + */ if err != nil { return nil, fmt.Errorf("error running terraform query command: %w", err) From cb0e717fe76d4e536179be29c3ebdbf5ffd7edc9 Mon Sep 17 00:00:00 2001 From: Rain Kwan <91649079+rainkwan@users.noreply.github.com> Date: Thu, 31 Jul 2025 12:48:49 -0400 Subject: [PATCH 09/45] Update testing_new.go --- helper/resource/testing_new.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/helper/resource/testing_new.go b/helper/resource/testing_new.go index 1ff2c6eaf..63c03ca66 100644 --- a/helper/resource/testing_new.go +++ b/helper/resource/testing_new.go @@ -255,7 +255,6 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest testStepConfig = teststep.Configuration(confRequest) if !step.Query { - //fmt.Println("Writing pre-switch configuration:", rawCfg) err = wd.SetConfig(ctx, testStepConfig, step.ConfigVariables) } @@ -603,8 +602,6 @@ func testIDRefresh(ctx context.Context, t testing.T, c TestCase, wd *plugintest. testStepConfigDefer := teststep.Configuration(confRequest) - //fmt.Println("Writing the reset to original configuration:", rawCfg) - err = wd.SetConfig(ctx, testStepConfigDefer, step.ConfigVariables) if err != nil { From f3be28ddb13a659bddff4a7252256dec29d03e12 Mon Sep 17 00:00:00 2001 From: Rain Kwan <91649079+rainkwan@users.noreply.github.com> Date: Mon, 4 Aug 2025 10:05:07 -0400 Subject: [PATCH 10/45] Update helper/resource/query/query_test.go Co-authored-by: stephybun --- helper/resource/query/query_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helper/resource/query/query_test.go b/helper/resource/query/query_test.go index 5fe52e738..4a2c94bb4 100644 --- a/helper/resource/query/query_test.go +++ b/helper/resource/query/query_test.go @@ -19,7 +19,7 @@ func TestQuery(t *testing.T) { r.UnitTest(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), // Query mode requires Terraform 1.13.0 or later + tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ From b5c37cab85fd206b1a037c07b9b68d873ecf23ac Mon Sep 17 00:00:00 2001 From: Rain Kwan <91649079+rainkwan@users.noreply.github.com> Date: Mon, 4 Aug 2025 10:05:14 -0400 Subject: [PATCH 11/45] Update internal/plugintest/working_dir.go Co-authored-by: stephybun --- internal/plugintest/working_dir.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/plugintest/working_dir.go b/internal/plugintest/working_dir.go index af680f9cb..79d653156 100644 --- a/internal/plugintest/working_dir.go +++ b/internal/plugintest/working_dir.go @@ -180,7 +180,7 @@ func (wd *WorkingDir) SetQuery(ctx context.Context, cfg teststep.Config, vars co for _, file := range fi { if file.Mode().IsRegular() { - if filepath.Ext(file.Name()) == ".warioform" || filepath.Ext(file.Name()) == ".json" || filepath.Ext(file.Name()) == ".hcl" { + if filepath.Ext(file.Name()) == ".warioform" || filepath.Ext(file.Name()) == ".json" || filepath.Ext(file.Name()) == ".tfquery.hcl" { err = os.Remove(filepath.Join(d.Name(), file.Name())) if err != nil && !os.IsNotExist(err) { From 896310ecea436219eae69fe76a3f69d511eed40b Mon Sep 17 00:00:00 2001 From: Rain Kwan <91649079+rainkwan@users.noreply.github.com> Date: Mon, 4 Aug 2025 10:05:22 -0400 Subject: [PATCH 12/45] Update internal/teststep/file.go Co-authored-by: stephybun --- internal/teststep/file.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/teststep/file.go b/internal/teststep/file.go index 99cefbd5b..a4862b05d 100644 --- a/internal/teststep/file.go +++ b/internal/teststep/file.go @@ -72,7 +72,7 @@ func (c configurationFile) HasTerraformBlock(ctx context.Context) (bool, error) } func (c configurationFile) WriteQuery(ctx context.Context, dest string) error { - panic("WriteQuery not supported for configurationDirectory") + panic("WriteQuery not supported for configurationFile") } // Write copies file from c.file to destination. From 116911fa893fd7791f01d0fcc3c34772ef20de9e Mon Sep 17 00:00:00 2001 From: Rain Date: Mon, 4 Aug 2025 10:07:47 -0400 Subject: [PATCH 13/45] Updating branch --- internal/plugintest/working_dir.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/plugintest/working_dir.go b/internal/plugintest/working_dir.go index 79d653156..d1606a248 100644 --- a/internal/plugintest/working_dir.go +++ b/internal/plugintest/working_dir.go @@ -535,6 +535,7 @@ func (wd *WorkingDir) Query(ctx context.Context) ([]string, error) { // terraform-exec also only exposes the raw json writer for now // So we have to Marshall buffer something like JSON.mashallto___ for terraform-json.Query /* The struct Terraform is using to encode a query result: + type QueryResult struct { Address string `json:"address"` DisplayName string `json:"display_name"` @@ -544,6 +545,7 @@ func (wd *WorkingDir) Query(ctx context.Context) ([]string, error) { Config string `json:"config,omitempty"` ImportConfig string `json:"import_config,omitempty"` } + */ if err != nil { From fef3bb61bf40af10e0094f45f2dd161c8fe5a9fa Mon Sep 17 00:00:00 2001 From: Rain Date: Mon, 18 Aug 2025 11:02:22 -0400 Subject: [PATCH 14/45] Working on using unmarshalled buffer instead of tfjson --- helper/resource/query/query_checks.go | 5 +- internal/plugintest/working_dir.go | 31 +- internal/testing/testsdk/resource/resource.go | 2 + querycheck/compare_value.go | 26 +- querycheck/compare_value_collection.go | 39 +- querycheck/compare_value_pairs.go | 39 +- querycheck/expect_identity.go | 39 +- querycheck/expect_identity_value.go | 27 +- .../expect_identity_value_matches_query.go | 31 +- ...ct_identity_value_matches_query_at_path.go | 29 +- querycheck/expect_known_output_value.go | 76 - .../expect_known_output_value_at_path.go | 78 - ...known_output_value_at_path_example_test.go | 42 - .../expect_known_output_value_at_path_test.go | 1628 ----------------- .../expect_known_output_value_example_test.go | 40 - querycheck/expect_known_output_value_test.go | 1562 ---------------- querycheck/expect_known_value.go | 25 +- querycheck/expect_known_value_test.go | 19 +- querycheck/expect_sensitive_value.go | 101 - querycheck/expect_sensitive_value_test.go | 308 ---- querycheck/query_check.go | 5 +- 21 files changed, 95 insertions(+), 4057 deletions(-) delete mode 100644 querycheck/expect_known_output_value.go delete mode 100644 querycheck/expect_known_output_value_at_path.go delete mode 100644 querycheck/expect_known_output_value_at_path_example_test.go delete mode 100644 querycheck/expect_known_output_value_at_path_test.go delete mode 100644 querycheck/expect_known_output_value_example_test.go delete mode 100644 querycheck/expect_known_output_value_test.go delete mode 100644 querycheck/expect_sensitive_value.go delete mode 100644 querycheck/expect_sensitive_value_test.go diff --git a/helper/resource/query/query_checks.go b/helper/resource/query/query_checks.go index 6ff4eca42..b5de7c6bb 100644 --- a/helper/resource/query/query_checks.go +++ b/helper/resource/query/query_checks.go @@ -6,14 +6,13 @@ package query import ( "context" "errors" - - tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" "github.com/mitchellh/go-testing-interface" "github.com/hashicorp/terraform-plugin-testing/querycheck" ) -func runQueryChecks(ctx context.Context, t testing.T, query *tfjson.Query, queryChecks []querycheck.QueryCheck) error { +func runQueryChecks(ctx context.Context, t testing.T, query *plugintest.QueryResult, queryChecks []querycheck.QueryCheck) error { t.Helper() var result []error diff --git a/internal/plugintest/working_dir.go b/internal/plugintest/working_dir.go index d1606a248..c2693c99a 100644 --- a/internal/plugintest/working_dir.go +++ b/internal/plugintest/working_dir.go @@ -6,6 +6,7 @@ package plugintest import ( "bytes" "context" + "encoding/json" "fmt" "io" "os" @@ -525,6 +526,16 @@ func (wd *WorkingDir) Schemas(ctx context.Context) (*tfjson.ProviderSchemas, err return providerSchemas, err } +type QueryResult struct { + Address string `json:"address"` + DisplayName string `json:"display_name"` + Identity map[string]json.RawMessage `json:"identity"` + ResourceType string `json:"resource_type"` + ResourceObject map[string]json.RawMessage `json:"resource_object,omitempty"` + Config string `json:"config,omitempty"` + ImportConfig string `json:"import_config,omitempty"` +} + func (wd *WorkingDir) Query(ctx context.Context) ([]string, error) { logging.HelperResourceTrace(ctx, "Calling Terraform CLI providers query command") @@ -532,21 +543,15 @@ func (wd *WorkingDir) Query(ctx context.Context) ([]string, error) { var buffer bytes.Buffer err := wd.tf.QueryJSON(context.Background(), &buffer) - // terraform-exec also only exposes the raw json writer for now - // So we have to Marshall buffer something like JSON.mashallto___ for terraform-json.Query - /* The struct Terraform is using to encode a query result: - - type QueryResult struct { - Address string `json:"address"` - DisplayName string `json:"display_name"` - Identity map[string]json.RawMessage `json:"identity"` - ResourceType string `json:"resource_type"` - ResourceObject map[string]json.RawMessage `json:"resource_object,omitempty"` - Config string `json:"config,omitempty"` - ImportConfig string `json:"import_config,omitempty"` + var results []QueryResult + if err := json.Unmarshal(buffer.Bytes(), &results); err != nil { + return nil, fmt.Errorf("failed to unmarshal query result: %w", err) } - */ + returned := make([]string, len(results)) + for i, r := range results { + returned[i] = r.Address + } if err != nil { return nil, fmt.Errorf("error running terraform query command: %w", err) diff --git a/internal/testing/testsdk/resource/resource.go b/internal/testing/testsdk/resource/resource.go index dc6412d55..2a64e323f 100644 --- a/internal/testing/testsdk/resource/resource.go +++ b/internal/testing/testsdk/resource/resource.go @@ -31,6 +31,7 @@ type CreateResponse struct { Diagnostics []*tfprotov6.Diagnostic NewState tftypes.Value NewIdentity *tftypes.Value + NewQuery tftypes.Value } type DeleteRequest struct { @@ -83,6 +84,7 @@ type ReadResponse struct { Diagnostics []*tfprotov6.Diagnostic NewState tftypes.Value NewIdentity *tftypes.Value + NewQuery tftypes.Value } type SchemaRequest struct{} diff --git a/querycheck/compare_value.go b/querycheck/compare_value.go index 35f6777f4..93cc65f11 100644 --- a/querycheck/compare_value.go +++ b/querycheck/compare_value.go @@ -6,10 +6,8 @@ package querycheck import ( "context" "fmt" - - tfjson "github.com/hashicorp/terraform-json" - "github.com/hashicorp/terraform-plugin-testing/compare" + "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" ) @@ -32,7 +30,7 @@ func (e *compareValue) AddQueryValue(resourceAddress string, attributePath tfjso // CheckQuery implements the query check logic. func (e *compareValue) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { - var resource *tfjson.QueryResource + var resource *plugintest.QueryResult if req.Query == nil { resp.Error = fmt.Errorf("query is nil") @@ -40,14 +38,8 @@ func (e *compareValue) CheckQuery(ctx context.Context, req CheckQueryRequest, re return } - if req.Query.Values == nil { - resp.Error = fmt.Errorf("query does not contain any query values") - - return - } - - if req.Query.Values.RootModule == nil { - resp.Error = fmt.Errorf("query does not contain a root module") + if len(req.Query.Address) == 0 { + resp.Error = fmt.Errorf("query does not contain any address values") return } @@ -66,12 +58,8 @@ func (e *compareValue) CheckQuery(ctx context.Context, req CheckQueryRequest, re resourceAddress := e.resourceAddresses[currentIndex] - for _, r := range req.Query.Values.RootModule.Resources { - if resourceAddress == r.Address { - resource = r - - break - } + if resourceAddress == req.Query.Address { + resource = req.Query } if resource == nil { @@ -88,7 +76,7 @@ func (e *compareValue) CheckQuery(ctx context.Context, req CheckQueryRequest, re attributePath := e.attributePaths[currentIndex] - result, err := tfjsonpath.Traverse(resource.AttributeValues, attributePath) + result, err := tfjsonpath.Traverse(resource.ResourceObject, attributePath) if err != nil { resp.Error = err diff --git a/querycheck/compare_value_collection.go b/querycheck/compare_value_collection.go index fd3a3b3b2..4ca546f7b 100644 --- a/querycheck/compare_value_collection.go +++ b/querycheck/compare_value_collection.go @@ -7,10 +7,9 @@ import ( "context" "errors" "fmt" + "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" "sort" - tfjson "github.com/hashicorp/terraform-json" - "github.com/hashicorp/terraform-plugin-testing/compare" "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" ) @@ -83,8 +82,8 @@ func walkCollectionPath(obj any, paths []tfjsonpath.Path, results []any) ([]any, // CheckQuery implements the query check logic. func (e *compareValueCollection) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { - var resourceOne *tfjson.QueryResource - var resourceTwo *tfjson.QueryResource + var resourceOne *plugintest.QueryResult + var resourceTwo *plugintest.QueryResult if req.Query == nil { resp.Error = fmt.Errorf("query is nil") @@ -92,24 +91,8 @@ func (e *compareValueCollection) CheckQuery(ctx context.Context, req CheckQueryR return } - if req.Query.Values == nil { - resp.Error = fmt.Errorf("query does not contain any query values") - - return - } - - if req.Query.Values.RootModule == nil { - resp.Error = fmt.Errorf("query does not contain a root module") - - return - } - - for _, r := range req.Query.Values.RootModule.Resources { - if e.resourceAddressOne == r.Address { - resourceOne = r - - break - } + if e.resourceAddressOne == req.Query.Address { + resourceOne = req.Query } if resourceOne == nil { @@ -124,7 +107,7 @@ func (e *compareValueCollection) CheckQuery(ctx context.Context, req CheckQueryR return } - resultOne, err := tfjsonpath.Traverse(resourceOne.AttributeValues, e.collectionPath[0]) + resultOne, err := tfjsonpath.Traverse(resourceOne.ResourceObject, e.collectionPath[0]) if err != nil { resp.Error = err @@ -158,12 +141,8 @@ func (e *compareValueCollection) CheckQuery(ctx context.Context, req CheckQueryR return } - for _, r := range req.Query.Values.RootModule.Resources { - if e.resourceAddressTwo == r.Address { - resourceTwo = r - - break - } + if e.resourceAddressTwo == req.Query.Address { + resourceTwo = req.Query } if resourceTwo == nil { @@ -172,7 +151,7 @@ func (e *compareValueCollection) CheckQuery(ctx context.Context, req CheckQueryR return } - resultTwo, err := tfjsonpath.Traverse(resourceTwo.AttributeValues, e.attributePath) + resultTwo, err := tfjsonpath.Traverse(resourceTwo.ResourceObject, e.attributePath) if err != nil { resp.Error = err diff --git a/querycheck/compare_value_pairs.go b/querycheck/compare_value_pairs.go index 7ce055bc4..908b01749 100644 --- a/querycheck/compare_value_pairs.go +++ b/querycheck/compare_value_pairs.go @@ -6,8 +6,7 @@ package querycheck import ( "context" "fmt" - - tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" "github.com/hashicorp/terraform-plugin-testing/compare" "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" @@ -26,8 +25,8 @@ type compareValuePairs struct { // CheckQuery implements the query check logic. func (e *compareValuePairs) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { - var resourceOne *tfjson.QueryResource - var resourceTwo *tfjson.QueryResource + var resourceOne *plugintest.QueryResult + var resourceTwo *plugintest.QueryResult if req.Query == nil { resp.Error = fmt.Errorf("query is nil") @@ -35,24 +34,8 @@ func (e *compareValuePairs) CheckQuery(ctx context.Context, req CheckQueryReques return } - if req.Query.Values == nil { - resp.Error = fmt.Errorf("query does not contain any query values") - - return - } - - if req.Query.Values.RootModule == nil { - resp.Error = fmt.Errorf("query does not contain a root module") - - return - } - - for _, r := range req.Query.Values.RootModule.Resources { - if e.resourceAddressOne == r.Address { - resourceOne = r - - break - } + if e.resourceAddressOne == req.Query.Address { + resourceOne = req.Query } if resourceOne == nil { @@ -61,7 +44,7 @@ func (e *compareValuePairs) CheckQuery(ctx context.Context, req CheckQueryReques return } - resultOne, err := tfjsonpath.Traverse(resourceOne.AttributeValues, e.attributePathOne) + resultOne, err := tfjsonpath.Traverse(resourceOne.ResourceObject, e.attributePathOne) if err != nil { resp.Error = err @@ -69,12 +52,8 @@ func (e *compareValuePairs) CheckQuery(ctx context.Context, req CheckQueryReques return } - for _, r := range req.Query.Values.RootModule.Resources { - if e.resourceAddressTwo == r.Address { - resourceTwo = r - - break - } + if e.resourceAddressTwo == req.Query.Address { + resourceTwo = req.Query } if resourceTwo == nil { @@ -83,7 +62,7 @@ func (e *compareValuePairs) CheckQuery(ctx context.Context, req CheckQueryReques return } - resultTwo, err := tfjsonpath.Traverse(resourceTwo.AttributeValues, e.attributePathTwo) + resultTwo, err := tfjsonpath.Traverse(resourceTwo.ResourceObject, e.attributePathTwo) if err != nil { resp.Error = err diff --git a/querycheck/expect_identity.go b/querycheck/expect_identity.go index 4b695bea8..e0e79b03b 100644 --- a/querycheck/expect_identity.go +++ b/querycheck/expect_identity.go @@ -6,12 +6,11 @@ package querycheck import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" "maps" "slices" "sort" - tfjson "github.com/hashicorp/terraform-json" - "github.com/hashicorp/terraform-plugin-testing/knownvalue" ) @@ -24,7 +23,7 @@ type expectIdentity struct { // CheckQuery implements the query check logic. func (e expectIdentity) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { - var resource *tfjson.QueryResource + var resource *plugintest.QueryResult if req.Query == nil { resp.Error = fmt.Errorf("query is nil") @@ -32,24 +31,8 @@ func (e expectIdentity) CheckQuery(ctx context.Context, req CheckQueryRequest, r return } - if req.Query.Values == nil { - resp.Error = fmt.Errorf("query does not contain any query values") - - return - } - - if req.Query.Values.RootModule == nil { - resp.Error = fmt.Errorf("query does not contain a root module") - - return - } - - for _, r := range req.Query.Values.RootModule.Resources { - if e.resourceAddress == r.Address { - resource = r - - break - } + if e.resourceAddress == req.Query.Address { + resource = req.Query } if resource == nil { @@ -58,21 +41,21 @@ func (e expectIdentity) CheckQuery(ctx context.Context, req CheckQueryRequest, r return } - if resource.IdentitySchemaVersion == nil || len(resource.IdentityValues) == 0 { + if resource.Identity == nil || len(resource.Identity) == 0 { resp.Error = fmt.Errorf("%s - Identity not found in query. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.12+)", e.resourceAddress) return } - if len(resource.IdentityValues) != len(e.identity) { + if len(resource.Identity) != len(e.identity) { deltaMsg := "" - if len(resource.IdentityValues) > len(e.identity) { - deltaMsg = createDeltaString(resource.IdentityValues, e.identity, "actual identity has extra attribute(s): ") + if len(resource.Identity) > len(e.identity) { + deltaMsg = createDeltaString(resource.Identity, e.identity, "actual identity has extra attribute(s): ") } else { - deltaMsg = createDeltaString(e.identity, resource.IdentityValues, "actual identity is missing attribute(s): ") + deltaMsg = createDeltaString(e.identity, resource.Identity, "actual identity is missing attribute(s): ") } - resp.Error = fmt.Errorf("%s - Expected %d attribute(s) in the actual identity object, got %d attribute(s): %s", e.resourceAddress, len(e.identity), len(resource.IdentityValues), deltaMsg) + resp.Error = fmt.Errorf("%s - Expected %d attribute(s) in the actual identity object, got %d attribute(s): %s", e.resourceAddress, len(e.identity), len(resource.Identity), deltaMsg) return } @@ -87,7 +70,7 @@ func (e expectIdentity) CheckQuery(ctx context.Context, req CheckQueryRequest, r }) for _, k := range keys { - actualIdentityVal, ok := resource.IdentityValues[k] + actualIdentityVal, ok := resource.Identity[k] if !ok { resp.Error = fmt.Errorf("%s - missing attribute %q in actual identity object", e.resourceAddress, k) diff --git a/querycheck/expect_identity_value.go b/querycheck/expect_identity_value.go index 468264f5b..0f1045cde 100644 --- a/querycheck/expect_identity_value.go +++ b/querycheck/expect_identity_value.go @@ -6,8 +6,7 @@ package querycheck import ( "context" "fmt" - - tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" "github.com/hashicorp/terraform-plugin-testing/knownvalue" "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" @@ -23,7 +22,7 @@ type expectIdentityValue struct { // CheckQuery implements the query check logic. func (e expectIdentityValue) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { - var resource *tfjson.QueryResource + var resource *plugintest.QueryResult if req.Query == nil { resp.Error = fmt.Errorf("query is nil") @@ -31,24 +30,14 @@ func (e expectIdentityValue) CheckQuery(ctx context.Context, req CheckQueryReque return } - if req.Query.Values == nil { - resp.Error = fmt.Errorf("query does not contain any query values") - - return - } - - if req.Query.Values.RootModule == nil { - resp.Error = fmt.Errorf("query does not contain a root module") + if len(req.Query.Address) == 0 { + resp.Error = fmt.Errorf("query does not contain any address values") return } - for _, r := range req.Query.Values.RootModule.Resources { - if e.resourceAddress == r.Address { - resource = r - - break - } + if e.resourceAddress == req.Query.Address { + resource = req.Query } if resource == nil { @@ -57,13 +46,13 @@ func (e expectIdentityValue) CheckQuery(ctx context.Context, req CheckQueryReque return } - if resource.IdentitySchemaVersion == nil || len(resource.IdentityValues) == 0 { + if resource.Identity == nil || len(resource.Identity) == 0 { resp.Error = fmt.Errorf("%s - Identity not found in query. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.12+)", e.resourceAddress) return } - result, err := tfjsonpath.Traverse(resource.IdentityValues, e.attributePath) + result, err := tfjsonpath.Traverse(resource.Identity, e.attributePath) if err != nil { resp.Error = err diff --git a/querycheck/expect_identity_value_matches_query.go b/querycheck/expect_identity_value_matches_query.go index ed4e3d40c..91c61d8dc 100644 --- a/querycheck/expect_identity_value_matches_query.go +++ b/querycheck/expect_identity_value_matches_query.go @@ -6,10 +6,9 @@ package querycheck import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" "reflect" - tfjson "github.com/hashicorp/terraform-json" - "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" ) @@ -22,7 +21,7 @@ type expectIdentityValueMatchesQuery struct { // CheckQuery implements the query check logic. func (e expectIdentityValueMatchesQuery) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { - var resource *tfjson.QueryResource + var resource *plugintest.QueryResult if req.Query == nil { resp.Error = fmt.Errorf("query is nil") @@ -30,24 +29,8 @@ func (e expectIdentityValueMatchesQuery) CheckQuery(ctx context.Context, req Che return } - if req.Query.Values == nil { - resp.Error = fmt.Errorf("query does not contain any query values") - - return - } - - if req.Query.Values.RootModule == nil { - resp.Error = fmt.Errorf("query does not contain a root module") - - return - } - - for _, r := range req.Query.Values.RootModule.Resources { - if e.resourceAddress == r.Address { - resource = r - - break - } + if e.resourceAddress == req.Query.Address { + resource = req.Query } if resource == nil { @@ -56,13 +39,13 @@ func (e expectIdentityValueMatchesQuery) CheckQuery(ctx context.Context, req Che return } - if resource.IdentitySchemaVersion == nil || len(resource.IdentityValues) == 0 { + if resource.Identity == nil || len(resource.Identity) == 0 { resp.Error = fmt.Errorf("%s - Identity not found in query. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.12+)", e.resourceAddress) return } - identityResult, err := tfjsonpath.Traverse(resource.IdentityValues, e.attributePath) + identityResult, err := tfjsonpath.Traverse(resource.Identity, e.attributePath) if err != nil { resp.Error = err @@ -70,7 +53,7 @@ func (e expectIdentityValueMatchesQuery) CheckQuery(ctx context.Context, req Che return } - queryResult, err := tfjsonpath.Traverse(resource.AttributeValues, e.attributePath) + queryResult, err := tfjsonpath.Traverse(resource.ResourceObject, e.attributePath) if err != nil { resp.Error = err diff --git a/querycheck/expect_identity_value_matches_query_at_path.go b/querycheck/expect_identity_value_matches_query_at_path.go index a638b5c7d..979a604c2 100644 --- a/querycheck/expect_identity_value_matches_query_at_path.go +++ b/querycheck/expect_identity_value_matches_query_at_path.go @@ -6,10 +6,9 @@ package querycheck import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" "reflect" - tfjson "github.com/hashicorp/terraform-json" - "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" ) @@ -23,7 +22,7 @@ type expectIdentityValueMatchesQueryAtPath struct { // CheckQuery implements the query check logic. func (e expectIdentityValueMatchesQueryAtPath) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { - var resource *tfjson.QueryResource + var resource *plugintest.QueryResult if req.Query == nil { resp.Error = fmt.Errorf("query is nil") @@ -31,24 +30,14 @@ func (e expectIdentityValueMatchesQueryAtPath) CheckQuery(ctx context.Context, r return } - if req.Query.Values == nil { - resp.Error = fmt.Errorf("query does not contain any query values") - - return - } - - if req.Query.Values.RootModule == nil { - resp.Error = fmt.Errorf("query does not contain a root module") + if len(req.Query.Address) == 0 { + resp.Error = fmt.Errorf("query does not contain any address values") return } - for _, r := range req.Query.Values.RootModule.Resources { - if e.resourceAddress == r.Address { - resource = r - - break - } + if e.resourceAddress == req.Query.Address { + resource = req.Query } if resource == nil { @@ -57,13 +46,13 @@ func (e expectIdentityValueMatchesQueryAtPath) CheckQuery(ctx context.Context, r return } - if resource.IdentitySchemaVersion == nil || len(resource.IdentityValues) == 0 { + if resource.Identity == nil || len(resource.Identity) == 0 { resp.Error = fmt.Errorf("%s - Identity not found in query. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.12+)", e.resourceAddress) return } - identityResult, err := tfjsonpath.Traverse(resource.IdentityValues, e.identityAttrPath) + identityResult, err := tfjsonpath.Traverse(resource.Identity, e.identityAttrPath) if err != nil { resp.Error = err @@ -71,7 +60,7 @@ func (e expectIdentityValueMatchesQueryAtPath) CheckQuery(ctx context.Context, r return } - queryResult, err := tfjsonpath.Traverse(resource.AttributeValues, e.queryAttrPath) + queryResult, err := tfjsonpath.Traverse(resource.ResourceObject, e.queryAttrPath) if err != nil { resp.Error = err diff --git a/querycheck/expect_known_output_value.go b/querycheck/expect_known_output_value.go deleted file mode 100644 index d63cafabd..000000000 --- a/querycheck/expect_known_output_value.go +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -import ( - "context" - "fmt" - - tfjson "github.com/hashicorp/terraform-json" - - "github.com/hashicorp/terraform-plugin-testing/knownvalue" - "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" -) - -// Resource Query Check -var _ QueryCheck = expectKnownOutputValue{} - -type expectKnownOutputValue struct { - outputAddress string - knownValue knownvalue.Check -} - -// CheckQuery implements the query check logic. -func (e expectKnownOutputValue) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { - var output *tfjson.QueryOutput - - if req.Query == nil { - resp.Error = fmt.Errorf("query is nil") - - return - } - - if req.Query.Values == nil { - resp.Error = fmt.Errorf("query does not contain any query values") - - return - } - - for address, oc := range req.Query.Values.Outputs { - if e.outputAddress == address { - output = oc - - break - } - } - - if output == nil { - resp.Error = fmt.Errorf("%s - Output not found in query", e.outputAddress) - - return - } - - result, err := tfjsonpath.Traverse(output.Value, tfjsonpath.Path{}) - - if err != nil { - resp.Error = err - - return - } - - if err := e.knownValue.CheckValue(result); err != nil { - resp.Error = fmt.Errorf("error checking value for output at path: %s, err: %s", e.outputAddress, err) - - return - } -} - -// ExpectKnownOutputValue returns a query check that asserts that the specified value -// has a known type, and value. -func ExpectKnownOutputValue(outputAddress string, knownValue knownvalue.Check) QueryCheck { - return expectKnownOutputValue{ - outputAddress: outputAddress, - knownValue: knownValue, - } -} diff --git a/querycheck/expect_known_output_value_at_path.go b/querycheck/expect_known_output_value_at_path.go deleted file mode 100644 index 5ad5c66e2..000000000 --- a/querycheck/expect_known_output_value_at_path.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -import ( - "context" - "fmt" - - tfjson "github.com/hashicorp/terraform-json" - - "github.com/hashicorp/terraform-plugin-testing/knownvalue" - "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" -) - -// Resource Query Check -var _ QueryCheck = expectKnownOutputValueAtPath{} - -type expectKnownOutputValueAtPath struct { - outputAddress string - outputPath tfjsonpath.Path - knownValue knownvalue.Check -} - -// CheckQuery implements the query check logic. -func (e expectKnownOutputValueAtPath) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { - var output *tfjson.QueryOutput - - if req.Query == nil { - resp.Error = fmt.Errorf("query is nil") - - return - } - - if req.Query.Values == nil { - resp.Error = fmt.Errorf("query does not contain any query values") - - return - } - - for address, oc := range req.Query.Values.Outputs { - if e.outputAddress == address { - output = oc - - break - } - } - - if output == nil { - resp.Error = fmt.Errorf("%s - Output not found in query", e.outputAddress) - - return - } - - result, err := tfjsonpath.Traverse(output.Value, e.outputPath) - - if err != nil { - resp.Error = err - - return - } - - if err := e.knownValue.CheckValue(result); err != nil { - resp.Error = fmt.Errorf("error checking value for output at path: %s.%s, err: %s", e.outputAddress, e.outputPath.String(), err) - - return - } -} - -// ExpectKnownOutputValueAtPath returns a query check that asserts that the specified output at the given path -// has a known type and value. -func ExpectKnownOutputValueAtPath(outputAddress string, outputPath tfjsonpath.Path, knownValue knownvalue.Check) QueryCheck { - return expectKnownOutputValueAtPath{ - outputAddress: outputAddress, - outputPath: outputPath, - knownValue: knownValue, - } -} diff --git a/querycheck/expect_known_output_value_at_path_example_test.go b/querycheck/expect_known_output_value_at_path_example_test.go deleted file mode 100644 index 423cb6ddf..000000000 --- a/querycheck/expect_known_output_value_at_path_example_test.go +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck_test - -import ( - "testing" - - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/knownvalue" - "github.com/hashicorp/terraform-plugin-testing/querycheck" - "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" -) - -func ExampleExpectKnownOutputValueAtPath() { - // A typical test would accept *testing.T as a function parameter, for instance `func TestSomething(t *testing.T) { ... }`. - t := &testing.T{} - t.Parallel() - - resource.Test(t, resource.TestCase{ - // Provider definition omitted. - Steps: []resource.TestStep{ - { - Config: `resource "test_resource" "one" { - bool_attribute = true - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("bool_attribute"), - knownvalue.Bool(true), - ), - }, - }, - }, - }) -} diff --git a/querycheck/expect_known_output_value_at_path_test.go b/querycheck/expect_known_output_value_at_path_test.go deleted file mode 100644 index caf174d44..000000000 --- a/querycheck/expect_known_output_value_at_path_test.go +++ /dev/null @@ -1,1628 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck_test - -import ( - "context" - "fmt" - "math/big" - "regexp" - "testing" - - "github.com/google/go-cmp/cmp" - tfjson "github.com/hashicorp/terraform-json" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - - r "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/knownvalue" - "github.com/hashicorp/terraform-plugin-testing/querycheck" - "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" -) - -func TestExpectKnownOutputValueAtPath_CheckQuery_ResourceNotFound(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - bool_attribute = true - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_two_output", - tfjsonpath.New("bool_attribute"), - knownvalue.Bool(true), - ), - }, - ExpectError: regexp.MustCompile("test_resource_two_output - Output not found in query"), - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_AttributeValueNull(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" {} - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("bool_attribute"), - knownvalue.Null(), - ), - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("float_attribute"), - knownvalue.Null(), - ), - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("int_attribute"), - knownvalue.Null(), - ), - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("list_attribute"), - knownvalue.Null(), - ), - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("list_nested_block"), - knownvalue.ListExact([]knownvalue.Check{}), - ), - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("map_attribute"), - knownvalue.Null(), - ), - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("set_attribute"), - knownvalue.Null(), - ), - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("set_nested_block"), - knownvalue.SetExact([]knownvalue.Check{}), - ), - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("string_attribute"), - knownvalue.Null(), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_AttributeValueNotNull(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - bool_attribute = true - float_attribute = 1.23 - int_attribute = 123 - list_attribute = ["value1", "value2"] - list_nested_block { - list_nested_block_attribute = "str" - } - map_attribute = { - key1 = "value1" - } - set_attribute = ["value1", "value2"] - set_nested_block { - set_nested_block_attribute = "str" - } - string_attribute = "str" - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("bool_attribute"), - knownvalue.NotNull(), - ), - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("float_attribute"), - knownvalue.NotNull(), - ), - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("int_attribute"), - knownvalue.NotNull(), - ), - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("list_attribute"), - knownvalue.NotNull(), - ), - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("list_nested_block"), - knownvalue.ListSizeExact(1), - ), - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("map_attribute"), - knownvalue.NotNull(), - ), - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("set_attribute"), - knownvalue.NotNull(), - ), - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("set_nested_block"), - knownvalue.SetSizeExact(1), - ), - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("string_attribute"), - knownvalue.NotNull(), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_Bool(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - bool_attribute = true - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("bool_attribute"), - knownvalue.Bool(true), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_Bool_KnownValueWrongType(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - bool_attribute = true - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("bool_attribute"), - knownvalue.Float64Exact(1.23), - ), - }, - ExpectError: regexp.MustCompile(`expected json\.Number value for Float64Exact check, got: bool`), - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_Bool_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - bool_attribute = true - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("bool_attribute"), - knownvalue.Bool(false), - ), - }, - ExpectError: regexp.MustCompile("expected value false for Bool check, got: true"), - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_Float64(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - float_attribute = 1.23 - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("float_attribute"), - knownvalue.Float64Exact(1.23), - ), - }, - }, - }, - }) -} - -// We do not need equivalent tests for Int64 and Number as they all test the same logic. -func TestExpectKnownOutputValueAtPath_CheckQuery_Float64_KnownValueWrongType(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - float_attribute = 1.23 - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("float_attribute"), - knownvalue.StringExact("str"), - ), - }, - ExpectError: regexp.MustCompile(`expected string value for StringExact check, got: json\.Number`), - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_Float64_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - float_attribute = 1.23 - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("float_attribute"), - knownvalue.Float64Exact(3.21), - ), - }, - ExpectError: regexp.MustCompile("expected value 3.21 for Float64Exact check, got: 1.23"), - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_Int64(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - int_attribute = 123 - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("int_attribute"), - knownvalue.Int64Exact(123), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_Int64_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - int_attribute = 123 - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("int_attribute"), - knownvalue.Int64Exact(321), - ), - }, - ExpectError: regexp.MustCompile("expected value 321 for Int64Exact check, got: 123"), - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_List(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - list_attribute = [ - "value1", - "value2" - ] - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("list_attribute"), - knownvalue.ListExact([]knownvalue.Check{ - knownvalue.StringExact("value1"), - knownvalue.StringExact("value2"), - }), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_List_KnownValueWrongType(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - list_attribute = [ - "value1", - "value2" - ] - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("list_attribute"), - knownvalue.MapExact(map[string]knownvalue.Check{}), - ), - }, - ExpectError: regexp.MustCompile(`expected map\[string\]any value for MapExact check, got: \[\]interface {}`), - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_List_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - list_attribute = [ - "value1", - "value2" - ] - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("list_attribute"), - knownvalue.ListExact([]knownvalue.Check{ - knownvalue.StringExact("value3"), - knownvalue.StringExact("value4"), - }), - ), - }, - ExpectError: regexp.MustCompile(`list element index 0: expected value value3 for StringExact check, got: value1`), - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_ListPartial(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - list_attribute = [ - "value1", - "value2" - ] - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("list_attribute"), - knownvalue.ListPartial(map[int]knownvalue.Check{ - 0: knownvalue.StringExact("value1"), - }), - ), - }, - }, - }, - }) -} - -// No need to check KnownValueWrongType for ListPartial as all lists, and sets are []any in -// tfjson.Query. -func TestExpectKnownOutputValueAtPath_CheckQuery_ListPartial_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - list_attribute = [ - "value1", - "value2" - ] - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("list_attribute"), - knownvalue.ListPartial(map[int]knownvalue.Check{ - 0: knownvalue.StringExact("value3"), - }), - ), - }, - ExpectError: regexp.MustCompile(`list element 0: expected value value3 for StringExact check, got: value1`), - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_ListElements(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - list_attribute = [ - "value1", - "value2" - ] - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("list_attribute"), - knownvalue.ListSizeExact(2), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_ListElements_WrongNum(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - list_attribute = [ - "value1", - "value2" - ] - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("list_attribute"), - knownvalue.ListSizeExact(3), - ), - }, - ExpectError: regexp.MustCompile("expected 3 elements for ListSizeExact check, got 2 elements"), - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_ListNestedBlock(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - list_nested_block { - list_nested_block_attribute = "str" - } - list_nested_block { - list_nested_block_attribute = "rts" - } - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("list_nested_block"), - knownvalue.ListExact([]knownvalue.Check{ - knownvalue.MapExact(map[string]knownvalue.Check{ - "list_nested_block_attribute": knownvalue.StringExact("str"), - }), - knownvalue.MapExact(map[string]knownvalue.Check{ - "list_nested_block_attribute": knownvalue.StringExact("rts"), - }), - }), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_ListNestedBlockPartial(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - list_nested_block { - list_nested_block_attribute = "str" - } - list_nested_block { - list_nested_block_attribute = "rts" - } - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("list_nested_block"), - knownvalue.ListPartial(map[int]knownvalue.Check{ - 1: knownvalue.MapExact(map[string]knownvalue.Check{ - "list_nested_block_attribute": knownvalue.StringExact("rts"), - }), - }), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_ListNestedBlockElements(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - list_nested_block { - list_nested_block_attribute = "str" - } - list_nested_block { - list_nested_block_attribute = "rts" - } - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("list_nested_block"), - knownvalue.ListSizeExact(2), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_Map(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - map_attribute = { - key1 = "value1" - key2 = "value2" - } - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("map_attribute"), - knownvalue.MapExact(map[string]knownvalue.Check{ - "key1": knownvalue.StringExact("value1"), - "key2": knownvalue.StringExact("value2"), - }), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_Map_KnownValueWrongType(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - map_attribute = { - key1 = "value1" - key2 = "value2" - } - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("map_attribute"), - knownvalue.ListExact([]knownvalue.Check{}), - ), - }, - ExpectError: regexp.MustCompile(`expected \[\]any value for ListExact check, got: map\[string\]interface {}`), - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_Map_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - map_attribute = { - key1 = "value1" - key2 = "value2" - } - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("map_attribute"), - knownvalue.MapExact(map[string]knownvalue.Check{ - "key3": knownvalue.StringExact("value3"), - "key4": knownvalue.StringExact("value4"), - }), - ), - }, - ExpectError: regexp.MustCompile(`missing element key3 for MapExact check`), - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_MapPartial(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - map_attribute = { - key1 = "value1" - key2 = "value2" - } - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("map_attribute"), - knownvalue.MapPartial(map[string]knownvalue.Check{ - "key1": knownvalue.StringExact("value1"), - }), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_MapPartial_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - map_attribute = { - key1 = "value1" - key2 = "value2" - } - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("map_attribute"), - knownvalue.MapPartial(map[string]knownvalue.Check{ - "key3": knownvalue.StringExact("value1"), - }), - ), - }, - ExpectError: regexp.MustCompile(`missing element key3 for MapPartial check`), - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_MapElements(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - map_attribute = { - key1 = "value1" - key2 = "value2" - } - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("map_attribute"), - knownvalue.MapSizeExact(2), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_MapElements_WrongNum(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - map_attribute = { - key1 = "value1" - key2 = "value2" - } - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("map_attribute"), - knownvalue.MapSizeExact(3), - ), - }, - ExpectError: regexp.MustCompile("expected 3 elements for MapSizeExact check, got 2 elements"), - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_Number(t *testing.T) { - t.Parallel() - - f, _, err := big.ParseFloat("123", 10, 512, big.ToNearestEven) - - if err != nil { - t.Errorf("%s", err) - } - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - int_attribute = 123 - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("int_attribute"), - knownvalue.NumberExact(f), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_Number_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - f, _, err := big.ParseFloat("321", 10, 512, big.ToNearestEven) - - if err != nil { - t.Errorf("%s", err) - } - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - int_attribute = 123 - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("int_attribute"), - knownvalue.NumberExact(f), - ), - }, - ExpectError: regexp.MustCompile("expected value 321 for NumberExact check, got: 123"), - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_Set(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_attribute = [ - "value1", - "value2" - ] - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("set_attribute"), - knownvalue.SetExact([]knownvalue.Check{ - knownvalue.StringExact("value1"), - knownvalue.StringExact("value2"), - }), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_Set_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_attribute = [ - "value1", - "value2" - ] - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("set_attribute"), - knownvalue.SetExact([]knownvalue.Check{ - knownvalue.StringExact("value1"), - knownvalue.StringExact("value3"), - }), - ), - }, - ExpectError: regexp.MustCompile(`missing value value3 for SetExact check`), - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_SetPartial(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_attribute = [ - "value1", - "value2" - ] - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("set_attribute"), - knownvalue.SetPartial([]knownvalue.Check{ - knownvalue.StringExact("value2"), - }), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_SetPartial_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_attribute = [ - "value1", - "value2" - ] - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("set_attribute"), - knownvalue.SetPartial([]knownvalue.Check{ - knownvalue.StringExact("value3"), - }), - ), - }, - ExpectError: regexp.MustCompile(`missing value value3 for SetPartial check`), - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_SetElements(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_attribute = [ - "value1", - "value2" - ] - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("set_attribute"), - knownvalue.SetSizeExact(2), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_SetNestedBlock(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_nested_block { - set_nested_block_attribute = "str" - } - set_nested_block { - set_nested_block_attribute = "rts" - } - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("set_nested_block"), - knownvalue.SetExact([]knownvalue.Check{ - knownvalue.MapExact(map[string]knownvalue.Check{ - "set_nested_block_attribute": knownvalue.StringExact("str"), - }), - knownvalue.MapExact(map[string]knownvalue.Check{ - "set_nested_block_attribute": knownvalue.StringExact("rts"), - }), - }), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_SetNestedBlockPartial(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_nested_block { - set_nested_block_attribute = "str" - } - set_nested_block { - set_nested_block_attribute = "rts" - } - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("set_nested_block"), - knownvalue.SetPartial([]knownvalue.Check{ - knownvalue.MapExact(map[string]knownvalue.Check{ - "set_nested_block_attribute": knownvalue.StringExact("rts"), - }), - }), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_SetNestedBlockElements(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_nested_block { - set_nested_block_attribute = "str" - } - set_nested_block { - set_nested_block_attribute = "rts" - } - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("set_nested_block"), - knownvalue.SetSizeExact(2), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_String(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - string_attribute = "str" - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("string_attribute"), - knownvalue.StringExact("str")), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_String_Custom(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - string_attribute = "string" - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("string_attribute"), - StringContains("str")), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_String_KnownValueWrongType(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - string_attribute = "str" - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("string_attribute"), - knownvalue.Bool(true)), - }, - ExpectError: regexp.MustCompile("expected bool value for Bool check, got: string"), - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_String_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - string_attribute = "str" - } - - output test_resource_one_output { - value = test_resource.one - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValueAtPath( - "test_resource_one_output", - tfjsonpath.New("string_attribute"), - knownvalue.StringExact("rts")), - }, - ExpectError: regexp.MustCompile("expected value rts for StringExact check, got: str"), - }, - }, - }) -} - -func TestExpectKnownOutputValueAtPath_CheckQuery_UnknownAttributeType(t *testing.T) { - t.Parallel() - - testCases := map[string]struct { - knownValue knownvalue.Check - req querycheck.CheckQueryRequest - expectedErr error - }{ - "unrecognised-type": { - knownValue: knownvalue.Int64Exact(123), - req: querycheck.CheckQueryRequest{ - Query: &tfjson.Query{ - Values: &tfjson.QueryValues{ - Outputs: map[string]*tfjson.QueryOutput{ - "obj": { - Value: map[string]any{ - "float32_output": float32(123), - }, - }, - }, - }, - }, - }, - expectedErr: fmt.Errorf("error checking value for output at path: obj.float32_output, err: expected json.Number value for Int64Exact check, got: float32"), - }, - } - - for name, testCase := range testCases { - t.Run(name, func(t *testing.T) { - t.Parallel() - - e := querycheck.ExpectKnownOutputValueAtPath("obj", tfjsonpath.New("float32_output"), testCase.knownValue) - - resp := querycheck.CheckQueryResponse{} - - e.CheckQuery(context.Background(), testCase.req, &resp) - - if diff := cmp.Diff(resp.Error, testCase.expectedErr, equateErrorMessage); diff != "" { - t.Errorf("unexpected difference: %s", diff) - } - }) - } -} diff --git a/querycheck/expect_known_output_value_example_test.go b/querycheck/expect_known_output_value_example_test.go deleted file mode 100644 index b3107a16d..000000000 --- a/querycheck/expect_known_output_value_example_test.go +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck_test - -import ( - "testing" - - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/knownvalue" - "github.com/hashicorp/terraform-plugin-testing/querycheck" -) - -func ExampleExpectKnownOutputValue() { - // A typical test would accept *testing.T as a function parameter, for instance `func TestSomething(t *testing.T) { ... }`. - t := &testing.T{} - t.Parallel() - - resource.Test(t, resource.TestCase{ - // Provider definition omitted. - Steps: []resource.TestStep{ - { - Config: `resource "test_resource" "one" { - bool_attribute = true - } - - output bool_output { - value = test_resource.one.bool_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "bool_output", - knownvalue.Bool(true), - ), - }, - }, - }, - }) -} diff --git a/querycheck/expect_known_output_value_test.go b/querycheck/expect_known_output_value_test.go deleted file mode 100644 index d8c2bcb3a..000000000 --- a/querycheck/expect_known_output_value_test.go +++ /dev/null @@ -1,1562 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck_test - -import ( - "context" - "fmt" - "math/big" - "regexp" - "testing" - - "github.com/google/go-cmp/cmp" - tfjson "github.com/hashicorp/terraform-json" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - - r "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/knownvalue" - "github.com/hashicorp/terraform-plugin-testing/querycheck" -) - -func TestExpectKnownOutputValue_CheckQuery_OutputNotFound(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - bool_attribute = true - } - - output bool_output { - value = test_resource.one.bool_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "bool_not_found", - knownvalue.Bool(true), - ), - }, - ExpectError: regexp.MustCompile("bool_not_found - Output not found in query"), - }, - }, - }) -} - -// TestExpectKnownOutputValue_CheckQuery_AttributeValueNull shows that outputs that reference -// null values do not appear in query. Indicating that there is no way to discriminate -// between null outputs and non-existent outputs. -// Reference: https://github.com/hashicorp/terraform/issues/34080 -func TestExpectKnownOutputValue_CheckQuery_AttributeValueNull(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" {} - output bool_output { - value = test_resource.one.bool_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "bool_output", - knownvalue.Bool(true), - ), - }, - ExpectError: regexp.MustCompile("bool_output - Output not found in query"), - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_AttributeValueNotNull(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - bool_attribute = true - float_attribute = 1.23 - int_attribute = 123 - list_attribute = ["value1", "value2"] - list_nested_block { - list_nested_block_attribute = "str" - } - map_attribute = { - key1 = "value1" - } - set_attribute = ["value1", "value2"] - set_nested_block { - set_nested_block_attribute = "str" - } - string_attribute = "str" - } - output bool_output { - value = test_resource.one.bool_attribute - } - output float64_output { - value = test_resource.one.float_attribute - } - output int64_output { - value = test_resource.one.int_attribute - } - output list_output { - value = test_resource.one.list_attribute - } - output list_nested_block_output { - value = test_resource.one.list_nested_block - } - output map_output { - value = test_resource.one.map_attribute - } - output set_output { - value = test_resource.one.set_attribute - } - output set_nested_block_output { - value = test_resource.one.set_nested_block - } - output string_output { - value = test_resource.one.string_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "bool_output", - knownvalue.NotNull(), - ), - querycheck.ExpectKnownOutputValue( - "float64_output", - knownvalue.NotNull(), - ), - querycheck.ExpectKnownOutputValue( - "int64_output", - knownvalue.NotNull(), - ), - querycheck.ExpectKnownOutputValue( - "list_output", - knownvalue.NotNull(), - ), - querycheck.ExpectKnownOutputValue( - "list_nested_block_output", - knownvalue.ListSizeExact(1), - ), - querycheck.ExpectKnownOutputValue( - "map_output", - knownvalue.NotNull(), - ), - querycheck.ExpectKnownOutputValue( - "set_output", - knownvalue.NotNull(), - ), - querycheck.ExpectKnownOutputValue( - "set_nested_block_output", - knownvalue.SetSizeExact(1), - ), - querycheck.ExpectKnownOutputValue( - "string_output", - knownvalue.NotNull(), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_Bool(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - bool_attribute = true - } - - output bool_output { - value = test_resource.one.bool_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "bool_output", - knownvalue.Bool(true), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_Bool_KnownValueWrongType(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - bool_attribute = true - } - - output bool_output { - value = test_resource.one.bool_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "bool_output", - knownvalue.Float64Exact(1.23), - ), - }, - ExpectError: regexp.MustCompile(`expected json\.Number value for Float64Exact check, got: bool`), - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_Bool_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - bool_attribute = true - } - - output bool_output { - value = test_resource.one.bool_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "bool_output", - knownvalue.Bool(false), - ), - }, - ExpectError: regexp.MustCompile("expected value false for Bool check, got: true"), - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_Float64(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - float_attribute = 1.23 - } - - output float64_output { - value = test_resource.one.float_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "float64_output", - knownvalue.Float64Exact(1.23), - ), - }, - }, - }, - }) -} - -// We do not need equivalent tests for Int64 and Number as they all test the same logic. -func TestExpectKnownOutputValue_CheckQuery_Float64_KnownValueWrongType(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - float_attribute = 1.23 - } - - output float64_output { - value = test_resource.one.float_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "float64_output", - knownvalue.StringExact("str"), - ), - }, - ExpectError: regexp.MustCompile(`expected string value for StringExact check, got: json\.Number`), - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_Float64_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - float_attribute = 1.23 - } - - output float64_output { - value = test_resource.one.float_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "float64_output", - knownvalue.Float64Exact(3.21), - ), - }, - ExpectError: regexp.MustCompile("expected value 3.21 for Float64Exact check, got: 1.23"), - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_Int64(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - int_attribute = 123 - } - - output int64_output { - value = test_resource.one.int_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "int64_output", - knownvalue.Int64Exact(123), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_Int64_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - int_attribute = 123 - } - - output int64_output { - value = test_resource.one.int_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "int64_output", - knownvalue.Int64Exact(321), - ), - }, - ExpectError: regexp.MustCompile("expected value 321 for Int64Exact check, got: 123"), - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_List(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - list_attribute = [ - "value1", - "value2" - ] - } - - output list_output { - value = test_resource.one.list_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "list_output", - knownvalue.ListExact([]knownvalue.Check{ - knownvalue.StringExact("value1"), - knownvalue.StringExact("value2"), - }), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_List_KnownValueWrongType(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - list_attribute = [ - "value1", - "value2" - ] - } - - output list_output { - value = test_resource.one.list_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "list_output", - knownvalue.MapExact(map[string]knownvalue.Check{}), - ), - }, - ExpectError: regexp.MustCompile(`expected map\[string\]any value for MapExact check, got: \[\]interface {}`), - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_List_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - list_attribute = [ - "value1", - "value2" - ] - } - - output list_output { - value = test_resource.one.list_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "list_output", - knownvalue.ListExact([]knownvalue.Check{ - knownvalue.StringExact("value3"), - knownvalue.StringExact("value4"), - }), - ), - }, - ExpectError: regexp.MustCompile(`list element index 0: expected value value3 for StringExact check, got: value1`), - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_ListPartial(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - list_attribute = [ - "value1", - "value2" - ] - } - - output list_output { - value = test_resource.one.list_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "list_output", - knownvalue.ListPartial(map[int]knownvalue.Check{ - 0: knownvalue.StringExact("value1"), - }), - ), - }, - }, - }, - }) -} - -// No need to check KnownValueWrongType for ListPartial as all lists, and sets are []any in -// tfjson.Query. -func TestExpectKnownOutputValue_CheckQuery_ListPartial_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - list_attribute = [ - "value1", - "value2" - ] - } - - output list_output { - value = test_resource.one.list_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "list_output", - knownvalue.ListPartial(map[int]knownvalue.Check{ - 0: knownvalue.StringExact("value3"), - }), - ), - }, - ExpectError: regexp.MustCompile(`list element 0: expected value value3 for StringExact check, got: value1`), - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_ListElements(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - list_attribute = [ - "value1", - "value2" - ] - } - - output list_output { - value = test_resource.one.list_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "list_output", - knownvalue.ListSizeExact(2), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_ListElements_WrongNum(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - list_attribute = [ - "value1", - "value2" - ] - } - - output list_output { - value = test_resource.one.list_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "list_output", - knownvalue.ListSizeExact(3), - ), - }, - ExpectError: regexp.MustCompile("expected 3 elements for ListSizeExact check, got 2 elements"), - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_ListNestedBlock(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - list_nested_block { - list_nested_block_attribute = "str" - } - list_nested_block { - list_nested_block_attribute = "rts" - } - } - - output list_nested_block_output { - value = test_resource.one.list_nested_block - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "list_nested_block_output", - knownvalue.ListExact([]knownvalue.Check{ - knownvalue.MapExact(map[string]knownvalue.Check{ - "list_nested_block_attribute": knownvalue.StringExact("str"), - }), - knownvalue.MapExact(map[string]knownvalue.Check{ - "list_nested_block_attribute": knownvalue.StringExact("rts"), - }), - }), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_ListNestedBlockPartial(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - list_nested_block { - list_nested_block_attribute = "str" - } - list_nested_block { - list_nested_block_attribute = "rts" - } - } - - output list_nested_block_output { - value = test_resource.one.list_nested_block - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "list_nested_block_output", - knownvalue.ListPartial(map[int]knownvalue.Check{ - 1: knownvalue.MapExact(map[string]knownvalue.Check{ - "list_nested_block_attribute": knownvalue.StringExact("rts"), - }), - }), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_ListNestedBlockElements(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - list_nested_block { - list_nested_block_attribute = "str" - } - list_nested_block { - list_nested_block_attribute = "rts" - } - } - - output list_nested_block_output { - value = test_resource.one.list_nested_block - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "list_nested_block_output", - knownvalue.ListSizeExact(2), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_Map(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - map_attribute = { - key1 = "value1" - key2 = "value2" - } - } - - output map_output { - value = test_resource.one.map_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "map_output", - knownvalue.MapExact(map[string]knownvalue.Check{ - "key1": knownvalue.StringExact("value1"), - "key2": knownvalue.StringExact("value2"), - }), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_Map_KnownValueWrongType(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - map_attribute = { - key1 = "value1" - key2 = "value2" - } - } - - output map_output { - value = test_resource.one.map_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "map_output", - knownvalue.ListExact([]knownvalue.Check{}), - ), - }, - ExpectError: regexp.MustCompile(`expected \[\]any value for ListExact check, got: map\[string\]interface {}`), - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_Map_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - map_attribute = { - key1 = "value1" - key2 = "value2" - } - } - - output map_output { - value = test_resource.one.map_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "map_output", - knownvalue.MapExact(map[string]knownvalue.Check{ - "key3": knownvalue.StringExact("value3"), - "key4": knownvalue.StringExact("value4"), - }), - ), - }, - ExpectError: regexp.MustCompile(`missing element key3 for MapExact check`), - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_MapPartial(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - map_attribute = { - key1 = "value1" - key2 = "value2" - } - } - - output map_output { - value = test_resource.one.map_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "map_output", - knownvalue.MapPartial(map[string]knownvalue.Check{ - "key1": knownvalue.StringExact("value1"), - }), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_MapPartial_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - map_attribute = { - key1 = "value1" - key2 = "value2" - } - } - - output map_output { - value = test_resource.one.map_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "map_output", - knownvalue.MapPartial(map[string]knownvalue.Check{ - "key3": knownvalue.StringExact("value1"), - }), - ), - }, - ExpectError: regexp.MustCompile(`missing element key3 for MapPartial check`), - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_MapElements(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - map_attribute = { - key1 = "value1" - key2 = "value2" - } - } - - output map_output { - value = test_resource.one.map_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "map_output", - knownvalue.MapSizeExact(2), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_MapElements_WrongNum(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - map_attribute = { - key1 = "value1" - key2 = "value2" - } - } - - output map_output { - value = test_resource.one.map_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "map_output", - knownvalue.MapSizeExact(3), - ), - }, - ExpectError: regexp.MustCompile("expected 3 elements for MapSizeExact check, got 2 elements"), - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_Number(t *testing.T) { - t.Parallel() - - f, _, err := big.ParseFloat("123", 10, 512, big.ToNearestEven) - - if err != nil { - t.Errorf("%s", err) - } - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - int_attribute = 123 - } - - output int64_output { - value = test_resource.one.int_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "int64_output", - knownvalue.NumberExact(f), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_Number_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - f, _, err := big.ParseFloat("321", 10, 512, big.ToNearestEven) - - if err != nil { - t.Errorf("%s", err) - } - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - int_attribute = 123 - } - - output int64_output { - value = test_resource.one.int_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "int64_output", - knownvalue.NumberExact(f), - ), - }, - ExpectError: regexp.MustCompile("expected value 321 for NumberExact check, got: 123"), - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_Set(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_attribute = [ - "value1", - "value2" - ] - } - - output set_output { - value = test_resource.one.set_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "set_output", - knownvalue.SetExact([]knownvalue.Check{ - knownvalue.StringExact("value1"), - knownvalue.StringExact("value2"), - }), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_Set_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_attribute = [ - "value1", - "value2" - ] - } - - output set_output { - value = test_resource.one.set_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "set_output", - knownvalue.SetExact([]knownvalue.Check{ - knownvalue.StringExact("value1"), - knownvalue.StringExact("value3"), - }), - ), - }, - ExpectError: regexp.MustCompile(`missing value value3 for SetExact check`), - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_SetPartial(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_attribute = [ - "value1", - "value2" - ] - } - - output set_output { - value = test_resource.one.set_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "set_output", - knownvalue.SetPartial([]knownvalue.Check{ - knownvalue.StringExact("value2"), - }), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_SetPartial_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_attribute = [ - "value1", - "value2" - ] - } - - output set_output { - value = test_resource.one.set_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "set_output", - knownvalue.SetPartial([]knownvalue.Check{ - knownvalue.StringExact("value3"), - }), - ), - }, - ExpectError: regexp.MustCompile(`missing value value3 for SetPartial check`), - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_SetElements(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_attribute = [ - "value1", - "value2" - ] - } - - output set_output { - value = test_resource.one.set_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "set_output", - knownvalue.SetSizeExact(2), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_SetNestedBlock(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_nested_block { - set_nested_block_attribute = "str" - } - set_nested_block { - set_nested_block_attribute = "rts" - } - } - - output set_nested_block_output { - value = test_resource.one.set_nested_block - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "set_nested_block_output", - knownvalue.SetExact([]knownvalue.Check{ - knownvalue.MapExact(map[string]knownvalue.Check{ - "set_nested_block_attribute": knownvalue.StringExact("str"), - }), - knownvalue.MapExact(map[string]knownvalue.Check{ - "set_nested_block_attribute": knownvalue.StringExact("rts"), - }), - }), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_SetNestedBlockPartial(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_nested_block { - set_nested_block_attribute = "str" - } - set_nested_block { - set_nested_block_attribute = "rts" - } - } - - output set_nested_block_output { - value = test_resource.one.set_nested_block - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "set_nested_block_output", - knownvalue.SetPartial([]knownvalue.Check{ - knownvalue.MapExact(map[string]knownvalue.Check{ - "set_nested_block_attribute": knownvalue.StringExact("rts"), - }), - }), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_SetNestedBlockElements(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_nested_block { - set_nested_block_attribute = "str" - } - set_nested_block { - set_nested_block_attribute = "rts" - } - } - - output set_nested_block_output { - value = test_resource.one.set_nested_block - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "set_nested_block_output", - knownvalue.SetSizeExact(2), - ), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_String(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - string_attribute = "str" - } - - output string_output { - value = test_resource.one.string_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "string_output", - knownvalue.StringExact("str")), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_String_Custom(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - string_attribute = "string" - } - - output string_output { - value = test_resource.one.string_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "string_output", - StringContains("str")), - }, - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_String_KnownValueWrongType(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - string_attribute = "str" - } - - output string_output { - value = test_resource.one.string_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "string_output", - knownvalue.Bool(true)), - }, - ExpectError: regexp.MustCompile("expected bool value for Bool check, got: string"), - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_String_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - string_attribute = "str" - } - - output string_output { - value = test_resource.one.string_attribute - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownOutputValue( - "string_output", - knownvalue.StringExact("rts")), - }, - ExpectError: regexp.MustCompile("expected value rts for StringExact check, got: str"), - }, - }, - }) -} - -func TestExpectKnownOutputValue_CheckQuery_UnknownAttributeType(t *testing.T) { - t.Parallel() - - testCases := map[string]struct { - knownValue knownvalue.Check - req querycheck.CheckQueryRequest - expectedErr error - }{ - "unrecognised-type": { - knownValue: knownvalue.Int64Exact(123), - req: querycheck.CheckQueryRequest{ - Query: &tfjson.Query{ - Values: &tfjson.QueryValues{ - Outputs: map[string]*tfjson.QueryOutput{ - "float32_output": { - Value: float32(123), - }, - }, - }, - }, - }, - expectedErr: fmt.Errorf("error checking value for output at path: float32_output, err: expected json.Number value for Int64Exact check, got: float32"), - }, - } - - for name, testCase := range testCases { - t.Run(name, func(t *testing.T) { - t.Parallel() - - e := querycheck.ExpectKnownOutputValue("float32_output", testCase.knownValue) - - resp := querycheck.CheckQueryResponse{} - - e.CheckQuery(context.Background(), testCase.req, &resp) - - if diff := cmp.Diff(resp.Error, testCase.expectedErr, equateErrorMessage); diff != "" { - t.Errorf("unexpected difference: %s", diff) - } - }) - } -} diff --git a/querycheck/expect_known_value.go b/querycheck/expect_known_value.go index 4ea8ba91c..647ef4e5b 100644 --- a/querycheck/expect_known_value.go +++ b/querycheck/expect_known_value.go @@ -6,8 +6,7 @@ package querycheck import ( "context" "fmt" - - tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" "github.com/hashicorp/terraform-plugin-testing/knownvalue" "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" @@ -24,7 +23,7 @@ type expectKnownValue struct { // CheckQuery implements the query check logic. func (e expectKnownValue) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { - var resource *tfjson.QueryResource + var resource *plugintest.QueryResult if req.Query == nil { resp.Error = fmt.Errorf("query is nil") @@ -32,24 +31,14 @@ func (e expectKnownValue) CheckQuery(ctx context.Context, req CheckQueryRequest, return } - if req.Query.Values == nil { - resp.Error = fmt.Errorf("query does not contain any query values") - - return - } - - if req.Query.Values.RootModule == nil { - resp.Error = fmt.Errorf("query does not contain a root module") + if len(req.Query.Address) == 0 { + resp.Error = fmt.Errorf("query does not contain any address values") return } - for _, r := range req.Query.Values.RootModule.Resources { - if e.resourceAddress == r.Address { - resource = r - - break - } + if e.resourceAddress == req.Query.Address { + resource = req.Query } if resource == nil { @@ -58,7 +47,7 @@ func (e expectKnownValue) CheckQuery(ctx context.Context, req CheckQueryRequest, return } - result, err := tfjsonpath.Traverse(resource.AttributeValues, e.attributePath) + result, err := tfjsonpath.Traverse(resource.ResourceObject, e.attributePath) if err != nil { resp.Error = err diff --git a/querycheck/expect_known_value_test.go b/querycheck/expect_known_value_test.go index e31648f9b..8e5fc63b6 100644 --- a/querycheck/expect_known_value_test.go +++ b/querycheck/expect_known_value_test.go @@ -6,13 +6,13 @@ package querycheck_test import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" "math/big" "regexp" "strings" "testing" "github.com/google/go-cmp/cmp" - tfjson "github.com/hashicorp/terraform-json" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -1495,22 +1495,11 @@ func TestExpectKnownValue_CheckQuery_UnknownAttributeType(t *testing.T) { "unrecognised-type": { knownValue: knownvalue.Int64Exact(123), req: querycheck.CheckQueryRequest{ - Query: &tfjson.Query{ - Values: &tfjson.QueryValues{ - RootModule: &tfjson.QueryModule{ - Resources: []*tfjson.QueryResource{ - { - Address: "example_resource.test", - AttributeValues: map[string]any{ - "attribute": float32(123), - }, - }, - }, - }, - }, + Query: &plugintest.QueryResult{ + ResourceType: "attribute", }, }, - expectedErr: fmt.Errorf("error checking value for attribute at path: example_resource.test.attribute, err: expected json.Number value for Int64Exact check, got: float32"), + expectedErr: fmt.Errorf("error"), }, } diff --git a/querycheck/expect_sensitive_value.go b/querycheck/expect_sensitive_value.go deleted file mode 100644 index b0837c5d8..000000000 --- a/querycheck/expect_sensitive_value.go +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -import ( - "context" - "encoding/json" - "fmt" - - tfjson "github.com/hashicorp/terraform-json" - - "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" -) - -var _ QueryCheck = expectSensitiveValue{} - -type expectSensitiveValue struct { - resourceAddress string - attributePath tfjsonpath.Path -} - -// CheckQuery implements the query check logic. -func (e expectSensitiveValue) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { - var resource *tfjson.QueryResource - - if req.Query == nil { - resp.Error = fmt.Errorf("query is nil") - - return - } - - if req.Query.Values == nil { - resp.Error = fmt.Errorf("query does not contain any query values") - - return - } - - if req.Query.Values.RootModule == nil { - resp.Error = fmt.Errorf("query does not contain a root module") - - return - } - - for _, r := range req.Query.Values.RootModule.Resources { - if e.resourceAddress == r.Address { - resource = r - - break - } - } - - if resource == nil { - resp.Error = fmt.Errorf("%s - Resource not found in query", e.resourceAddress) - - return - } - - var data map[string]any - - err := json.Unmarshal(resource.SensitiveValues, &data) - - if err != nil { - resp.Error = fmt.Errorf("could not unmarshal SensitiveValues: %s", err) - - return - } - - result, err := tfjsonpath.Traverse(data, e.attributePath) - - if err != nil { - resp.Error = err - - return - } - - isSensitive, ok := result.(bool) - if !ok { - resp.Error = fmt.Errorf("invalid path: the path value cannot be asserted as bool") - - return - } - - if !isSensitive { - resp.Error = fmt.Errorf("attribute at path is not sensitive") - - return - } -} - -// ExpectSensitiveValue returns a query check that asserts that the specified attribute at the given resource has a sensitive value. -// -// Due to implementation differences between the terraform-plugin-sdk and the terraform-plugin-framework, representation of sensitive -// values may differ. For example, terraform-plugin-sdk based providers may have less precise representations of sensitive values, such -// as marking whole maps as sensitive rather than individual element values. -func ExpectSensitiveValue(resourceAddress string, attributePath tfjsonpath.Path) QueryCheck { - return expectSensitiveValue{ - resourceAddress: resourceAddress, - attributePath: attributePath, - } -} diff --git a/querycheck/expect_sensitive_value_test.go b/querycheck/expect_sensitive_value_test.go deleted file mode 100644 index 4b575571c..000000000 --- a/querycheck/expect_sensitive_value_test.go +++ /dev/null @@ -1,308 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck_test - -import ( - "context" - "regexp" - "testing" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - - r "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/querycheck" - "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" - "github.com/hashicorp/terraform-plugin-testing/tfversion" -) - -func Test_ExpectSensitiveValue_SensitiveStringAttribute(t *testing.T) { - t.Parallel() - - r.UnitTest(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_4_6), // QueryResource.SensitiveValues - }, - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProviderSensitive(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: ` - resource "test_resource" "one" { - sensitive_string_attribute = "test" - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectSensitiveValue("test_resource.one", - tfjsonpath.New("sensitive_string_attribute")), - }, - }, - }, - }) -} - -func Test_ExpectSensitiveValue_SensitiveListAttribute(t *testing.T) { - t.Parallel() - - r.UnitTest(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_4_6), // QueryResource.SensitiveValues - }, - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProviderSensitive(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: ` - resource "test_resource" "one" { - sensitive_list_attribute = ["value1"] - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectSensitiveValue("test_resource.one", - tfjsonpath.New("sensitive_list_attribute")), - }, - }, - }, - }) -} - -func Test_ExpectSensitiveValue_SensitiveSetAttribute(t *testing.T) { - t.Parallel() - - r.UnitTest(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_4_6), // QueryResource.SensitiveValues - }, - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProviderSensitive(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: ` - resource "test_resource" "one" { - sensitive_set_attribute = ["value1"] - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectSensitiveValue("test_resource.one", - tfjsonpath.New("sensitive_set_attribute")), - }, - }, - }, - }) -} - -func Test_ExpectSensitiveValue_SensitiveMapAttribute(t *testing.T) { - t.Parallel() - - r.UnitTest(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_4_6), // QueryResource.SensitiveValues - }, - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProviderSensitive(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: ` - resource "test_resource" "one" { - sensitive_map_attribute = { - key1 = "value1", - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectSensitiveValue("test_resource.one", - tfjsonpath.New("sensitive_map_attribute")), - }, - }, - }, - }) -} - -func Test_ExpectSensitiveValue_ListNestedBlock_SensitiveAttribute(t *testing.T) { - t.Parallel() - - r.UnitTest(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_4_6), // QueryResource.SensitiveValues - }, - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProviderSensitive(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: ` - resource "test_resource" "one" { - list_nested_block_sensitive_attribute { - sensitive_list_nested_block_attribute = "sensitive-test" - list_nested_block_attribute = "test" - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectSensitiveValue("test_resource.one", - tfjsonpath.New("list_nested_block_sensitive_attribute").AtSliceIndex(0). - AtMapKey("sensitive_list_nested_block_attribute")), - }, - }, - }, - }) -} - -func Test_ExpectSensitiveValue_SetNestedBlock_SensitiveAttribute(t *testing.T) { - t.Parallel() - - r.UnitTest(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_4_6), // QueryResource.SensitiveValues - }, - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProviderSensitive(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: ` - resource "test_resource" "one" { - set_nested_block_sensitive_attribute { - sensitive_set_nested_block_attribute = "sensitive-test" - set_nested_block_attribute = "test" - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectSensitiveValue("test_resource.one", - tfjsonpath.New("set_nested_block_sensitive_attribute")), - }, - }, - }, - }) -} - -func Test_ExpectSensitiveValue_ExpectError_ResourceNotFound(t *testing.T) { - t.Parallel() - - r.UnitTest(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_4_6), // QueryResource.SensitiveValues - }, - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProviderSensitive(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: ` - resource "test_resource" "one" {} - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectSensitiveValue("test_resource.two", tfjsonpath.New("set_attribute")), - }, - ExpectError: regexp.MustCompile(`test_resource.two - Resource not found in query`), - }, - }, - }) -} - -func testProviderSensitive() *schema.Provider { - return &schema.Provider{ - ResourcesMap: map[string]*schema.Resource{ - "test_resource": { - CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { - d.SetId("test") - return nil - }, - UpdateContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { - return nil - }, - DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { - return nil - }, - ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { - return nil - }, - Schema: map[string]*schema.Schema{ - "sensitive_string_attribute": { - Sensitive: true, - Optional: true, - Type: schema.TypeString, - }, - "sensitive_list_attribute": { - Sensitive: true, - Type: schema.TypeList, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - Optional: true, - }, - "sensitive_set_attribute": { - Sensitive: true, - Type: schema.TypeSet, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - Optional: true, - }, - "sensitive_map_attribute": { - Sensitive: true, - Type: schema.TypeMap, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - Optional: true, - }, - "list_nested_block_sensitive_attribute": { - Type: schema.TypeList, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "list_nested_block_attribute": { - Type: schema.TypeString, - Optional: true, - }, - "sensitive_list_nested_block_attribute": { - Sensitive: true, - Type: schema.TypeString, - Optional: true, - }, - }, - }, - }, - "set_nested_block_sensitive_attribute": { - Type: schema.TypeSet, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "set_nested_block_attribute": { - Type: schema.TypeString, - Optional: true, - }, - "sensitive_set_nested_block_attribute": { - Sensitive: true, - Type: schema.TypeString, - Optional: true, - }, - }, - }, - }, - }, - }, - }, - } -} diff --git a/querycheck/query_check.go b/querycheck/query_check.go index 289c28e55..98b987443 100644 --- a/querycheck/query_check.go +++ b/querycheck/query_check.go @@ -5,8 +5,7 @@ package querycheck import ( "context" - - tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" ) // QueryCheck defines an interface for implementing test logic that checks a query file and then returns an error @@ -19,7 +18,7 @@ type QueryCheck interface { // CheckQueryRequest is a request for an invoke of the CheckQuery function. type CheckQueryRequest struct { // Query represents a parsed query file, retrieved via the `terraform show -json` command. - Query *tfjson.Query + Query *plugintest.QueryResult } // CheckQueryResponse is a response to an invoke of the CheckQuery function. From a8e37e2770c1993978d22b684588349f31fe1368 Mon Sep 17 00:00:00 2001 From: Rain Date: Wed, 20 Aug 2025 10:23:26 -0400 Subject: [PATCH 15/45] Removed tests that get skipped for version --- helper/resource/testing_new.go | 2 +- querycheck/compare_value_test.go | 53 ------------- querycheck/expect_identity_test.go | 77 ------------------- ...xpect_identity_value_matches_query_test.go | 29 ------- querycheck/expect_identity_value_test.go | 30 -------- querycheck/expect_known_value_test.go | 2 +- querycheck/query_check.go | 1 + 7 files changed, 3 insertions(+), 191 deletions(-) diff --git a/helper/resource/testing_new.go b/helper/resource/testing_new.go index 63c03ca66..213ae3b5b 100644 --- a/helper/resource/testing_new.go +++ b/helper/resource/testing_new.go @@ -681,7 +681,7 @@ func copyWorkingDir(ctx context.Context, t testing.T, stepNumber int, wd *plugin dest := filepath.Join(workingDir, fmt.Sprintf("%s%s", "step_", strconv.Itoa(stepNumber))) baseDir := wd.BaseDir() - rootBaseDir := strings.TrimLeft(baseDir, workingDir) + rootBaseDir := strings.TrimPrefix(baseDir, workingDir) err := plugintest.CopyDir(workingDir, dest, rootBaseDir) if err != nil { diff --git a/querycheck/compare_value_test.go b/querycheck/compare_value_test.go index 16ced6aed..06e3810ed 100644 --- a/querycheck/compare_value_test.go +++ b/querycheck/compare_value_test.go @@ -135,59 +135,6 @@ func TestCompareValue_CheckQuery_ValuesSame(t *testing.T) { }) } -func TestCompareValue_CheckQuery_ValuesDiffer_ValueSameError(t *testing.T) { - t.Parallel() - - boolValuesDiffer := querycheck.CompareValue(compare.ValuesDiffer()) - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - bool_attribute = true - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - boolValuesDiffer.AddQueryValue( - "test_resource.one", - tfjsonpath.New("bool_attribute"), - ), - }, - }, - { - Config: `resource "test_resource" "one" { - bool_attribute = false - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - boolValuesDiffer.AddQueryValue( - "test_resource.one", - tfjsonpath.New("bool_attribute"), - ), - }, - }, - { - Config: `resource "test_resource" "one" { - bool_attribute = false - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - boolValuesDiffer.AddQueryValue( - "test_resource.one", - tfjsonpath.New("bool_attribute"), - ), - }, - ExpectError: regexp.MustCompile(`expected values to differ, but they are the same: false == false`), - }, - }, - }) -} - func TestCompareValue_CheckQuery_ValuesDiffer(t *testing.T) { t.Parallel() diff --git a/querycheck/expect_identity_test.go b/querycheck/expect_identity_test.go index cf7fa8969..1e32f9e33 100644 --- a/querycheck/expect_identity_test.go +++ b/querycheck/expect_identity_test.go @@ -50,83 +50,6 @@ func TestExpectIdentity_CheckQuery_ResourceNotFound(t *testing.T) { }) } -func TestExpectIdentity_CheckQuery_No_Terraform_Identity_Support(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories - tfversion.SkipAbove(tfversion.Version1_11_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - // Resource support identity, but the Terraform versions running will not. - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectIdentity( - "examplecloud_thing.one", - 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), - }, - ), - }, - ), - }, - ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in query. Either the resource ` + - `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, - ), - }, - }, - }) -} - -func TestExpectIdentity_CheckQuery_No_Identity(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - // Resource does not support identity - "examplecloud": examplecloudProviderNoIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectIdentity( - "examplecloud_thing.one", - 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), - }, - ), - }, - ), - }, - ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in query. Either the resource ` + - `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, - ), - }, - }, - }) -} - func TestExpectIdentity_CheckQuery(t *testing.T) { t.Parallel() diff --git a/querycheck/expect_identity_value_matches_query_test.go b/querycheck/expect_identity_value_matches_query_test.go index 58d40fb35..709efa3e2 100644 --- a/querycheck/expect_identity_value_matches_query_test.go +++ b/querycheck/expect_identity_value_matches_query_test.go @@ -45,35 +45,6 @@ func TestExpectIdentityValueMatchesQuery_CheckQuery_ResourceNotFound(t *testing. }) } -func TestExpectIdentityValueMatchesQuery_CheckQuery_No_Terraform_Identity_Support(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories - tfversion.SkipAbove(tfversion.Version1_11_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - // Resource support identity, but the Terraform versions running will not. - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectIdentityValueMatchesQuery( - "examplecloud_thing.one", - tfjsonpath.New("id"), - ), - }, - ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in query. Either the resource ` + - `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, - ), - }, - }, - }) -} - func TestExpectIdentityValueMatchesQuery_CheckQuery_No_Identity(t *testing.T) { t.Parallel() diff --git a/querycheck/expect_identity_value_test.go b/querycheck/expect_identity_value_test.go index 5569f6496..a4568df64 100644 --- a/querycheck/expect_identity_value_test.go +++ b/querycheck/expect_identity_value_test.go @@ -47,36 +47,6 @@ func TestExpectIdentityValue_CheckQuery_ResourceNotFound(t *testing.T) { }) } -func TestExpectIdentityValue_CheckQuery_No_Terraform_Identity_Support(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories - tfversion.SkipAbove(tfversion.Version1_11_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - // Resource support identity, but the Terraform versions running will not. - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectIdentityValue( - "examplecloud_thing.one", - tfjsonpath.New("id"), - knownvalue.StringExact("id-123"), - ), - }, - ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in query. Either the resource ` + - `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, - ), - }, - }, - }) -} - func TestExpectIdentityValue_CheckQuery_No_Identity(t *testing.T) { t.Parallel() diff --git a/querycheck/expect_known_value_test.go b/querycheck/expect_known_value_test.go index 8e5fc63b6..0f5a132df 100644 --- a/querycheck/expect_known_value_test.go +++ b/querycheck/expect_known_value_test.go @@ -1499,7 +1499,7 @@ func TestExpectKnownValue_CheckQuery_UnknownAttributeType(t *testing.T) { ResourceType: "attribute", }, }, - expectedErr: fmt.Errorf("error"), + expectedErr: fmt.Errorf("query does not contain any address values"), }, } diff --git a/querycheck/query_check.go b/querycheck/query_check.go index 98b987443..99e57fd9c 100644 --- a/querycheck/query_check.go +++ b/querycheck/query_check.go @@ -5,6 +5,7 @@ package querycheck import ( "context" + "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" ) From f59ae5d5683f00626cb01026fa7d10a3b77b3142 Mon Sep 17 00:00:00 2001 From: Steph Date: Fri, 22 Aug 2025 16:29:29 +0200 Subject: [PATCH 16/45] pass reattach info into QueryJSON call --- internal/plugintest/working_dir.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/plugintest/working_dir.go b/internal/plugintest/working_dir.go index c2693c99a..1b0c1ac5d 100644 --- a/internal/plugintest/working_dir.go +++ b/internal/plugintest/working_dir.go @@ -539,9 +539,11 @@ type QueryResult struct { func (wd *WorkingDir) Query(ctx context.Context) ([]string, error) { logging.HelperResourceTrace(ctx, "Calling Terraform CLI providers query command") + args := []tfexec.QueryOption{tfexec.Reattach(wd.reattachInfo)} + // Query the provider using the Terraform CLI function var buffer bytes.Buffer - err := wd.tf.QueryJSON(context.Background(), &buffer) + err := wd.tf.QueryJSON(context.Background(), &buffer, args...) var results []QueryResult if err := json.Unmarshal(buffer.Bytes(), &results); err != nil { From 280f10bd94e11fbf867ba48f8b984ce548a8f8f5 Mon Sep 17 00:00:00 2001 From: Steph Date: Mon, 25 Aug 2025 10:06:54 +0200 Subject: [PATCH 17/45] unmarshal found list result --- go.mod | 2 +- go.sum | 2 + helper/resource/query/query_test.go | 10 +++- internal/plugintest/working_dir.go | 85 +++++++++++++++++++++++++---- 4 files changed, 87 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index e2b9abf9b..d55a430d9 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/hashicorp/hcl/v2 v2.24.0 github.com/hashicorp/logutils v1.0.0 github.com/hashicorp/terraform-exec v0.23.1-0.20250717072919-061a850a52d2 - github.com/hashicorp/terraform-json v0.25.0 + github.com/hashicorp/terraform-json v0.26.1-0.20250813115529-3154666f4cc5 github.com/hashicorp/terraform-plugin-go v0.29.0-beta.1 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0 diff --git a/go.sum b/go.sum index 0b8071dd2..463063b71 100644 --- a/go.sum +++ b/go.sum @@ -80,6 +80,8 @@ github.com/hashicorp/terraform-exec v0.23.1-0.20250717072919-061a850a52d2 h1:90f github.com/hashicorp/terraform-exec v0.23.1-0.20250717072919-061a850a52d2/go.mod h1:8D3RLLpzAZdhT9jvALYz1KHyGU4OvI73I1o0+01QJxA= github.com/hashicorp/terraform-json v0.25.0 h1:rmNqc/CIfcWawGiwXmRuiXJKEiJu1ntGoxseG1hLhoQ= github.com/hashicorp/terraform-json v0.25.0/go.mod h1:sMKS8fiRDX4rVlR6EJUMudg1WcanxCMoWwTLkgZP/vc= +github.com/hashicorp/terraform-json v0.26.1-0.20250813115529-3154666f4cc5 h1:MT+7C2md19Fin/UDsKfGSeZgOXhHlzhTi3oWD5fM8QU= +github.com/hashicorp/terraform-json v0.26.1-0.20250813115529-3154666f4cc5/go.mod h1:eyWCeC3nrZamyrKLFnrvwpc3LQPIJsx8hWHQ/nu2/v4= github.com/hashicorp/terraform-plugin-go v0.29.0-beta.1 h1:xeHlRQYev3iMXwX2W7+D1bSfLRBs9jojZXqE6hmNxMI= github.com/hashicorp/terraform-plugin-go v0.29.0-beta.1/go.mod h1:5pww/UULn9C2tItq6o5sbScEkJxBUt9X9kI4DkeRsIw= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= diff --git a/helper/resource/query/query_test.go b/helper/resource/query/query_test.go index 4a2c94bb4..3ff0d7fdd 100644 --- a/helper/resource/query/query_test.go +++ b/helper/resource/query/query_test.go @@ -46,13 +46,21 @@ func TestQuery(t *testing.T) { }), }, Steps: []r.TestStep{ + { + Config: ` + resource "examplecloud_containerette" "primary" { + id = "westeurope/somevalue" + location = "westeurope" + name = "somevalue" + }`, + }, { Query: true, Config: ` provider "examplecloud" {} list "examplecloud_containerette" "test" { provider = examplecloud - + config { id = "bat" } diff --git a/internal/plugintest/working_dir.go b/internal/plugintest/working_dir.go index 1b0c1ac5d..d07aa96c6 100644 --- a/internal/plugintest/working_dir.go +++ b/internal/plugintest/working_dir.go @@ -8,12 +8,13 @@ import ( "context" "encoding/json" "fmt" + "github.com/hashicorp/terraform-exec/tfexec" + tfjson "github.com/hashicorp/terraform-json" "io" "os" "path/filepath" - - "github.com/hashicorp/terraform-exec/tfexec" - tfjson "github.com/hashicorp/terraform-json" + "strings" + "time" "github.com/hashicorp/terraform-plugin-testing/config" "github.com/hashicorp/terraform-plugin-testing/internal/logging" @@ -543,18 +544,42 @@ func (wd *WorkingDir) Query(ctx context.Context) ([]string, error) { // Query the provider using the Terraform CLI function var buffer bytes.Buffer + + // This returns a slice of log messages but is not expressed as a valid JSON array, so we're going to convert the + // buffer to a string, split this on new line then process each line individually since we're only interested in + // the list/query log messages err := wd.tf.QueryJSON(context.Background(), &buffer, args...) - var results []QueryResult - if err := json.Unmarshal(buffer.Bytes(), &results); err != nil { - return nil, fmt.Errorf("failed to unmarshal query result: %w", err) - } + bufSplit := strings.Split(string(buffer.Bytes()), "\n") + + returned := make([]Result, 0) + for _, line := range bufSplit { + if line == "" { + continue + } + d := json.NewDecoder(bytes.NewReader([]byte(line))) + + mt := msgType{} + err := d.Decode(&mt) + if err != nil { + return nil, err + } + + msg, err := unmarshalResult(mt.Type, []byte(line)) + if err != nil { + // TODO + } - returned := make([]string, len(results)) - for i, r := range results { - returned[i] = r.Address + if msg != nil { + returned = append(returned, *msg) + } } + //returned := make([]string, len(results)) + //for i, r := range results { + // returned[i] = r.Address + //} + if err != nil { return nil, fmt.Errorf("error running terraform query command: %w", err) } @@ -565,3 +590,43 @@ func (wd *WorkingDir) Query(ctx context.Context) ([]string, error) { return []string{output}, nil } + +// Taken from https://github.com/hashicorp/terraform-json/pull/169/ +const ( + MessageListResourceFound tfjson.LogMessageType = "list_resource_found" +) + +type ListResourceFoundMessage struct { + baseLogMessage + Address string `json:"address"` + DisplayName string `json:"display_name"` + Identity map[string]json.RawMessage `json:"identity"` + ResourceType string `json:"resource_type"` + ResourceObject map[string]json.RawMessage `json:"resource_object,omitempty"` + Config string `json:"config,omitempty"` + ImportConfig string `json:"import_config,omitempty"` +} + +type baseLogMessage struct { + Lvl tfjson.LogMessageLevel `json:"@level"` + Msg string `json:"@message"` + Time time.Time `json:"@timestamp"` +} + +type msgType struct { + Type tfjson.LogMessageType `json:"type"` +} + +type Result struct { + ListResourceFoundMessage `json:"list_resource_found"` +} + +func unmarshalResult(t tfjson.LogMessageType, b []byte) (*Result, error) { + v := Result{} + switch t { + case MessageListResourceFound: + return &v, json.Unmarshal(b, &v) + } + + return nil, nil +} From 549962f5013901961efbd9796c799211dd379dbb Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 26 Aug 2025 17:00:45 -0400 Subject: [PATCH 18/45] change tfversion checks to skip below `1.14` --- querycheck/expect_identity.go | 7 ++++--- querycheck/expect_identity_example_test.go | 4 ++-- querycheck/expect_identity_test.go | 14 +++++++------- querycheck/expect_identity_value.go | 5 +++-- .../expect_identity_value_example_test.go | 4 ++-- .../expect_identity_value_matches_query.go | 7 ++++--- ...ect_identity_value_matches_query_at_path.go | 7 ++++--- ...value_matches_query_at_path_example_test.go | 4 ++-- ...dentity_value_matches_query_at_path_test.go | 16 ++++++++-------- ...dentity_value_matches_query_example_test.go | 4 ++-- ...expect_identity_value_matches_query_test.go | 14 +++++++------- querycheck/expect_identity_value_test.go | 18 +++++++++--------- 12 files changed, 54 insertions(+), 50 deletions(-) diff --git a/querycheck/expect_identity.go b/querycheck/expect_identity.go index e0e79b03b..453964e3e 100644 --- a/querycheck/expect_identity.go +++ b/querycheck/expect_identity.go @@ -6,11 +6,12 @@ package querycheck import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" "maps" "slices" "sort" + "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" ) @@ -42,7 +43,7 @@ func (e expectIdentity) CheckQuery(ctx context.Context, req CheckQueryRequest, r } if resource.Identity == nil || len(resource.Identity) == 0 { - resp.Error = fmt.Errorf("%s - Identity not found in query. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.12+)", e.resourceAddress) + resp.Error = fmt.Errorf("%s - Identity not found in query. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.14+)", e.resourceAddress) return } @@ -88,7 +89,7 @@ func (e expectIdentity) CheckQuery(ctx context.Context, req CheckQueryRequest, r // map key represents an identity attribute name. The identity in query must exactly match the given object and any missing/extra // attributes will raise a diagnostic. // -// This query check can only be used with managed resources that support resource identity. Resource identity is only supported in Terraform v1.12+ +// This query check can only be used with managed resources that support resource identity. Resource identity is only supported in Terraform v1.14+ func ExpectIdentity(resourceAddress string, identity map[string]knownvalue.Check) QueryCheck { return expectIdentity{ resourceAddress: resourceAddress, diff --git a/querycheck/expect_identity_example_test.go b/querycheck/expect_identity_example_test.go index b625afc45..642fe61bd 100644 --- a/querycheck/expect_identity_example_test.go +++ b/querycheck/expect_identity_example_test.go @@ -18,9 +18,9 @@ func ExampleExpectIdentity() { t.Parallel() resource.Test(t, resource.TestCase{ - // Resource identity support is only available in Terraform v1.12+ + // Resource identity support is only available in Terraform v1.14+ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), + tfversion.SkipBelow(tfversion.Version1_14_0), }, // Provider definition omitted. Assuming "test_resource" has an identity schema with "id" and "name" string attributes Steps: []resource.TestStep{ diff --git a/querycheck/expect_identity_test.go b/querycheck/expect_identity_test.go index 1e32f9e33..18b73e3a1 100644 --- a/querycheck/expect_identity_test.go +++ b/querycheck/expect_identity_test.go @@ -20,7 +20,7 @@ func TestExpectIdentity_CheckQuery_ResourceNotFound(t *testing.T) { r.Test(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), + tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": examplecloudProviderWithResourceIdentity(), @@ -55,7 +55,7 @@ func TestExpectIdentity_CheckQuery(t *testing.T) { r.Test(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), + tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": examplecloudProviderWithResourceIdentity(), @@ -89,7 +89,7 @@ func TestExpectIdentity_CheckQuery_KnownValueWrongType(t *testing.T) { r.Test(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), + tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": examplecloudProviderWithResourceIdentity(), @@ -125,7 +125,7 @@ func TestExpectIdentity_CheckQuery_KnownValueWrongValue(t *testing.T) { r.Test(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), + tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": examplecloudProviderWithResourceIdentity(), @@ -161,7 +161,7 @@ func TestExpectIdentity_CheckQuery_ExtraAttribute(t *testing.T) { r.Test(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), + tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": examplecloudProviderWithResourceIdentity(), @@ -189,7 +189,7 @@ func TestExpectIdentity_CheckQuery_MissingAttribute(t *testing.T) { r.Test(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), + tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": examplecloudProviderWithResourceIdentity(), @@ -226,7 +226,7 @@ func TestExpectIdentity_CheckQuery_MismatchedAttribute(t *testing.T) { r.Test(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), + tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": examplecloudProviderWithResourceIdentity(), diff --git a/querycheck/expect_identity_value.go b/querycheck/expect_identity_value.go index 0f1045cde..0808255d3 100644 --- a/querycheck/expect_identity_value.go +++ b/querycheck/expect_identity_value.go @@ -6,6 +6,7 @@ package querycheck import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" "github.com/hashicorp/terraform-plugin-testing/knownvalue" @@ -47,7 +48,7 @@ func (e expectIdentityValue) CheckQuery(ctx context.Context, req CheckQueryReque } if resource.Identity == nil || len(resource.Identity) == 0 { - resp.Error = fmt.Errorf("%s - Identity not found in query. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.12+)", e.resourceAddress) + resp.Error = fmt.Errorf("%s - Identity not found in query. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.14+)", e.resourceAddress) return } @@ -70,7 +71,7 @@ func (e expectIdentityValue) CheckQuery(ctx context.Context, req CheckQueryReque // ExpectIdentityValue returns a query check that asserts that the specified identity attribute at the given resource // matches a known value. This query check can only be used with managed resources that support resource identity. // -// Resource identity is only supported in Terraform v1.12+ +// Resource identity is only supported in Terraform v1.14+ func ExpectIdentityValue(resourceAddress string, attributePath tfjsonpath.Path, identityValue knownvalue.Check) QueryCheck { return expectIdentityValue{ resourceAddress: resourceAddress, diff --git a/querycheck/expect_identity_value_example_test.go b/querycheck/expect_identity_value_example_test.go index 776632d68..4043e0a8d 100644 --- a/querycheck/expect_identity_value_example_test.go +++ b/querycheck/expect_identity_value_example_test.go @@ -19,9 +19,9 @@ func ExampleExpectIdentityValue() { t.Parallel() resource.Test(t, resource.TestCase{ - // Resource identity support is only available in Terraform v1.12+ + // Resource identity support is only available in Terraform v1.14+ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), + tfversion.SkipBelow(tfversion.Version1_14_0), }, // Provider definition omitted. Assuming "test_resource" has an identity schema with an "id" string attribute Steps: []resource.TestStep{ diff --git a/querycheck/expect_identity_value_matches_query.go b/querycheck/expect_identity_value_matches_query.go index 91c61d8dc..b4240c112 100644 --- a/querycheck/expect_identity_value_matches_query.go +++ b/querycheck/expect_identity_value_matches_query.go @@ -6,9 +6,10 @@ package querycheck import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" "reflect" + "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" ) @@ -40,7 +41,7 @@ func (e expectIdentityValueMatchesQuery) CheckQuery(ctx context.Context, req Che } if resource.Identity == nil || len(resource.Identity) == 0 { - resp.Error = fmt.Errorf("%s - Identity not found in query. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.12+)", e.resourceAddress) + resp.Error = fmt.Errorf("%s - Identity not found in query. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.14+)", e.resourceAddress) return } @@ -71,7 +72,7 @@ func (e expectIdentityValueMatchesQuery) CheckQuery(ctx context.Context, req Che // ExpectIdentityValueMatchesQuery returns a query check that asserts that the specified identity attribute at the given resource // matches the same attribute in query. This is useful when an identity attribute is in sync with a query attribute of the same path. // -// This query check can only be used with managed resources that support resource identity. Resource identity is only supported in Terraform v1.12+ +// This query check can only be used with managed resources that support resource identity. Resource identity is only supported in Terraform v1.14+ func ExpectIdentityValueMatchesQuery(resourceAddress string, attributePath tfjsonpath.Path) QueryCheck { return expectIdentityValueMatchesQuery{ resourceAddress: resourceAddress, diff --git a/querycheck/expect_identity_value_matches_query_at_path.go b/querycheck/expect_identity_value_matches_query_at_path.go index 979a604c2..f36adbe76 100644 --- a/querycheck/expect_identity_value_matches_query_at_path.go +++ b/querycheck/expect_identity_value_matches_query_at_path.go @@ -6,9 +6,10 @@ package querycheck import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" "reflect" + "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" ) @@ -47,7 +48,7 @@ func (e expectIdentityValueMatchesQueryAtPath) CheckQuery(ctx context.Context, r } if resource.Identity == nil || len(resource.Identity) == 0 { - resp.Error = fmt.Errorf("%s - Identity not found in query. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.12+)", e.resourceAddress) + resp.Error = fmt.Errorf("%s - Identity not found in query. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.14+)", e.resourceAddress) return } @@ -85,7 +86,7 @@ func (e expectIdentityValueMatchesQueryAtPath) CheckQuery(ctx context.Context, r // ExpectIdentityValueMatchesQueryAtPath returns a query check that asserts that the specified identity attribute at the given resource // matches the specified attribute in query. This is useful when an identity attribute is in sync with a query attribute of a different path. // -// This query check can only be used with managed resources that support resource identity. Resource identity is only supported in Terraform v1.12+ +// This query check can only be used with managed resources that support resource identity. Resource identity is only supported in Terraform v1.14+ func ExpectIdentityValueMatchesQueryAtPath(resourceAddress string, identityAttrPath, queryAttrPath tfjsonpath.Path) QueryCheck { return expectIdentityValueMatchesQueryAtPath{ resourceAddress: resourceAddress, diff --git a/querycheck/expect_identity_value_matches_query_at_path_example_test.go b/querycheck/expect_identity_value_matches_query_at_path_example_test.go index 4824cf8d6..6764c1781 100644 --- a/querycheck/expect_identity_value_matches_query_at_path_example_test.go +++ b/querycheck/expect_identity_value_matches_query_at_path_example_test.go @@ -18,9 +18,9 @@ func ExampleExpectIdentityValueMatchesQueryAtPath() { t.Parallel() resource.Test(t, resource.TestCase{ - // Resource identity support is only available in Terraform v1.12+ + // Resource identity support is only available in Terraform v1.14+ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), + tfversion.SkipBelow(tfversion.Version1_14_0), }, // Provider definition omitted. Assuming "test_resource": // - Has an identity schema with an "identity_id" string attribute diff --git a/querycheck/expect_identity_value_matches_query_at_path_test.go b/querycheck/expect_identity_value_matches_query_at_path_test.go index 4dbb5e921..8b9f7dc5b 100644 --- a/querycheck/expect_identity_value_matches_query_at_path_test.go +++ b/querycheck/expect_identity_value_matches_query_at_path_test.go @@ -25,7 +25,7 @@ func TestExpectIdentityValueMatchesQueryAtPath_CheckQuery_ResourceNotFound(t *te r.Test(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), + tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": examplecloudProviderWithResourceIdentity(), @@ -69,7 +69,7 @@ func TestExpectIdentityValueMatchesQueryAtPath_CheckQuery_No_Terraform_Identity_ ), }, ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in query. Either the resource ` + - `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, + `does not support identity or the Terraform version running the test does not support identity. \(must be v1.14\+\)`, ), }, }, @@ -81,7 +81,7 @@ func TestExpectIdentityValueMatchesQueryAtPath_CheckQuery_No_Identity(t *testing r.Test(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), + tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ // Resource does not support identity @@ -98,7 +98,7 @@ func TestExpectIdentityValueMatchesQueryAtPath_CheckQuery_No_Identity(t *testing ), }, ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in query. Either the resource ` + - `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, + `does not support identity or the Terraform version running the test does not support identity. \(must be v1.14\+\)`, ), }, }, @@ -110,7 +110,7 @@ func TestExpectIdentityValueMatchesQueryAtPath_CheckQuery_String_Matches(t *test r.Test(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), + tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": examplecloudProviderWithResourceIdentityDifferentPaths(), @@ -135,7 +135,7 @@ func TestExpectIdentityValueMatchesQueryAtPath_CheckQuery_String_DoesntMatch(t * r.Test(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), + tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": examplecloudProviderWithMismatchedResourceIdentity(), @@ -161,7 +161,7 @@ func TestExpectIdentityValueMatchesQueryAtPath_CheckQuery_List(t *testing.T) { r.Test(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), + tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": examplecloudProviderWithResourceIdentityDifferentPaths(), @@ -186,7 +186,7 @@ func TestExpectIdentityValueMatchesQueryAtPath_CheckQuery_List_DoesntMatch(t *te r.Test(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), + tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": examplecloudProviderWithMismatchedResourceIdentity(), diff --git a/querycheck/expect_identity_value_matches_query_example_test.go b/querycheck/expect_identity_value_matches_query_example_test.go index d0ae5d600..390f80fe0 100644 --- a/querycheck/expect_identity_value_matches_query_example_test.go +++ b/querycheck/expect_identity_value_matches_query_example_test.go @@ -18,9 +18,9 @@ func ExampleExpectIdentityValueMatchesQuery() { t.Parallel() resource.Test(t, resource.TestCase{ - // Resource identity support is only available in Terraform v1.12+ + // Resource identity support is only available in Terraform v1.14+ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), + tfversion.SkipBelow(tfversion.Version1_14_0), }, // Provider definition omitted. Assuming "test_resource": // - Has an identity schema with an "id" string attribute diff --git a/querycheck/expect_identity_value_matches_query_test.go b/querycheck/expect_identity_value_matches_query_test.go index 709efa3e2..a18a380ed 100644 --- a/querycheck/expect_identity_value_matches_query_test.go +++ b/querycheck/expect_identity_value_matches_query_test.go @@ -25,7 +25,7 @@ func TestExpectIdentityValueMatchesQuery_CheckQuery_ResourceNotFound(t *testing. r.Test(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), + tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": examplecloudProviderWithResourceIdentity(), @@ -50,7 +50,7 @@ func TestExpectIdentityValueMatchesQuery_CheckQuery_No_Identity(t *testing.T) { r.Test(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), + tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ // Resource does not support identity @@ -66,7 +66,7 @@ func TestExpectIdentityValueMatchesQuery_CheckQuery_No_Identity(t *testing.T) { ), }, ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in query. Either the resource ` + - `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, + `does not support identity or the Terraform version running the test does not support identity. \(must be v1.14\+\)`, ), }, }, @@ -78,7 +78,7 @@ func TestExpectIdentityValueMatchesQuery_CheckQuery_String_Matches(t *testing.T) r.Test(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), + tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": examplecloudProviderWithResourceIdentity(), @@ -102,7 +102,7 @@ func TestExpectIdentityValueMatchesQuery_CheckQuery_String_DoesntMatch(t *testin r.Test(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), + tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": examplecloudProviderWithMismatchedResourceIdentity(), @@ -127,7 +127,7 @@ func TestExpectIdentityValueMatchesQuery_CheckQuery_List(t *testing.T) { r.Test(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), + tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": examplecloudProviderWithResourceIdentity(), @@ -151,7 +151,7 @@ func TestExpectIdentityValueMatchesQuery_CheckQuery_List_DoesntMatch(t *testing. r.Test(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), + tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": examplecloudProviderWithMismatchedResourceIdentity(), diff --git a/querycheck/expect_identity_value_test.go b/querycheck/expect_identity_value_test.go index a4568df64..d7042ca87 100644 --- a/querycheck/expect_identity_value_test.go +++ b/querycheck/expect_identity_value_test.go @@ -26,7 +26,7 @@ func TestExpectIdentityValue_CheckQuery_ResourceNotFound(t *testing.T) { r.Test(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), + tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": examplecloudProviderWithResourceIdentity(), @@ -52,7 +52,7 @@ func TestExpectIdentityValue_CheckQuery_No_Identity(t *testing.T) { r.Test(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), + tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ // Resource does not support identity @@ -69,7 +69,7 @@ func TestExpectIdentityValue_CheckQuery_No_Identity(t *testing.T) { ), }, ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in query. Either the resource ` + - `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, + `does not support identity or the Terraform version running the test does not support identity. \(must be v1.14\+\)`, ), }, }, @@ -81,7 +81,7 @@ func TestExpectIdentityValue_CheckQuery_String(t *testing.T) { r.Test(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), + tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": examplecloudProviderWithResourceIdentity(), @@ -105,7 +105,7 @@ func TestExpectIdentityValue_CheckQuery_String_KnownValueWrongType(t *testing.T) r.Test(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), + tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": examplecloudProviderWithResourceIdentity(), @@ -130,7 +130,7 @@ func TestExpectIdentityValue_CheckQuery_String_KnownValueWrongValue(t *testing.T r.Test(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), + tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": examplecloudProviderWithResourceIdentity(), @@ -155,7 +155,7 @@ func TestExpectIdentityValue_CheckQuery_List(t *testing.T) { r.Test(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), + tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": examplecloudProviderWithResourceIdentity(), @@ -195,7 +195,7 @@ func TestExpectIdentityValue_CheckQuery_List_KnownValueWrongType(t *testing.T) { r.Test(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), + tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": examplecloudProviderWithResourceIdentity(), @@ -222,7 +222,7 @@ func TestExpectIdentityValue_CheckQuery_List_KnownValueWrongValue(t *testing.T) r.Test(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), + tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": examplecloudProviderWithResourceIdentity(), From a46b2dc7fd0358d485e5ff30e76d630e6cab8713 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 26 Aug 2025 19:44:44 -0400 Subject: [PATCH 19/45] try unmarshalling query results and traversing using `tfjsonpath` --- helper/resource/testing_new.go | 4 +- internal/plugintest/working_dir.go | 76 ++++++++++++++++++------------ 2 files changed, 49 insertions(+), 31 deletions(-) diff --git a/helper/resource/testing_new.go b/helper/resource/testing_new.go index 213ae3b5b..d06dd2368 100644 --- a/helper/resource/testing_new.go +++ b/helper/resource/testing_new.go @@ -376,7 +376,7 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest t.Fatalf("Step %d/%d error running init: %s", stepNumber, len(c.Steps), err) } - var queryOut []string + var queryOut any err = runProviderCommand(ctx, t, wd, providers, func() error { var err error queryOut, err = wd.Query(ctx) @@ -388,7 +388,9 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest } fmt.Printf("Step %d/%d Query Output:\n%s\n", stepNumber, len(c.Steps), queryOut) + continue + } if cfg != nil { diff --git a/internal/plugintest/working_dir.go b/internal/plugintest/working_dir.go index d07aa96c6..e74feb9af 100644 --- a/internal/plugintest/working_dir.go +++ b/internal/plugintest/working_dir.go @@ -8,17 +8,19 @@ import ( "context" "encoding/json" "fmt" - "github.com/hashicorp/terraform-exec/tfexec" - tfjson "github.com/hashicorp/terraform-json" "io" "os" "path/filepath" "strings" "time" + "github.com/hashicorp/terraform-exec/tfexec" + tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-plugin-testing/config" "github.com/hashicorp/terraform-plugin-testing/internal/logging" "github.com/hashicorp/terraform-plugin-testing/internal/teststep" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" ) const ( @@ -537,7 +539,7 @@ type QueryResult struct { ImportConfig string `json:"import_config,omitempty"` } -func (wd *WorkingDir) Query(ctx context.Context) ([]string, error) { +func (wd *WorkingDir) Query(ctx context.Context) (any, error) { logging.HelperResourceTrace(ctx, "Calling Terraform CLI providers query command") args := []tfexec.QueryOption{tfexec.Reattach(wd.reattachInfo)} @@ -545,50 +547,64 @@ func (wd *WorkingDir) Query(ctx context.Context) ([]string, error) { // Query the provider using the Terraform CLI function var buffer bytes.Buffer + var unmarshalled map[string]any + // This returns a slice of log messages but is not expressed as a valid JSON array, so we're going to convert the // buffer to a string, split this on new line then process each line individually since we're only interested in // the list/query log messages - err := wd.tf.QueryJSON(context.Background(), &buffer, args...) + _ = wd.tf.QueryJSON(context.Background(), &buffer, args...) bufSplit := strings.Split(string(buffer.Bytes()), "\n") - returned := make([]Result, 0) for _, line := range bufSplit { if line == "" { continue } - d := json.NewDecoder(bytes.NewReader([]byte(line))) - - mt := msgType{} - err := d.Decode(&mt) + err := json.Unmarshal([]byte(line), &unmarshalled) if err != nil { return nil, err } - msg, err := unmarshalResult(mt.Type, []byte(line)) - if err != nil { - // TODO - } - - if msg != nil { - returned = append(returned, *msg) + traverse, _ := tfjsonpath.Traverse(unmarshalled, tfjsonpath.New("list_resource_found")) + if traverse != nil { + return traverse, nil } } - - //returned := make([]string, len(results)) - //for i, r := range results { - // returned[i] = r.Address + return bufSplit, nil + + //returned := make([]Result, 0) + //for _, line := range bufSplit { + // if line == "" { + // continue + // } + // d := json.NewDecoder(bytes.NewReader([]byte(line))) + // + // mt := msgType{} + // err := d.Decode(&mt) + // if err != nil { + // return nil, err + // } + // + // msg, err := unmarshalResult(mt.Type, []byte(line)) + // if err != nil { + // // TODO + // } + // + // if msg != nil { + // returned = append(returned, *msg) + // } //} - - if err != nil { - return nil, fmt.Errorf("error running terraform query command: %w", err) - } - - logging.HelperResourceTrace(ctx, "Called Terraform CLI providers query command") - - output := buffer.String() - - return []string{output}, nil + // + ////returned := make([]string, len(results)) + ////for i, r := range results { + //// returned[i] = r.Address + ////} + // + //if err != nil { + // return nil, fmt.Errorf("error running terraform query command: %w", err) + //} + // + //logging.HelperResourceTrace(ctx, "Called Terraform CLI providers query command") } // Taken from https://github.com/hashicorp/terraform-json/pull/169/ From 6953c7f4c6bf85990e2831bc48640eab86329409 Mon Sep 17 00:00:00 2001 From: Rain Date: Tue, 2 Sep 2025 12:10:09 -0400 Subject: [PATCH 20/45] Updated go mod to latest --- go.mod | 4 ++-- go.sum | 10 ++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index d55a430d9..01747ca18 100644 --- a/go.mod +++ b/go.mod @@ -14,12 +14,12 @@ require ( github.com/hashicorp/hcl/v2 v2.24.0 github.com/hashicorp/logutils v1.0.0 github.com/hashicorp/terraform-exec v0.23.1-0.20250717072919-061a850a52d2 - github.com/hashicorp/terraform-json v0.26.1-0.20250813115529-3154666f4cc5 + github.com/hashicorp/terraform-json v0.26.1-0.20250829125600-5c1a00f3ccc4 github.com/hashicorp/terraform-plugin-go v0.29.0-beta.1 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0 github.com/mitchellh/go-testing-interface v1.14.1 - github.com/zclconf/go-cty v1.16.3 + github.com/zclconf/go-cty v1.16.4 golang.org/x/crypto v0.40.0 ) diff --git a/go.sum b/go.sum index 463063b71..fce1840ca 100644 --- a/go.sum +++ b/go.sum @@ -78,10 +78,8 @@ github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/terraform-exec v0.23.1-0.20250717072919-061a850a52d2 h1:90fcAqw0Qmv4vY7zL4jEKgKarHmOnNN6SjTY68eLKGA= github.com/hashicorp/terraform-exec v0.23.1-0.20250717072919-061a850a52d2/go.mod h1:8D3RLLpzAZdhT9jvALYz1KHyGU4OvI73I1o0+01QJxA= -github.com/hashicorp/terraform-json v0.25.0 h1:rmNqc/CIfcWawGiwXmRuiXJKEiJu1ntGoxseG1hLhoQ= -github.com/hashicorp/terraform-json v0.25.0/go.mod h1:sMKS8fiRDX4rVlR6EJUMudg1WcanxCMoWwTLkgZP/vc= -github.com/hashicorp/terraform-json v0.26.1-0.20250813115529-3154666f4cc5 h1:MT+7C2md19Fin/UDsKfGSeZgOXhHlzhTi3oWD5fM8QU= -github.com/hashicorp/terraform-json v0.26.1-0.20250813115529-3154666f4cc5/go.mod h1:eyWCeC3nrZamyrKLFnrvwpc3LQPIJsx8hWHQ/nu2/v4= +github.com/hashicorp/terraform-json v0.26.1-0.20250829125600-5c1a00f3ccc4 h1:qErXY0TfojskxvAlCiqS4IMmXulQ4TfApiO8IlKkImc= +github.com/hashicorp/terraform-json v0.26.1-0.20250829125600-5c1a00f3ccc4/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE= github.com/hashicorp/terraform-plugin-go v0.29.0-beta.1 h1:xeHlRQYev3iMXwX2W7+D1bSfLRBs9jojZXqE6hmNxMI= github.com/hashicorp/terraform-plugin-go v0.29.0-beta.1/go.mod h1:5pww/UULn9C2tItq6o5sbScEkJxBUt9X9kI4DkeRsIw= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= @@ -148,8 +146,8 @@ github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk= -github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +github.com/zclconf/go-cty v1.16.4 h1:QGXaag7/7dCzb+odlGrgr+YmYZFaOCMW6DEpS+UD1eE= +github.com/zclconf/go-cty v1.16.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= From 95081f880861ec5b852ac5b8b2561786483a546d Mon Sep 17 00:00:00 2001 From: Rain Date: Tue, 2 Sep 2025 16:28:03 -0400 Subject: [PATCH 21/45] Changes from pairing --- go.mod | 2 +- go.sum | 4 +- helper/resource/query/query_checks.go | 5 +- internal/plugintest/working_dir.go | 70 +- querycheck/compare_value.go | 102 - querycheck/compare_value_collection.go | 202 -- querycheck/compare_value_collection_test.go | 1988 ----------------- querycheck/compare_value_pairs.go | 90 - querycheck/compare_value_pairs_test.go | 142 -- querycheck/compare_value_test.go | 188 -- querycheck/expect_identity.go | 122 - querycheck/expect_identity_example_test.go | 41 - querycheck/expect_identity_test.go | 257 --- querycheck/expect_identity_value.go | 81 - .../expect_identity_value_example_test.go | 40 - .../expect_identity_value_matches_query.go | 81 - ...ct_identity_value_matches_query_at_path.go | 96 - ...alue_matches_query_at_path_example_test.go | 42 - ...entity_value_matches_query_at_path_test.go | 344 --- ...entity_value_matches_query_example_test.go | 38 - ...xpect_identity_value_matches_query_test.go | 308 --- querycheck/expect_identity_value_test.go | 431 ---- querycheck/expect_known_value.go | 73 - querycheck/expect_known_value_example_test.go | 38 - querycheck/expect_known_value_test.go | 1644 -------------- querycheck/query_check.go | 4 +- 26 files changed, 45 insertions(+), 6388 deletions(-) delete mode 100644 querycheck/compare_value.go delete mode 100644 querycheck/compare_value_collection.go delete mode 100644 querycheck/compare_value_collection_test.go delete mode 100644 querycheck/compare_value_pairs.go delete mode 100644 querycheck/compare_value_pairs_test.go delete mode 100644 querycheck/compare_value_test.go delete mode 100644 querycheck/expect_identity.go delete mode 100644 querycheck/expect_identity_example_test.go delete mode 100644 querycheck/expect_identity_test.go delete mode 100644 querycheck/expect_identity_value.go delete mode 100644 querycheck/expect_identity_value_example_test.go delete mode 100644 querycheck/expect_identity_value_matches_query.go delete mode 100644 querycheck/expect_identity_value_matches_query_at_path.go delete mode 100644 querycheck/expect_identity_value_matches_query_at_path_example_test.go delete mode 100644 querycheck/expect_identity_value_matches_query_at_path_test.go delete mode 100644 querycheck/expect_identity_value_matches_query_example_test.go delete mode 100644 querycheck/expect_identity_value_matches_query_test.go delete mode 100644 querycheck/expect_identity_value_test.go delete mode 100644 querycheck/expect_known_value.go delete mode 100644 querycheck/expect_known_value_example_test.go delete mode 100644 querycheck/expect_known_value_test.go diff --git a/go.mod b/go.mod index 01747ca18..8f5c74a0e 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/hashicorp/hc-install v0.9.2 github.com/hashicorp/hcl/v2 v2.24.0 github.com/hashicorp/logutils v1.0.0 - github.com/hashicorp/terraform-exec v0.23.1-0.20250717072919-061a850a52d2 + github.com/hashicorp/terraform-exec v0.23.1-0.20250902152613-b3bfedb74246 github.com/hashicorp/terraform-json v0.26.1-0.20250829125600-5c1a00f3ccc4 github.com/hashicorp/terraform-plugin-go v0.29.0-beta.1 github.com/hashicorp/terraform-plugin-log v0.9.0 diff --git a/go.sum b/go.sum index fce1840ca..9303cdcec 100644 --- a/go.sum +++ b/go.sum @@ -76,8 +76,8 @@ github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQx github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/terraform-exec v0.23.1-0.20250717072919-061a850a52d2 h1:90fcAqw0Qmv4vY7zL4jEKgKarHmOnNN6SjTY68eLKGA= -github.com/hashicorp/terraform-exec v0.23.1-0.20250717072919-061a850a52d2/go.mod h1:8D3RLLpzAZdhT9jvALYz1KHyGU4OvI73I1o0+01QJxA= +github.com/hashicorp/terraform-exec v0.23.1-0.20250902152613-b3bfedb74246 h1:x0J+WssCgSoU+lXLhZceitUlKfq5DvrtO8EwiEUa8Ng= +github.com/hashicorp/terraform-exec v0.23.1-0.20250902152613-b3bfedb74246/go.mod h1:rG9V56jmBbB5hXCo4MpZTr6tYKLaykUONa8mX01yBhg= github.com/hashicorp/terraform-json v0.26.1-0.20250829125600-5c1a00f3ccc4 h1:qErXY0TfojskxvAlCiqS4IMmXulQ4TfApiO8IlKkImc= github.com/hashicorp/terraform-json v0.26.1-0.20250829125600-5c1a00f3ccc4/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE= github.com/hashicorp/terraform-plugin-go v0.29.0-beta.1 h1:xeHlRQYev3iMXwX2W7+D1bSfLRBs9jojZXqE6hmNxMI= diff --git a/helper/resource/query/query_checks.go b/helper/resource/query/query_checks.go index b5de7c6bb..20de966e9 100644 --- a/helper/resource/query/query_checks.go +++ b/helper/resource/query/query_checks.go @@ -6,13 +6,14 @@ package query import ( "context" "errors" - "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" + + tfjson "github.com/hashicorp/terraform-json" "github.com/mitchellh/go-testing-interface" "github.com/hashicorp/terraform-plugin-testing/querycheck" ) -func runQueryChecks(ctx context.Context, t testing.T, query *plugintest.QueryResult, queryChecks []querycheck.QueryCheck) error { +func runQueryChecks(ctx context.Context, t testing.T, query *[]tfjson.LogMsg, queryChecks []querycheck.QueryCheck) error { t.Helper() var result []error diff --git a/internal/plugintest/working_dir.go b/internal/plugintest/working_dir.go index e74feb9af..4712720b2 100644 --- a/internal/plugintest/working_dir.go +++ b/internal/plugintest/working_dir.go @@ -4,14 +4,12 @@ package plugintest import ( - "bytes" "context" "encoding/json" "fmt" "io" "os" "path/filepath" - "strings" "time" "github.com/hashicorp/terraform-exec/tfexec" @@ -20,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-testing/config" "github.com/hashicorp/terraform-plugin-testing/internal/logging" "github.com/hashicorp/terraform-plugin-testing/internal/teststep" - "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" ) const ( @@ -529,34 +526,33 @@ func (wd *WorkingDir) Schemas(ctx context.Context) (*tfjson.ProviderSchemas, err return providerSchemas, err } -type QueryResult struct { - Address string `json:"address"` - DisplayName string `json:"display_name"` - Identity map[string]json.RawMessage `json:"identity"` - ResourceType string `json:"resource_type"` - ResourceObject map[string]json.RawMessage `json:"resource_object,omitempty"` - Config string `json:"config,omitempty"` - ImportConfig string `json:"import_config,omitempty"` -} - -func (wd *WorkingDir) Query(ctx context.Context) (any, error) { +func (wd *WorkingDir) Query(ctx context.Context) ([]tfjson.LogMsg, error) { logging.HelperResourceTrace(ctx, "Calling Terraform CLI providers query command") args := []tfexec.QueryOption{tfexec.Reattach(wd.reattachInfo)} + var messages []tfjson.LogMsg + // Query the provider using the Terraform CLI function - var buffer bytes.Buffer + //var buffer bytes.Buffer - var unmarshalled map[string]any + // var unmarshalled map[string]any // This returns a slice of log messages but is not expressed as a valid JSON array, so we're going to convert the // buffer to a string, split this on new line then process each line individually since we're only interested in // the list/query log messages - _ = wd.tf.QueryJSON(context.Background(), &buffer, args...) + var logEmit *tfexec.LogMsgEmitter + var execErr, err error + + logEmit, execErr = wd.tf.QueryJSON(context.Background(), args...) + + if execErr != nil { + return nil, fmt.Errorf("error running terraform query command: %w", err) + } - bufSplit := strings.Split(string(buffer.Bytes()), "\n") + //bufSplit := strings.Split(string(buffer.Bytes()), "\n") - for _, line := range bufSplit { + /*for _, line := range bufSplit { if line == "" { continue } @@ -569,10 +565,24 @@ func (wd *WorkingDir) Query(ctx context.Context) (any, error) { if traverse != nil { return traverse, nil } + }*/ + + + + var message tfjson.LogMsg + for message, err = logEmit.NextMessage(); err != nil || message != nil; { + messages = append(messages, message) } - return bufSplit, nil + if err != nil { + return nil, fmt.Errorf("error running terraform query command: %w", err) + } + + logging.HelperResourceTrace(ctx, "Called Terraform CLI providers query command") + + return messages, nil + + // do a type onversion to list start data or list found message - //returned := make([]Result, 0) //for _, line := range bufSplit { // if line == "" { // continue @@ -594,17 +604,11 @@ func (wd *WorkingDir) Query(ctx context.Context) (any, error) { // returned = append(returned, *msg) // } //} - // - ////returned := make([]string, len(results)) - ////for i, r := range results { - //// returned[i] = r.Address - ////} - // - //if err != nil { - // return nil, fmt.Errorf("error running terraform query command: %w", err) + + //returned := make([]string, len(results)) + //for i, r := range results { + // returned[i] = r.Address //} - // - //logging.HelperResourceTrace(ctx, "Called Terraform CLI providers query command") } // Taken from https://github.com/hashicorp/terraform-json/pull/169/ @@ -637,8 +641,8 @@ type Result struct { ListResourceFoundMessage `json:"list_resource_found"` } -func unmarshalResult(t tfjson.LogMessageType, b []byte) (*Result, error) { - v := Result{} +func unmarshalResult(t tfjson.LogMessageType, b []byte) (*tfjson.ListResourceFoundData, error) { + v := tfjson.ListResourceFoundData{} switch t { case MessageListResourceFound: return &v, json.Unmarshal(b, &v) diff --git a/querycheck/compare_value.go b/querycheck/compare_value.go deleted file mode 100644 index 93cc65f11..000000000 --- a/querycheck/compare_value.go +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -import ( - "context" - "fmt" - "github.com/hashicorp/terraform-plugin-testing/compare" - "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" - "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" -) - -// Resource Query Check -var _ QueryCheck = &compareValue{} - -type compareValue struct { - resourceAddresses []string - attributePaths []tfjsonpath.Path - queryValues []any - comparer compare.ValueComparer -} - -func (e *compareValue) AddQueryValue(resourceAddress string, attributePath tfjsonpath.Path) QueryCheck { - e.resourceAddresses = append(e.resourceAddresses, resourceAddress) - e.attributePaths = append(e.attributePaths, attributePath) - - return e -} - -// CheckQuery implements the query check logic. -func (e *compareValue) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { - var resource *plugintest.QueryResult - - if req.Query == nil { - resp.Error = fmt.Errorf("query is nil") - - return - } - - if len(req.Query.Address) == 0 { - resp.Error = fmt.Errorf("query does not contain any address values") - - return - } - - // All calls to AddQueryValue occur before any TestStep is run, populating the resourceAddresses - // and attributePaths slices. The queryValues slice is populated during execution of each TestStep. - // Each call to CheckQuery happens sequentially during each TestStep. - // The currentIndex is reflective of the current query value being checked. - currentIndex := len(e.queryValues) - - if len(e.resourceAddresses) <= currentIndex { - resp.Error = fmt.Errorf("resource addresses index out of bounds: %d", currentIndex) - - return - } - - resourceAddress := e.resourceAddresses[currentIndex] - - if resourceAddress == req.Query.Address { - resource = req.Query - } - - if resource == nil { - resp.Error = fmt.Errorf("%s - Resource not found in query", resourceAddress) - - return - } - - if len(e.attributePaths) <= currentIndex { - resp.Error = fmt.Errorf("attribute paths index out of bounds: %d", currentIndex) - - return - } - - attributePath := e.attributePaths[currentIndex] - - result, err := tfjsonpath.Traverse(resource.ResourceObject, attributePath) - - if err != nil { - resp.Error = err - - return - } - - e.queryValues = append(e.queryValues, result) - - err = e.comparer.CompareValues(e.queryValues...) - - if err != nil { - resp.Error = err - } -} - -// CompareValue returns a query check that compares values retrieved from query using the -// supplied value comparer. -func CompareValue(comparer compare.ValueComparer) *compareValue { - return &compareValue{ - comparer: comparer, - } -} diff --git a/querycheck/compare_value_collection.go b/querycheck/compare_value_collection.go deleted file mode 100644 index 4ca546f7b..000000000 --- a/querycheck/compare_value_collection.go +++ /dev/null @@ -1,202 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -import ( - "context" - "errors" - "fmt" - "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" - "sort" - - "github.com/hashicorp/terraform-plugin-testing/compare" - "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" -) - -// Resource Query Check -var _ QueryCheck = &compareValueCollection{} - -type compareValueCollection struct { - resourceAddressOne string - collectionPath []tfjsonpath.Path - resourceAddressTwo string - attributePath tfjsonpath.Path - comparer compare.ValueComparer -} - -func walkCollectionPath(obj any, paths []tfjsonpath.Path, results []any) ([]any, error) { - switch t := obj.(type) { - case []any: - for _, v := range t { - if len(paths) == 0 { - results = append(results, v) - continue - } - - x, err := tfjsonpath.Traverse(v, paths[0]) - - if err != nil { - return results, err - } - - results, err = walkCollectionPath(x, paths[1:], results) - - if err != nil { - return results, err - } - } - case map[string]any: - keys := make([]string, 0, len(t)) - - for k := range t { - keys = append(keys, k) - } - - sort.Strings(keys) - - for _, key := range keys { - if len(paths) == 0 { - results = append(results, t[key]) - continue - } - - x, err := tfjsonpath.Traverse(t, paths[0]) - - if err != nil { - return results, err - } - - results, err = walkCollectionPath(x, paths[1:], results) - - if err != nil { - return results, err - } - } - default: - results = append(results, obj) - } - - return results, nil -} - -// CheckQuery implements the query check logic. -func (e *compareValueCollection) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { - var resourceOne *plugintest.QueryResult - var resourceTwo *plugintest.QueryResult - - if req.Query == nil { - resp.Error = fmt.Errorf("query is nil") - - return - } - - if e.resourceAddressOne == req.Query.Address { - resourceOne = req.Query - } - - if resourceOne == nil { - resp.Error = fmt.Errorf("%s - Resource not found in query", e.resourceAddressOne) - - return - } - - if len(e.collectionPath) == 0 { - resp.Error = fmt.Errorf("%s - No collection path was provided", e.resourceAddressOne) - - return - } - - resultOne, err := tfjsonpath.Traverse(resourceOne.ResourceObject, e.collectionPath[0]) - - if err != nil { - resp.Error = err - - return - } - - // Verify resultOne is a collection. - switch t := resultOne.(type) { - case []any, map[string]any: - // Collection found. - default: - var pathStr string - - for _, v := range e.collectionPath { - pathStr += fmt.Sprintf(".%s", v.String()) - } - - resp.Error = fmt.Errorf("%s%s is not a collection type: %T", e.resourceAddressOne, pathStr, t) - - return - } - - var results []any - - results, err = walkCollectionPath(resultOne, e.collectionPath[1:], results) - - if err != nil { - resp.Error = err - - return - } - - if e.resourceAddressTwo == req.Query.Address { - resourceTwo = req.Query - } - - if resourceTwo == nil { - resp.Error = fmt.Errorf("%s - Resource not found in query", e.resourceAddressTwo) - - return - } - - resultTwo, err := tfjsonpath.Traverse(resourceTwo.ResourceObject, e.attributePath) - - if err != nil { - resp.Error = err - - return - } - - var errs []error - - for _, v := range results { - switch resultTwo.(type) { - case []any: - errs = append(errs, e.comparer.CompareValues([]any{v}, resultTwo)) - default: - errs = append(errs, e.comparer.CompareValues(v, resultTwo)) - } - } - - for _, err = range errs { - if err == nil { - return - } - } - - errMsgs := map[string]struct{}{} - - for _, err = range errs { - if _, ok := errMsgs[err.Error()]; ok { - continue - } - - resp.Error = errors.Join(resp.Error, err) - - errMsgs[err.Error()] = struct{}{} - } -} - -// CompareValueCollection returns a query check that iterates over each element in a collection and compares the value of each element -// with the value of an attribute using the given value comparer. -func CompareValueCollection(resourceAddressOne string, collectionPath []tfjsonpath.Path, resourceAddressTwo string, attributePath tfjsonpath.Path, comparer compare.ValueComparer) QueryCheck { - return &compareValueCollection{ - resourceAddressOne: resourceAddressOne, - collectionPath: collectionPath, - resourceAddressTwo: resourceAddressTwo, - attributePath: attributePath, - comparer: comparer, - } -} diff --git a/querycheck/compare_value_collection_test.go b/querycheck/compare_value_collection_test.go deleted file mode 100644 index 3fdb8095f..000000000 --- a/querycheck/compare_value_collection_test.go +++ /dev/null @@ -1,1988 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck_test - -import ( - "regexp" - "testing" - - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-go/tftypes" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - - "github.com/hashicorp/terraform-plugin-testing/compare" - 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/internal/testing/testsdk/resource" - "github.com/hashicorp/terraform-plugin-testing/querycheck" - "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" - "github.com/hashicorp/terraform-plugin-testing/tfversion" -) - -func TestCompareValueCollection_CheckQuery_Bool_Error_NotCollection(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - bool_attribute = true - } - - resource "test_resource" "two" { - bool_attribute = true - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("bool_attribute"), - }, - "test_resource.one", - tfjsonpath.New("bool_attribute"), - compare.ValuesSame(), - ), - }, - ExpectError: regexp.MustCompile("test_resource.two.bool_attribute is not a collection type: bool"), - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_Float_Error_NotCollection(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - float_attribute = 1.234 - } - - resource "test_resource" "two" { - float_attribute = 1.234 - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("float_attribute"), - }, - "test_resource.one", - tfjsonpath.New("float_attribute"), - compare.ValuesSame(), - ), - }, - ExpectError: regexp.MustCompile("test_resource.two.float_attribute is not a collection type: json.Number"), - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_Int_Error_NotCollection(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - int_attribute = 1234 - } - - resource "test_resource" "two" { - int_attribute = 1234 - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("int_attribute"), - }, - "test_resource.one", - tfjsonpath.New("int_attribute"), - compare.ValuesSame(), - ), - }, - ExpectError: regexp.MustCompile("test_resource.two.int_attribute is not a collection type: json.Number"), - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_List_ValuesSame_ErrorDiffer(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - string_attribute = "str" - } - - resource "test_resource" "two" { - list_attribute = [ - "str2", - "str3", - ] - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("list_attribute"), - }, - "test_resource.one", - tfjsonpath.New("string_attribute"), - compare.ValuesSame(), - ), - }, - ExpectError: regexp.MustCompile("expected values to be the same, but they differ: str2 != str\nexpected values to be the same, but they differ: str3 != str"), - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_EmptyCollectionPath(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - string_attribute = "str" - } - - resource "test_resource" "two" { - list_attribute = [ - "str2", - "str", - ] - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - // Empty path is invalid - []tfjsonpath.Path{}, - "test_resource.one", - tfjsonpath.New("string_attribute"), - compare.ValuesSame(), - ), - }, - ExpectError: regexp.MustCompile("test_resource.two - No collection path was provided"), - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_List_ValuesSame(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - string_attribute = "str" - } - - resource "test_resource" "two" { - list_attribute = [ - "str2", - "str", - ] - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("list_attribute"), - }, - "test_resource.one", - tfjsonpath.New("string_attribute"), - compare.ValuesSame(), - ), - }, - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_List_ValuesDiffer_ErrorSame(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - string_attribute = "str" - } - - resource "test_resource" "two" { - list_attribute = [ - "str", - "str", - ] - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("list_attribute"), - }, - "test_resource.one", - tfjsonpath.New("string_attribute"), - compare.ValuesDiffer(), - ), - }, - ExpectError: regexp.MustCompile("expected values to differ, but they are the same: str == str"), - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_List_ValuesDiffer(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - string_attribute = "str" - } - - resource "test_resource" "two" { - list_attribute = [ - "str", - "str2", - ] - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("list_attribute"), - }, - "test_resource.one", - tfjsonpath.New("string_attribute"), - compare.ValuesDiffer(), - ), - }, - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_ListNestedBlock_ValuesSame_ErrorDiffer(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - string_attribute = "str" - } - - resource "test_resource" "two" { - list_nested_block { - list_nested_block_attribute = "str2" - } - list_nested_block { - list_nested_block_attribute = "str3" - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("list_nested_block"), - tfjsonpath.New("list_nested_block_attribute"), - }, - "test_resource.one", - tfjsonpath.New("string_attribute"), - compare.ValuesSame(), - ), - }, - ExpectError: regexp.MustCompile("expected values to be the same, but they differ: str2 != str\nexpected values to be the same, but they differ: str3 != str"), - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_ListNestedBlock_ValuesSame(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - string_attribute = "str" - } - - resource "test_resource" "two" { - list_nested_block { - list_nested_block_attribute = "str2" - } - list_nested_block { - list_nested_block_attribute = "str" - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("list_nested_block"), - tfjsonpath.New("list_nested_block_attribute"), - }, - "test_resource.one", - tfjsonpath.New("string_attribute"), - compare.ValuesSame(), - ), - }, - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_ListNestedBlock_ValuesDiffer_ErrorSame(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - string_attribute = "str" - } - - resource "test_resource" "two" { - list_nested_block { - list_nested_block_attribute = "str" - } - list_nested_block { - list_nested_block_attribute = "str" - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("list_nested_block"), - tfjsonpath.New("list_nested_block_attribute"), - }, - "test_resource.one", - tfjsonpath.New("string_attribute"), - compare.ValuesDiffer(), - ), - }, - ExpectError: regexp.MustCompile("expected values to differ, but they are the same: str == str"), - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_ListNestedBlock_ValuesDiffer(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - string_attribute = "str" - } - - resource "test_resource" "two" { - list_nested_block { - list_nested_block_attribute = "str2" - } - list_nested_block { - list_nested_block_attribute = "str3" - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("list_nested_block"), - tfjsonpath.New("list_nested_block_attribute"), - }, - "test_resource.one", - tfjsonpath.New("string_attribute"), - compare.ValuesDiffer(), - ), - }, - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_Map_ValuesSame_ErrorDiffer(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - string_attribute = "str" - } - - resource "test_resource" "two" { - map_attribute = { - "a": "str2", - "b": "str3", - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("map_attribute"), - }, - "test_resource.one", - tfjsonpath.New("string_attribute"), - compare.ValuesSame(), - ), - }, - ExpectError: regexp.MustCompile("expected values to be the same, but they differ: str2 != str\nexpected values to be the same, but they differ: str3 != str"), - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_Map_ValuesSame(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - string_attribute = "str" - } - - resource "test_resource" "two" { - map_attribute = { - "a": "str2", - "b": "str", - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("map_attribute"), - }, - "test_resource.one", - tfjsonpath.New("string_attribute"), - compare.ValuesSame(), - ), - }, - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_Map_ValuesDiffer_ErrorSame(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - string_attribute = "str" - } - - resource "test_resource" "two" { - map_attribute = { - "a": "str", - "b": "str", - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("map_attribute"), - }, - "test_resource.one", - tfjsonpath.New("string_attribute"), - compare.ValuesDiffer(), - ), - }, - ExpectError: regexp.MustCompile("expected values to differ, but they are the same: str == str"), - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_Map_ValuesDiffer(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - string_attribute = "str" - } - - resource "test_resource" "two" { - map_attribute = { - "a": "str", - "b": "str2", - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("map_attribute"), - }, - "test_resource.one", - tfjsonpath.New("string_attribute"), - compare.ValuesDiffer(), - ), - }, - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_Set_ValuesSame_ErrorDiffer(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - string_attribute = "str" - } - - resource "test_resource" "two" { - set_attribute = [ - "str2", - "str3" - ] - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("set_attribute"), - }, - "test_resource.one", - tfjsonpath.New("string_attribute"), - compare.ValuesSame(), - ), - }, - ExpectError: regexp.MustCompile("expected values to be the same, but they differ: str2 != str\nexpected values to be the same, but they differ: str3 != str"), - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_Set_ValuesSame(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - string_attribute = "str" - } - - resource "test_resource" "two" { - set_attribute = [ - "str2", - "str" - ] - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("set_attribute"), - }, - "test_resource.one", - tfjsonpath.New("string_attribute"), - compare.ValuesSame(), - ), - }, - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_Set_ValuesDiffer_ErrorSame(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - string_attribute = "str" - } - - resource "test_resource" "two" { - set_attribute = [ - "str", - "str", - ] - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("set_attribute"), - }, - "test_resource.one", - tfjsonpath.New("string_attribute"), - compare.ValuesDiffer(), - ), - }, - ExpectError: regexp.MustCompile("expected values to differ, but they are the same: str == str"), - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_Set_ValuesDiffer(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - string_attribute = "str" - } - - resource "test_resource" "two" { - set_attribute = [ - "str", - "str2" - ] - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("set_attribute"), - }, - "test_resource.one", - tfjsonpath.New("string_attribute"), - compare.ValuesDiffer(), - ), - }, - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_SetNestedBlock_ValuesSame_ErrorDiffer(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - string_attribute = "str" - } - - resource "test_resource" "two" { - set_nested_block { - set_nested_block_attribute = "str2" - } - set_nested_block { - set_nested_block_attribute = "str3" - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("set_nested_block"), - tfjsonpath.New("set_nested_block_attribute"), - }, - "test_resource.one", - tfjsonpath.New("string_attribute"), - compare.ValuesSame(), - ), - }, - ExpectError: regexp.MustCompile("expected values to be the same, but they differ: str2 != str\nexpected values to be the same, but they differ: str3 != str"), - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_SetNestedBlock_ValuesSame(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - string_attribute = "str" - } - - resource "test_resource" "two" { - set_nested_block { - set_nested_block_attribute = "str2" - } - set_nested_block { - set_nested_block_attribute = "str" - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("set_nested_block"), - tfjsonpath.New("set_nested_block_attribute"), - }, - "test_resource.one", - tfjsonpath.New("string_attribute"), - compare.ValuesSame(), - ), - }, - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_SetNestedBlock_ValuesDiffer_ErrorSame(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - string_attribute = "str" - } - - resource "test_resource" "two" { - set_nested_block { - set_nested_block_attribute = "str" - } - set_nested_block { - set_nested_block_attribute = "str" - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("set_nested_block"), - tfjsonpath.New("set_nested_block_attribute"), - }, - "test_resource.one", - tfjsonpath.New("string_attribute"), - compare.ValuesDiffer(), - ), - }, - ExpectError: regexp.MustCompile("expected values to differ, but they are the same: str == str"), - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_SetNestedBlock_ValuesDiffer(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - string_attribute = "str" - } - - resource "test_resource" "two" { - set_nested_block { - set_nested_block_attribute = "str2" - } - set_nested_block { - set_nested_block_attribute = "str3" - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("set_nested_block"), - tfjsonpath.New("set_nested_block_attribute"), - }, - "test_resource.one", - tfjsonpath.New("string_attribute"), - compare.ValuesDiffer(), - ), - }, - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_SetNestedNestedBlock_ValuesDiffer_ErrorSameAttribute(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_nested_nested_block { - set_nested_block { - set_nested_block_attribute = "str" - } - } - } - - resource "test_resource" "two" { - set_nested_nested_block { - set_nested_block { - set_nested_block_attribute = "str" - } - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("set_nested_nested_block"), - tfjsonpath.New("set_nested_block"), - tfjsonpath.New("set_nested_block_attribute"), - }, - "test_resource.one", - tfjsonpath.New("set_nested_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block_attribute"), - compare.ValuesDiffer(), - ), - }, - ExpectError: regexp.MustCompile("expected values to differ, but they are the same: str == str"), - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_SetNestedNestedBlock_ValuesDiffer_ErrorSameNestedBlock(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_nested_nested_block { - set_nested_block { - set_nested_block_attribute = "str" - } - } - } - - resource "test_resource" "two" { - set_nested_nested_block { - set_nested_block { - set_nested_block_attribute = "str" - } - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("set_nested_nested_block"), - tfjsonpath.New("set_nested_block"), - }, - "test_resource.one", - tfjsonpath.New("set_nested_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block").AtSliceIndex(0), - compare.ValuesDiffer(), - ), - }, - ExpectError: regexp.MustCompile(`expected values to differ, but they are the same: map\[set_nested_block_attribute:str\] == map\[set_nested_block_attribute:str\]`), - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_SetNestedNestedBlock_ValuesDiffer_ErrorSameNestedNestedBlock(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_nested_nested_block { - set_nested_block { - set_nested_block_attribute = "str" - } - } - } - - resource "test_resource" "two" { - set_nested_nested_block { - set_nested_block { - set_nested_block_attribute = "str" - } - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("set_nested_nested_block"), - }, - "test_resource.one", - tfjsonpath.New("set_nested_nested_block"), - compare.ValuesDiffer(), - ), - }, - ExpectError: regexp.MustCompile(`expected values to differ, but they are the same: \[map\[set_nested_block:\[map\[set_nested_block_attribute:str\]\]\]\] == \[map\[set_nested_block:\[map\[set_nested_block_attribute:str\]\]\]\]`), - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_SetNestedNestedBlock_ValuesDifferAttribute(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_nested_nested_block { - set_nested_block { - set_nested_block_attribute = "str1" - } - set_nested_block { - set_nested_block_attribute = "str2" - } - } - } - - resource "test_resource" "two" { - set_nested_nested_block { - set_nested_block { - set_nested_block_attribute = "str3" - } - set_nested_block { - set_nested_block_attribute = "str4" - } - } - set_nested_nested_block { - set_nested_block { - set_nested_block_attribute = "str5" - } - set_nested_block { - set_nested_block_attribute = "str6" - } - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("set_nested_nested_block"), - tfjsonpath.New("set_nested_block"), - tfjsonpath.New("set_nested_block_attribute"), - }, - "test_resource.one", - tfjsonpath.New("set_nested_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block_attribute"), - compare.ValuesDiffer(), - ), - }, - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_SetNestedNestedBlock_ValuesDifferNestedBlock(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_nested_nested_block { - set_nested_block { - set_nested_block_attribute = "str1" - } - set_nested_block { - set_nested_block_attribute = "str2" - } - } - } - - resource "test_resource" "two" { - set_nested_nested_block { - set_nested_block { - set_nested_block_attribute = "str3" - } - set_nested_block { - set_nested_block_attribute = "str4" - } - } - set_nested_nested_block { - set_nested_block { - set_nested_block_attribute = "str5" - } - set_nested_block { - set_nested_block_attribute = "str6" - } - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("set_nested_nested_block"), - tfjsonpath.New("set_nested_block"), - }, - "test_resource.one", - tfjsonpath.New("set_nested_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block").AtSliceIndex(0), - compare.ValuesDiffer(), - ), - }, - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_SetNestedNestedBlock_ValuesDifferNestedNestedBlock(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_nested_nested_block { - set_nested_block { - set_nested_block_attribute = "str1" - } - set_nested_block { - set_nested_block_attribute = "str2" - } - } - } - - resource "test_resource" "two" { - set_nested_nested_block { - set_nested_block { - set_nested_block_attribute = "str3" - } - set_nested_block { - set_nested_block_attribute = "str4" - } - } - set_nested_nested_block { - set_nested_block { - set_nested_block_attribute = "str5" - } - set_nested_block { - set_nested_block_attribute = "str6" - } - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("set_nested_nested_block"), - }, - "test_resource.one", - tfjsonpath.New("set_nested_nested_block"), - compare.ValuesDiffer(), - ), - }, - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_SetNested_ValuesSame_ErrorAttribute(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_nested_nested_block { - set_nested_block { - set_nested_block_attribute = "str_a" - } - set_nested_block { - set_nested_block_attribute = "str_b" - } - } - } - - resource "test_resource" "two" { - set_nested_nested_block { - set_nested_block { - set_nested_block_attribute = "str_c" - } - set_nested_block { - set_nested_block_attribute = "str_d" - } - } - set_nested_nested_block { - set_nested_block { - set_nested_block_attribute = "str_e" - } - set_nested_block { - set_nested_block_attribute = "str_f" - } - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("set_nested_nested_block"), - tfjsonpath.New("set_nested_block"), - tfjsonpath.New("set_nested_block_attribute"), - }, - "test_resource.one", - tfjsonpath.New("set_nested_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block_attribute"), - compare.ValuesSame(), - ), - }, - ExpectError: regexp.MustCompile("expected values to be the same, but they differ: str_c != str_a\nexpected values to be the same, but they differ: str_d != str_a\nexpected values to be the same, but they differ: str_e != str_a\nexpected values to be the same, but they differ: str_f != str_a"), - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_SetNested_ValuesSame_ErrorNestedBlock(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_nested_nested_block { - set_nested_block { - set_nested_block_attribute = "str_a" - } - set_nested_block { - set_nested_block_attribute = "str_b" - } - } - } - - resource "test_resource" "two" { - set_nested_nested_block { - set_nested_block { - set_nested_block_attribute = "str_c" - } - set_nested_block { - set_nested_block_attribute = "str_d" - } - } - set_nested_nested_block { - set_nested_block { - set_nested_block_attribute = "str_e" - } - set_nested_block { - set_nested_block_attribute = "str_f" - } - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("set_nested_nested_block"), - tfjsonpath.New("set_nested_block"), - }, - "test_resource.one", - tfjsonpath.New("set_nested_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block").AtSliceIndex(0), - compare.ValuesSame(), - ), - }, - ExpectError: regexp.MustCompile(`expected values to be the same, but they differ: map\[set_nested_block_attribute:str_c\] != map\[set_nested_block_attribute:str_a\]\nexpected values to be the same, but they differ: map\[set_nested_block_attribute:str_d\] != map\[set_nested_block_attribute:str_a\]\nexpected values to be the same, but they differ: map\[set_nested_block_attribute:str_e\] != map\[set_nested_block_attribute:str_a\]\nexpected values to be the same, but they differ: map\[set_nested_block_attribute:str_f\] != map\[set_nested_block_attribute:str_a\]`), - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_SetNested_ValuesSame_ErrorNestedNestedBlock(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_nested_nested_block { - set_nested_block { - set_nested_block_attribute = "str_a" - } - set_nested_block { - set_nested_block_attribute = "str_b" - } - } - } - - resource "test_resource" "two" { - set_nested_nested_block { - set_nested_block { - set_nested_block_attribute = "str_c" - } - set_nested_block { - set_nested_block_attribute = "str_d" - } - } - set_nested_nested_block { - set_nested_block { - set_nested_block_attribute = "str_e" - } - set_nested_block { - set_nested_block_attribute = "str_f" - } - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("set_nested_nested_block"), - }, - "test_resource.one", - tfjsonpath.New("set_nested_nested_block"), - compare.ValuesSame(), - ), - }, - ExpectError: regexp.MustCompile(`expected values to be the same, but they differ: \[map\[set_nested_block:\[map\[set_nested_block_attribute:str_c\] map\[set_nested_block_attribute:str_d\]\]\]\] != \[map\[set_nested_block:\[map\[set_nested_block_attribute:str_a\] map\[set_nested_block_attribute:str_b\]\]\]\]\nexpected values to be the same, but they differ: \[map\[set_nested_block:\[map\[set_nested_block_attribute:str_e\] map\[set_nested_block_attribute:str_f\]\]\]\] != \[map\[set_nested_block:\[map\[set_nested_block_attribute:str_a\] map\[set_nested_block_attribute:str_b\]\]\]\]`), - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_SetNested_ValuesSameAttribute(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_nested_nested_block { - set_nested_block { - set_nested_block_attribute = "str_a" - } - set_nested_block { - set_nested_block_attribute = "str_b" - } - } - } - - resource "test_resource" "two" { - set_nested_nested_block { - set_nested_block { - set_nested_block_attribute = "str_c" - } - set_nested_block { - set_nested_block_attribute = "str_d" - } - } - set_nested_nested_block { - set_nested_block { - set_nested_block_attribute = "str_a" - } - set_nested_block { - set_nested_block_attribute = "str_b" - } - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("set_nested_nested_block"), - tfjsonpath.New("set_nested_block"), - tfjsonpath.New("set_nested_block_attribute"), - }, - "test_resource.one", - tfjsonpath.New("set_nested_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block_attribute"), - compare.ValuesSame(), - ), - }, - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_SetNested_ValuesSameNestedBlock(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_nested_nested_block { - set_nested_block { - set_nested_block_attribute = "str_a" - } - set_nested_block { - set_nested_block_attribute = "str_b" - } - } - } - - resource "test_resource" "two" { - set_nested_nested_block { - set_nested_block { - set_nested_block_attribute = "str_c" - } - set_nested_block { - set_nested_block_attribute = "str_d" - } - } - set_nested_nested_block { - set_nested_block { - set_nested_block_attribute = "str_a" - } - set_nested_block { - set_nested_block_attribute = "str_b" - } - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("set_nested_nested_block"), - tfjsonpath.New("set_nested_block"), - }, - "test_resource.one", - tfjsonpath.New("set_nested_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block").AtSliceIndex(0), - compare.ValuesSame(), - ), - }, - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_SetNested_ValuesSameNestedNestedBlock(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_nested_nested_block { - set_nested_block { - set_nested_block_attribute = "str_a" - } - set_nested_block { - set_nested_block_attribute = "str_b" - } - } - } - - resource "test_resource" "two" { - set_nested_nested_block { - set_nested_block { - set_nested_block_attribute = "str_c" - } - set_nested_block { - set_nested_block_attribute = "str_d" - } - } - set_nested_nested_block { - set_nested_block { - set_nested_block_attribute = "str_a" - } - set_nested_block { - set_nested_block_attribute = "str_b" - } - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("set_nested_nested_block"), - }, - "test_resource.one", - tfjsonpath.New("set_nested_nested_block"), - compare.ValuesSame(), - ), - }, - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_String_Error_NotCollection(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - string_attribute = "str" - } - - resource "test_resource" "two" { - string_attribute = "str" - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("string_attribute"), - }, - "test_resource.one", - tfjsonpath.New("string_attribute"), - compare.ValuesSame(), - ), - }, - ExpectError: regexp.MustCompile("test_resource.two.string_attribute is not a collection type: string"), - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_ListNestedAttribute_ValuesSame(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_0_0), // Nested attributes only available in protocol version 6 - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "test": providerserver.NewProviderServer(testprovider.Provider{ - Resources: map[string]testprovider.Resource{ - "test_resource": { - SchemaResponse: &resource.SchemaResponse{ - Schema: &tfprotov6.Schema{ - Block: &tfprotov6.SchemaBlock{ - Attributes: []*tfprotov6.SchemaAttribute{ - { - Name: "str_attr", - Type: tftypes.String, - Optional: true, - }, - { - Name: "nested_attr", - NestedType: &tfprotov6.SchemaObject{ - Attributes: []*tfprotov6.SchemaAttribute{ - { - Name: "str_attr", - Type: tftypes.String, - Optional: true, - }, - }, - Nesting: tfprotov6.SchemaObjectNestingModeList, - }, - Optional: true, - }, - }, - }, - }, - }, - }, - }, - }), - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - str_attr = "str2" - } - resource "test_resource" "two" { - nested_attr = [ - { - str_attr = "str1" - }, - { - str_attr = "str2" - } - ] - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("nested_attr"), - tfjsonpath.New("str_attr"), - }, - "test_resource.one", - tfjsonpath.New("str_attr"), - compare.ValuesSame(), - ), - }, - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_ListNestedAttribute_ValuesSame_ErrorDiff(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_0_0), // Nested attributes only available in protocol version 6 - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "test": providerserver.NewProviderServer(testprovider.Provider{ - Resources: map[string]testprovider.Resource{ - "test_resource": { - SchemaResponse: &resource.SchemaResponse{ - Schema: &tfprotov6.Schema{ - Block: &tfprotov6.SchemaBlock{ - Attributes: []*tfprotov6.SchemaAttribute{ - { - Name: "str_attr", - Type: tftypes.String, - Optional: true, - }, - { - Name: "nested_attr", - NestedType: &tfprotov6.SchemaObject{ - Attributes: []*tfprotov6.SchemaAttribute{ - { - Name: "str_attr", - Type: tftypes.String, - Optional: true, - }, - }, - Nesting: tfprotov6.SchemaObjectNestingModeList, - }, - Optional: true, - }, - }, - }, - }, - }, - }, - }, - }), - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - str_attr = "str1" - } - resource "test_resource" "two" { - nested_attr = [ - { - str_attr = "str2" - }, - { - str_attr = "str3" - } - ] - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("nested_attr"), - tfjsonpath.New("str_attr"), - }, - "test_resource.one", - tfjsonpath.New("str_attr"), - compare.ValuesSame(), - ), - }, - ExpectError: regexp.MustCompile("expected values to be the same, but they differ: str2 != str1\nexpected values to be the same, but they differ: str3 != str1"), - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_DoubleListNestedAttribute_ValuesSame(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_0_0), // Nested attributes only available in protocol version 6 - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "test": providerserver.NewProviderServer(testprovider.Provider{ - Resources: map[string]testprovider.Resource{ - "test_resource": { - SchemaResponse: &resource.SchemaResponse{ - Schema: &tfprotov6.Schema{ - Block: &tfprotov6.SchemaBlock{ - Attributes: []*tfprotov6.SchemaAttribute{ - { - Name: "str_attr", - Type: tftypes.String, - Optional: true, - }, - { - Name: "nested_attr", - NestedType: &tfprotov6.SchemaObject{ - Attributes: []*tfprotov6.SchemaAttribute{ - { - Name: "double_nested_attr", - NestedType: &tfprotov6.SchemaObject{ - Attributes: []*tfprotov6.SchemaAttribute{ - { - Name: "str_attr", - Type: tftypes.String, - Optional: true, - }, - }, - Nesting: tfprotov6.SchemaObjectNestingModeSingle, - }, - Optional: true, - }, - }, - Nesting: tfprotov6.SchemaObjectNestingModeList, - }, - Optional: true, - }, - }, - }, - }, - }, - }, - }, - }), - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - str_attr = "str2" - } - resource "test_resource" "two" { - nested_attr = [ - { - double_nested_attr = { - str_attr = "str1" - } - }, - { - double_nested_attr = { - str_attr = "str2" - } - } - ] - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("nested_attr"), - tfjsonpath.New("double_nested_attr"), - tfjsonpath.New("str_attr"), - }, - "test_resource.one", - tfjsonpath.New("str_attr"), - compare.ValuesSame(), - ), - }, - }, - }, - }) -} - -func TestCompareValueCollection_CheckQuery_DoubleListNestedAttribute_ValuesSame_ErrorDiff(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_0_0), // Nested attributes only available in protocol version 6 - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "test": providerserver.NewProviderServer(testprovider.Provider{ - Resources: map[string]testprovider.Resource{ - "test_resource": { - SchemaResponse: &resource.SchemaResponse{ - Schema: &tfprotov6.Schema{ - Block: &tfprotov6.SchemaBlock{ - Attributes: []*tfprotov6.SchemaAttribute{ - { - Name: "str_attr", - Type: tftypes.String, - Optional: true, - }, - { - Name: "nested_attr", - NestedType: &tfprotov6.SchemaObject{ - Attributes: []*tfprotov6.SchemaAttribute{ - { - Name: "double_nested_attr", - NestedType: &tfprotov6.SchemaObject{ - Attributes: []*tfprotov6.SchemaAttribute{ - { - Name: "str_attr", - Type: tftypes.String, - Optional: true, - }, - }, - Nesting: tfprotov6.SchemaObjectNestingModeSingle, - }, - Optional: true, - }, - }, - Nesting: tfprotov6.SchemaObjectNestingModeList, - }, - Optional: true, - }, - }, - }, - }, - }, - }, - }, - }), - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - str_attr = "str1" - } - resource "test_resource" "two" { - nested_attr = [ - { - double_nested_attr = { - str_attr = "str2" - } - }, - { - double_nested_attr = { - str_attr = "str3" - } - } - ] - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValueCollection( - "test_resource.two", - []tfjsonpath.Path{ - tfjsonpath.New("nested_attr"), - tfjsonpath.New("double_nested_attr"), - tfjsonpath.New("str_attr"), - }, - "test_resource.one", - tfjsonpath.New("str_attr"), - compare.ValuesSame(), - ), - }, - ExpectError: regexp.MustCompile("expected values to be the same, but they differ: str2 != str1\nexpected values to be the same, but they differ: str3 != str1"), - }, - }, - }) -} diff --git a/querycheck/compare_value_pairs.go b/querycheck/compare_value_pairs.go deleted file mode 100644 index 908b01749..000000000 --- a/querycheck/compare_value_pairs.go +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -import ( - "context" - "fmt" - "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" - - "github.com/hashicorp/terraform-plugin-testing/compare" - "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" -) - -// Resource Query Check -var _ QueryCheck = &compareValuePairs{} - -type compareValuePairs struct { - resourceAddressOne string - attributePathOne tfjsonpath.Path - resourceAddressTwo string - attributePathTwo tfjsonpath.Path - comparer compare.ValueComparer -} - -// CheckQuery implements the query check logic. -func (e *compareValuePairs) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { - var resourceOne *plugintest.QueryResult - var resourceTwo *plugintest.QueryResult - - if req.Query == nil { - resp.Error = fmt.Errorf("query is nil") - - return - } - - if e.resourceAddressOne == req.Query.Address { - resourceOne = req.Query - } - - if resourceOne == nil { - resp.Error = fmt.Errorf("%s - Resource not found in query", e.resourceAddressOne) - - return - } - - resultOne, err := tfjsonpath.Traverse(resourceOne.ResourceObject, e.attributePathOne) - - if err != nil { - resp.Error = err - - return - } - - if e.resourceAddressTwo == req.Query.Address { - resourceTwo = req.Query - } - - if resourceTwo == nil { - resp.Error = fmt.Errorf("%s - Resource not found in query", e.resourceAddressTwo) - - return - } - - resultTwo, err := tfjsonpath.Traverse(resourceTwo.ResourceObject, e.attributePathTwo) - - if err != nil { - resp.Error = err - - return - } - - err = e.comparer.CompareValues(resultOne, resultTwo) - - if err != nil { - resp.Error = err - } -} - -// CompareValuePairs returns a query check that compares the value in query for the first given resource address and -// path with the value in query for the second given resource address and path using the supplied value comparer. -func CompareValuePairs(resourceAddressOne string, attributePathOne tfjsonpath.Path, resourceAddressTwo string, attributePathTwo tfjsonpath.Path, comparer compare.ValueComparer) QueryCheck { - return &compareValuePairs{ - resourceAddressOne: resourceAddressOne, - attributePathOne: attributePathOne, - resourceAddressTwo: resourceAddressTwo, - attributePathTwo: attributePathTwo, - comparer: comparer, - } -} diff --git a/querycheck/compare_value_pairs_test.go b/querycheck/compare_value_pairs_test.go deleted file mode 100644 index e65c4420f..000000000 --- a/querycheck/compare_value_pairs_test.go +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck_test - -import ( - "regexp" - "testing" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - - "github.com/hashicorp/terraform-plugin-testing/compare" - r "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/querycheck" - "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" -) - -func TestCompareValuePairs_CheckQuery_ValuesSame_DifferError(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - bool_attribute = true - float_attribute = 1.234 - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValuePairs( - "test_resource.one", - tfjsonpath.New("bool_attribute"), - "test_resource.one", - tfjsonpath.New("float_attribute"), - compare.ValuesSame(), - ), - }, - ExpectError: regexp.MustCompile("expected values to be the same, but they differ: true != 1.234"), - }, - }, - }) -} - -func TestCompareValuePairs_CheckQuery_ValuesSame(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - bool_attribute = true - } - - resource "test_resource" "two" { - bool_attribute = true - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValuePairs( - "test_resource.one", - tfjsonpath.New("bool_attribute"), - "test_resource.two", - tfjsonpath.New("bool_attribute"), - compare.ValuesSame(), - ), - }, - }, - }, - }) -} - -func TestCompareValuePairs_CheckQuery_ValuesDiffer_SameError(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - bool_attribute = true - } - - resource "test_resource" "two" { - bool_attribute = true - }`, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValuePairs( - "test_resource.one", - tfjsonpath.New("bool_attribute"), - "test_resource.two", - tfjsonpath.New("bool_attribute"), - compare.ValuesDiffer(), - ), - }, - ExpectError: regexp.MustCompile("expected values to differ, but they are the same: true == true"), - }, - }, - }) -} - -func TestCompareValuePairs_CheckQuery_ValuesDiffer(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - bool_attribute = true - float_attribute = 1.234 - }`, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.CompareValuePairs( - "test_resource.one", - tfjsonpath.New("bool_attribute"), - "test_resource.one", - tfjsonpath.New("float_attribute"), - compare.ValuesDiffer(), - ), - }, - }, - }, - }) -} diff --git a/querycheck/compare_value_test.go b/querycheck/compare_value_test.go deleted file mode 100644 index 06e3810ed..000000000 --- a/querycheck/compare_value_test.go +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck_test - -import ( - "regexp" - "testing" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - - "github.com/hashicorp/terraform-plugin-testing/compare" - r "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/querycheck" - "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" -) - -func TestCompareValue_CheckQuery_NoQueryValues(t *testing.T) { - t.Parallel() - - boolValuesDiffer := querycheck.CompareValue(compare.ValuesSame()) - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - bool_attribute = true - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - // No query values have been added - boolValuesDiffer, - }, - ExpectError: regexp.MustCompile(`resource addresses index out of bounds: 0`), - }, - }, - }) -} - -func TestCompareValue_CheckQuery_ValuesSame_ValueDiffersError(t *testing.T) { - t.Parallel() - - boolValuesDiffer := querycheck.CompareValue(compare.ValuesSame()) - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - bool_attribute = true - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - boolValuesDiffer.AddQueryValue( - "test_resource.one", - tfjsonpath.New("bool_attribute"), - ), - }, - }, - { - Config: `resource "test_resource" "one" { - bool_attribute = true - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - boolValuesDiffer.AddQueryValue( - "test_resource.one", - tfjsonpath.New("bool_attribute"), - ), - }, - }, - { - Config: `resource "test_resource" "one" { - bool_attribute = false - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - boolValuesDiffer.AddQueryValue( - "test_resource.one", - tfjsonpath.New("bool_attribute"), - ), - }, - ExpectError: regexp.MustCompile(`expected values to be the same, but they differ: true != false`), - }, - }, - }) -} - -func TestCompareValue_CheckQuery_ValuesSame(t *testing.T) { - t.Parallel() - - boolValuesDiffer := querycheck.CompareValue(compare.ValuesSame()) - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - bool_attribute = true - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - boolValuesDiffer.AddQueryValue( - "test_resource.one", - tfjsonpath.New("bool_attribute"), - ), - }, - }, - { - Config: `resource "test_resource" "one" { - bool_attribute = true - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - boolValuesDiffer.AddQueryValue( - "test_resource.one", - tfjsonpath.New("bool_attribute"), - ), - }, - }, - }, - }) -} - -func TestCompareValue_CheckQuery_ValuesDiffer(t *testing.T) { - t.Parallel() - - boolValuesDiffer := querycheck.CompareValue(compare.ValuesDiffer()) - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - bool_attribute = true - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - boolValuesDiffer.AddQueryValue( - "test_resource.one", - tfjsonpath.New("bool_attribute"), - ), - }, - }, - { - Config: `resource "test_resource" "one" { - bool_attribute = false - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - boolValuesDiffer.AddQueryValue( - "test_resource.one", - tfjsonpath.New("bool_attribute"), - ), - }, - }, - { - Config: `resource "test_resource" "one" { - bool_attribute = true - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - boolValuesDiffer.AddQueryValue( - "test_resource.one", - tfjsonpath.New("bool_attribute"), - ), - }, - }, - }, - }) -} diff --git a/querycheck/expect_identity.go b/querycheck/expect_identity.go deleted file mode 100644 index 453964e3e..000000000 --- a/querycheck/expect_identity.go +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -import ( - "context" - "fmt" - "maps" - "slices" - "sort" - - "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" - - "github.com/hashicorp/terraform-plugin-testing/knownvalue" -) - -var _ QueryCheck = expectIdentity{} - -type expectIdentity struct { - resourceAddress string - identity map[string]knownvalue.Check -} - -// CheckQuery implements the query check logic. -func (e expectIdentity) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { - var resource *plugintest.QueryResult - - if req.Query == nil { - resp.Error = fmt.Errorf("query is nil") - - return - } - - if e.resourceAddress == req.Query.Address { - resource = req.Query - } - - if resource == nil { - resp.Error = fmt.Errorf("%s - Resource not found in query", e.resourceAddress) - - return - } - - if resource.Identity == nil || len(resource.Identity) == 0 { - resp.Error = fmt.Errorf("%s - Identity not found in query. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.14+)", e.resourceAddress) - - return - } - - if len(resource.Identity) != len(e.identity) { - deltaMsg := "" - if len(resource.Identity) > len(e.identity) { - deltaMsg = createDeltaString(resource.Identity, e.identity, "actual identity has extra attribute(s): ") - } else { - deltaMsg = createDeltaString(e.identity, resource.Identity, "actual identity is missing attribute(s): ") - } - - resp.Error = fmt.Errorf("%s - Expected %d attribute(s) in the actual identity object, got %d attribute(s): %s", e.resourceAddress, len(e.identity), len(resource.Identity), deltaMsg) - return - } - - var keys []string - - for k := range e.identity { - keys = append(keys, k) - } - - sort.SliceStable(keys, func(i, j int) bool { - return keys[i] < keys[j] - }) - - for _, k := range keys { - actualIdentityVal, ok := resource.Identity[k] - - if !ok { - resp.Error = fmt.Errorf("%s - missing attribute %q in actual identity object", e.resourceAddress, k) - return - } - - if err := e.identity[k].CheckValue(actualIdentityVal); err != nil { - resp.Error = fmt.Errorf("%s - %q identity attribute: %s", e.resourceAddress, k, err) - return - } - } -} - -// ExpectIdentity returns a query check that asserts that the identity at the given resource matches a known object, where each -// map key represents an identity attribute name. The identity in query must exactly match the given object and any missing/extra -// attributes will raise a diagnostic. -// -// This query check can only be used with managed resources that support resource identity. Resource identity is only supported in Terraform v1.14+ -func ExpectIdentity(resourceAddress string, identity map[string]knownvalue.Check) QueryCheck { - return expectIdentity{ - resourceAddress: resourceAddress, - identity: identity, - } -} - -// createDeltaString prints the map keys that are present in mapA and not present in mapB -func createDeltaString[T any, V any](mapA map[string]T, mapB map[string]V, msgPrefix string) string { - deltaMsg := "" - - deltaMap := make(map[string]T, len(mapA)) - maps.Copy(deltaMap, mapA) - for key := range mapB { - delete(deltaMap, key) - } - - deltaKeys := slices.Sorted(maps.Keys(deltaMap)) - - for i, k := range deltaKeys { - if i == 0 { - deltaMsg += msgPrefix - } else { - deltaMsg += ", " - } - deltaMsg += fmt.Sprintf("%q", k) - } - - return deltaMsg -} diff --git a/querycheck/expect_identity_example_test.go b/querycheck/expect_identity_example_test.go deleted file mode 100644 index 642fe61bd..000000000 --- a/querycheck/expect_identity_example_test.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck_test - -import ( - "testing" - - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/knownvalue" - "github.com/hashicorp/terraform-plugin-testing/querycheck" - "github.com/hashicorp/terraform-plugin-testing/tfversion" -) - -func ExampleExpectIdentity() { - // A typical test would accept *testing.T as a function parameter, for instance `func TestSomething(t *testing.T) { ... }`. - t := &testing.T{} - t.Parallel() - - resource.Test(t, resource.TestCase{ - // Resource identity support is only available in Terraform v1.14+ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - // Provider definition omitted. Assuming "test_resource" has an identity schema with "id" and "name" string attributes - Steps: []resource.TestStep{ - { - Config: `resource "test_resource" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectIdentity( - "test_resource.one", - map[string]knownvalue.Check{ - "id": knownvalue.StringExact("id-123"), - "name": knownvalue.StringExact("John Doe"), - }, - ), - }, - }, - }, - }) -} diff --git a/querycheck/expect_identity_test.go b/querycheck/expect_identity_test.go deleted file mode 100644 index 18b73e3a1..000000000 --- a/querycheck/expect_identity_test.go +++ /dev/null @@ -1,257 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// 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/knownvalue" - "github.com/hashicorp/terraform-plugin-testing/querycheck" - "github.com/hashicorp/terraform-plugin-testing/tfversion" -) - -func TestExpectIdentity_CheckQuery_ResourceNotFound(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectIdentity( - "examplecloud_thing.two", - 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), - }, - ), - }, - ), - }, - ExpectError: regexp.MustCompile("examplecloud_thing.two - Resource not found in query"), - }, - }, - }) -} - -func TestExpectIdentity_CheckQuery(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectIdentity( - "examplecloud_thing.one", - 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), - }, - ), - }, - ), - }, - }, - }, - }) -} - -func TestExpectIdentity_CheckQuery_KnownValueWrongType(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - - querycheck.ExpectIdentity( - "examplecloud_thing.one", - map[string]knownvalue.Check{ - "id": knownvalue.Bool(true), - "list_of_numbers": knownvalue.ListExact( - []knownvalue.Check{ - knownvalue.Int64Exact(1), - knownvalue.Int64Exact(2), - knownvalue.Int64Exact(3), - knownvalue.Int64Exact(4), - }, - ), - }, - ), - }, - ExpectError: regexp.MustCompile(`examplecloud_thing.one - "id" identity attribute: expected bool value for Bool check, got: string`), - }, - }, - }) -} - -func TestExpectIdentity_CheckQuery_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - - querycheck.ExpectIdentity( - "examplecloud_thing.one", - map[string]knownvalue.Check{ - "id": knownvalue.StringExact("321-id"), - "list_of_numbers": knownvalue.ListExact( - []knownvalue.Check{ - knownvalue.Int64Exact(1), - knownvalue.Int64Exact(2), - knownvalue.Int64Exact(3), - knownvalue.Int64Exact(4), - }, - ), - }, - ), - }, - ExpectError: regexp.MustCompile(`examplecloud_thing.one - "id" identity attribute: expected value 321-id for StringExact check, got: id-123`), - }, - }, - }) -} - -func TestExpectIdentity_CheckQuery_ExtraAttribute(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - - querycheck.ExpectIdentity( - "examplecloud_thing.one", - map[string]knownvalue.Check{ - "id": knownvalue.StringExact("321-id"), - }, - ), - }, - ExpectError: regexp.MustCompile(`examplecloud_thing.one - Expected 1 attribute\(s\) in the actual identity object, got 2 attribute\(s\): actual identity has extra attribute\(s\): "list_of_numbers"`), - }, - }, - }) -} - -func TestExpectIdentity_CheckQuery_MissingAttribute(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - - querycheck.ExpectIdentity( - "examplecloud_thing.one", - 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), - }, - ), - }, - ), - }, - ExpectError: regexp.MustCompile(`examplecloud_thing.one - Expected 3 attribute\(s\) in the actual identity object, got 2 attribute\(s\): actual identity is missing attribute\(s\): "nonexistent_attr"`), - }, - }, - }) -} - -func TestExpectIdentity_CheckQuery_MismatchedAttribute(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectIdentity( - "examplecloud_thing.one", - map[string]knownvalue.Check{ - "not_id": knownvalue.StringExact("id-123"), - "list_of_numbers": knownvalue.ListExact( - []knownvalue.Check{ - knownvalue.Int64Exact(1), - knownvalue.Int64Exact(2), - knownvalue.Int64Exact(3), - knownvalue.Int64Exact(4), - }, - ), - }, - ), - }, - ExpectError: regexp.MustCompile(`examplecloud_thing.one - missing attribute "not_id" in actual identity object`), - }, - }, - }) -} diff --git a/querycheck/expect_identity_value.go b/querycheck/expect_identity_value.go deleted file mode 100644 index 0808255d3..000000000 --- a/querycheck/expect_identity_value.go +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" - - "github.com/hashicorp/terraform-plugin-testing/knownvalue" - "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" -) - -var _ QueryCheck = expectIdentityValue{} - -type expectIdentityValue struct { - resourceAddress string - attributePath tfjsonpath.Path - identityValue knownvalue.Check -} - -// CheckQuery implements the query check logic. -func (e expectIdentityValue) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { - var resource *plugintest.QueryResult - - if req.Query == nil { - resp.Error = fmt.Errorf("query is nil") - - return - } - - if len(req.Query.Address) == 0 { - resp.Error = fmt.Errorf("query does not contain any address values") - - return - } - - if e.resourceAddress == req.Query.Address { - resource = req.Query - } - - if resource == nil { - resp.Error = fmt.Errorf("%s - Resource not found in query", e.resourceAddress) - - return - } - - if resource.Identity == nil || len(resource.Identity) == 0 { - resp.Error = fmt.Errorf("%s - Identity not found in query. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.14+)", e.resourceAddress) - - return - } - - result, err := tfjsonpath.Traverse(resource.Identity, e.attributePath) - - if err != nil { - resp.Error = err - - return - } - - if err := e.identityValue.CheckValue(result); err != nil { - resp.Error = fmt.Errorf("error checking identity value for attribute at path: %s.%s, err: %s", e.resourceAddress, e.attributePath.String(), err) - - return - } -} - -// ExpectIdentityValue returns a query check that asserts that the specified identity attribute at the given resource -// matches a known value. This query check can only be used with managed resources that support resource identity. -// -// Resource identity is only supported in Terraform v1.14+ -func ExpectIdentityValue(resourceAddress string, attributePath tfjsonpath.Path, identityValue knownvalue.Check) QueryCheck { - return expectIdentityValue{ - resourceAddress: resourceAddress, - attributePath: attributePath, - identityValue: identityValue, - } -} diff --git a/querycheck/expect_identity_value_example_test.go b/querycheck/expect_identity_value_example_test.go deleted file mode 100644 index 4043e0a8d..000000000 --- a/querycheck/expect_identity_value_example_test.go +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck_test - -import ( - "testing" - - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/knownvalue" - "github.com/hashicorp/terraform-plugin-testing/querycheck" - "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" - "github.com/hashicorp/terraform-plugin-testing/tfversion" -) - -func ExampleExpectIdentityValue() { - // A typical test would accept *testing.T as a function parameter, for instance `func TestSomething(t *testing.T) { ... }`. - t := &testing.T{} - t.Parallel() - - resource.Test(t, resource.TestCase{ - // Resource identity support is only available in Terraform v1.14+ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - // Provider definition omitted. Assuming "test_resource" has an identity schema with an "id" string attribute - Steps: []resource.TestStep{ - { - Config: `resource "test_resource" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectIdentityValue( - "test_resource.one", - tfjsonpath.New("id"), - knownvalue.StringExact("id-123"), - ), - }, - }, - }, - }) -} diff --git a/querycheck/expect_identity_value_matches_query.go b/querycheck/expect_identity_value_matches_query.go deleted file mode 100644 index b4240c112..000000000 --- a/querycheck/expect_identity_value_matches_query.go +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -import ( - "context" - "fmt" - "reflect" - - "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" - - "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" -) - -var _ QueryCheck = expectIdentityValueMatchesQuery{} - -type expectIdentityValueMatchesQuery struct { - resourceAddress string - attributePath tfjsonpath.Path -} - -// CheckQuery implements the query check logic. -func (e expectIdentityValueMatchesQuery) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { - var resource *plugintest.QueryResult - - if req.Query == nil { - resp.Error = fmt.Errorf("query is nil") - - return - } - - if e.resourceAddress == req.Query.Address { - resource = req.Query - } - - if resource == nil { - resp.Error = fmt.Errorf("%s - Resource not found in query", e.resourceAddress) - - return - } - - if resource.Identity == nil || len(resource.Identity) == 0 { - resp.Error = fmt.Errorf("%s - Identity not found in query. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.14+)", e.resourceAddress) - - return - } - - identityResult, err := tfjsonpath.Traverse(resource.Identity, e.attributePath) - - if err != nil { - resp.Error = err - - return - } - - queryResult, err := tfjsonpath.Traverse(resource.ResourceObject, e.attributePath) - - if err != nil { - resp.Error = err - - return - } - - if !reflect.DeepEqual(identityResult, queryResult) { - resp.Error = fmt.Errorf("expected identity and query value at path to match, but they differ: %s.%s, identity value: %v, query value: %v", e.resourceAddress, e.attributePath.String(), identityResult, queryResult) - - return - } -} - -// ExpectIdentityValueMatchesQuery returns a query check that asserts that the specified identity attribute at the given resource -// matches the same attribute in query. This is useful when an identity attribute is in sync with a query attribute of the same path. -// -// This query check can only be used with managed resources that support resource identity. Resource identity is only supported in Terraform v1.14+ -func ExpectIdentityValueMatchesQuery(resourceAddress string, attributePath tfjsonpath.Path) QueryCheck { - return expectIdentityValueMatchesQuery{ - resourceAddress: resourceAddress, - attributePath: attributePath, - } -} diff --git a/querycheck/expect_identity_value_matches_query_at_path.go b/querycheck/expect_identity_value_matches_query_at_path.go deleted file mode 100644 index f36adbe76..000000000 --- a/querycheck/expect_identity_value_matches_query_at_path.go +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -import ( - "context" - "fmt" - "reflect" - - "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" - - "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" -) - -var _ QueryCheck = expectIdentityValueMatchesQueryAtPath{} - -type expectIdentityValueMatchesQueryAtPath struct { - resourceAddress string - identityAttrPath tfjsonpath.Path - queryAttrPath tfjsonpath.Path -} - -// CheckQuery implements the query check logic. -func (e expectIdentityValueMatchesQueryAtPath) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { - var resource *plugintest.QueryResult - - if req.Query == nil { - resp.Error = fmt.Errorf("query is nil") - - return - } - - if len(req.Query.Address) == 0 { - resp.Error = fmt.Errorf("query does not contain any address values") - - return - } - - if e.resourceAddress == req.Query.Address { - resource = req.Query - } - - if resource == nil { - resp.Error = fmt.Errorf("%s - Resource not found in query", e.resourceAddress) - - return - } - - if resource.Identity == nil || len(resource.Identity) == 0 { - resp.Error = fmt.Errorf("%s - Identity not found in query. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.14+)", e.resourceAddress) - - return - } - - identityResult, err := tfjsonpath.Traverse(resource.Identity, e.identityAttrPath) - - if err != nil { - resp.Error = err - - return - } - - queryResult, err := tfjsonpath.Traverse(resource.ResourceObject, e.queryAttrPath) - - if err != nil { - resp.Error = err - - return - } - - if !reflect.DeepEqual(identityResult, queryResult) { - resp.Error = fmt.Errorf( - "expected identity (%[1]s.%[2]s) and query value (%[1]s.%[3]s) to match, but they differ: identity value: %[4]v, query value: %[5]v", - e.resourceAddress, - e.identityAttrPath.String(), - e.queryAttrPath.String(), - identityResult, - queryResult, - ) - - return - } -} - -// ExpectIdentityValueMatchesQueryAtPath returns a query check that asserts that the specified identity attribute at the given resource -// matches the specified attribute in query. This is useful when an identity attribute is in sync with a query attribute of a different path. -// -// This query check can only be used with managed resources that support resource identity. Resource identity is only supported in Terraform v1.14+ -func ExpectIdentityValueMatchesQueryAtPath(resourceAddress string, identityAttrPath, queryAttrPath tfjsonpath.Path) QueryCheck { - return expectIdentityValueMatchesQueryAtPath{ - resourceAddress: resourceAddress, - identityAttrPath: identityAttrPath, - queryAttrPath: queryAttrPath, - } -} diff --git a/querycheck/expect_identity_value_matches_query_at_path_example_test.go b/querycheck/expect_identity_value_matches_query_at_path_example_test.go deleted file mode 100644 index 6764c1781..000000000 --- a/querycheck/expect_identity_value_matches_query_at_path_example_test.go +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck_test - -import ( - "testing" - - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/querycheck" - "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" - "github.com/hashicorp/terraform-plugin-testing/tfversion" -) - -func ExampleExpectIdentityValueMatchesQueryAtPath() { - // A typical test would accept *testing.T as a function parameter, for instance `func TestSomething(t *testing.T) { ... }`. - t := &testing.T{} - t.Parallel() - - resource.Test(t, resource.TestCase{ - // Resource identity support is only available in Terraform v1.14+ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - // Provider definition omitted. Assuming "test_resource": - // - Has an identity schema with an "identity_id" string attribute - // - Has a resource schema with an "query_id" string attribute - Steps: []resource.TestStep{ - { - Config: `resource "test_resource" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - // The identity attribute at "identity_id" and query attribute at "query_id" must match - querycheck.ExpectIdentityValueMatchesQueryAtPath( - "test_resource.one", - tfjsonpath.New("identity_id"), - tfjsonpath.New("query_id"), - ), - }, - }, - }, - }) -} diff --git a/querycheck/expect_identity_value_matches_query_at_path_test.go b/querycheck/expect_identity_value_matches_query_at_path_test.go deleted file mode 100644 index 8b9f7dc5b..000000000 --- a/querycheck/expect_identity_value_matches_query_at_path_test.go +++ /dev/null @@ -1,344 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck_test - -import ( - "regexp" - "testing" - - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-go/tftypes" - - r "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" - "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" - "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" - "github.com/hashicorp/terraform-plugin-testing/internal/teststep" - "github.com/hashicorp/terraform-plugin-testing/querycheck" - "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" - "github.com/hashicorp/terraform-plugin-testing/tfversion" -) - -func TestExpectIdentityValueMatchesQueryAtPath_CheckQuery_ResourceNotFound(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectIdentityValueMatchesQueryAtPath( - "examplecloud_thing.two", - tfjsonpath.New("id"), - tfjsonpath.New("id"), - ), - }, - ExpectError: regexp.MustCompile("examplecloud_thing.two - Resource not found in query"), - }, - }, - }) -} - -func TestExpectIdentityValueMatchesQueryAtPath_CheckQuery_No_Terraform_Identity_Support(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories - tfversion.SkipAbove(tfversion.Version1_11_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - // Resource support identity, but the Terraform versions running will not. - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectIdentityValueMatchesQueryAtPath( - "examplecloud_thing.one", - tfjsonpath.New("id"), - tfjsonpath.New("id"), - ), - }, - ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in query. Either the resource ` + - `does not support identity or the Terraform version running the test does not support identity. \(must be v1.14\+\)`, - ), - }, - }, - }) -} - -func TestExpectIdentityValueMatchesQueryAtPath_CheckQuery_No_Identity(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - // Resource does not support identity - "examplecloud": examplecloudProviderNoIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectIdentityValueMatchesQueryAtPath( - "examplecloud_thing.one", - tfjsonpath.New("id"), - tfjsonpath.New("id"), - ), - }, - ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in query. Either the resource ` + - `does not support identity or the Terraform version running the test does not support identity. \(must be v1.14\+\)`, - ), - }, - }, - }) -} - -func TestExpectIdentityValueMatchesQueryAtPath_CheckQuery_String_Matches(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithResourceIdentityDifferentPaths(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectIdentityValueMatchesQueryAtPath( - "examplecloud_thing.one", - tfjsonpath.New("identity_id"), - tfjsonpath.New("query_id"), - ), - }, - }, - }, - }) -} - -func TestExpectIdentityValueMatchesQueryAtPath_CheckQuery_String_DoesntMatch(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithMismatchedResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectIdentityValueMatchesQueryAtPath( - "examplecloud_thing.one", - tfjsonpath.New("id"), - tfjsonpath.New("id"), - ), - }, - ExpectError: regexp.MustCompile(`expected identity \(examplecloud_thing.one.id\) and query value \(examplecloud_thing.one.id\) to match, but they differ: identity value: id-123, query value: 321-di`), - }, - }, - }) -} - -func TestExpectIdentityValueMatchesQueryAtPath_CheckQuery_List(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithResourceIdentityDifferentPaths(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectIdentityValueMatchesQueryAtPath( - "examplecloud_thing.one", - tfjsonpath.New("identity_list_of_numbers"), - tfjsonpath.New("query_list_of_numbers"), - ), - }, - }, - }, - }) -} - -func TestExpectIdentityValueMatchesQueryAtPath_CheckQuery_List_DoesntMatch(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithMismatchedResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectIdentityValueMatchesQueryAtPath( - "examplecloud_thing.one", - tfjsonpath.New("list_of_numbers"), - tfjsonpath.New("list_of_numbers"), - ), - }, - ExpectError: regexp.MustCompile(`expected identity \(examplecloud_thing.one.list_of_numbers\) and query value \(examplecloud_thing.one.list_of_numbers\) to match, but they differ: identity value: \[1 2 3 4\], query value: \[4 3 2 1\]`), - }, - }, - }) -} - -func examplecloudProviderWithResourceIdentityDifferentPaths() func() (tfprotov6.ProviderServer, error) { - return providerserver.NewProviderServer(testprovider.Provider{ - Resources: map[string]testprovider.Resource{ - "examplecloud_thing": { - CreateResponse: &resource.CreateResponse{ - NewQuery: tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "name": tftypes.String, - "query_id": tftypes.String, - "query_list_of_numbers": tftypes.List{ElementType: tftypes.Number}, - }, - }, - map[string]tftypes.Value{ - "name": tftypes.NewValue(tftypes.String, "test value"), - "query_id": tftypes.NewValue(tftypes.String, "id-123"), - "query_list_of_numbers": tftypes.NewValue( - tftypes.List{ElementType: tftypes.Number}, - []tftypes.Value{ - tftypes.NewValue(tftypes.Number, 1), - tftypes.NewValue(tftypes.Number, 2), - tftypes.NewValue(tftypes.Number, 3), - tftypes.NewValue(tftypes.Number, 4), - }, - ), - }, - ), - NewIdentity: teststep.Pointer(tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "identity_id": tftypes.String, - "identity_list_of_numbers": tftypes.List{ElementType: tftypes.Number}, - }, - }, - map[string]tftypes.Value{ - "identity_id": tftypes.NewValue(tftypes.String, "id-123"), - "identity_list_of_numbers": tftypes.NewValue( - tftypes.List{ElementType: tftypes.Number}, - []tftypes.Value{ - tftypes.NewValue(tftypes.Number, 1), - tftypes.NewValue(tftypes.Number, 2), - tftypes.NewValue(tftypes.Number, 3), - tftypes.NewValue(tftypes.Number, 4), - }, - ), - }, - )), - }, - ReadResponse: &resource.ReadResponse{ - NewQuery: tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "name": tftypes.String, - "query_id": tftypes.String, - "query_list_of_numbers": tftypes.List{ElementType: tftypes.Number}, - }, - }, - map[string]tftypes.Value{ - "name": tftypes.NewValue(tftypes.String, "test value"), - "query_id": tftypes.NewValue(tftypes.String, "id-123"), - "query_list_of_numbers": tftypes.NewValue( - tftypes.List{ElementType: tftypes.Number}, - []tftypes.Value{ - tftypes.NewValue(tftypes.Number, 1), - tftypes.NewValue(tftypes.Number, 2), - tftypes.NewValue(tftypes.Number, 3), - tftypes.NewValue(tftypes.Number, 4), - }, - ), - }, - ), - NewIdentity: teststep.Pointer(tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "identity_id": tftypes.String, - "identity_list_of_numbers": tftypes.List{ElementType: tftypes.Number}, - }, - }, - map[string]tftypes.Value{ - "identity_id": tftypes.NewValue(tftypes.String, "id-123"), - "identity_list_of_numbers": tftypes.NewValue( - tftypes.List{ElementType: tftypes.Number}, - []tftypes.Value{ - tftypes.NewValue(tftypes.Number, 1), - tftypes.NewValue(tftypes.Number, 2), - tftypes.NewValue(tftypes.Number, 3), - tftypes.NewValue(tftypes.Number, 4), - }, - ), - }, - )), - }, - IdentitySchemaResponse: &resource.IdentitySchemaResponse{ - Schema: &tfprotov6.ResourceIdentitySchema{ - IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ - { - Name: "identity_id", - Type: tftypes.String, - RequiredForImport: true, - }, - { - Name: "identity_list_of_numbers", - Type: tftypes.List{ElementType: tftypes.Number}, - OptionalForImport: true, - }, - }, - }, - }, - SchemaResponse: &resource.SchemaResponse{ - Schema: &tfprotov6.Schema{ - Block: &tfprotov6.SchemaBlock{ - Attributes: []*tfprotov6.SchemaAttribute{ - { - Name: "name", - Type: tftypes.String, - Computed: true, - }, - { - Name: "query_id", - Type: tftypes.String, - Computed: true, - }, - { - Name: "query_list_of_numbers", - Type: tftypes.List{ElementType: tftypes.Number}, - Computed: true, - }, - }, - }, - }, - }, - }, - }, - }) -} diff --git a/querycheck/expect_identity_value_matches_query_example_test.go b/querycheck/expect_identity_value_matches_query_example_test.go deleted file mode 100644 index 390f80fe0..000000000 --- a/querycheck/expect_identity_value_matches_query_example_test.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck_test - -import ( - "testing" - - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/querycheck" - "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" - "github.com/hashicorp/terraform-plugin-testing/tfversion" -) - -func ExampleExpectIdentityValueMatchesQuery() { - // A typical test would accept *testing.T as a function parameter, for instance `func TestSomething(t *testing.T) { ... }`. - t := &testing.T{} - t.Parallel() - - resource.Test(t, resource.TestCase{ - // Resource identity support is only available in Terraform v1.14+ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - // Provider definition omitted. Assuming "test_resource": - // - Has an identity schema with an "id" string attribute - // - Has a resource schema with an "id" string attribute - Steps: []resource.TestStep{ - { - Config: `resource "test_resource" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - // The identity attribute and query attribute at "id" must match - querycheck.ExpectIdentityValueMatchesQuery("test_resource.one", tfjsonpath.New("id")), - }, - }, - }, - }) -} diff --git a/querycheck/expect_identity_value_matches_query_test.go b/querycheck/expect_identity_value_matches_query_test.go deleted file mode 100644 index a18a380ed..000000000 --- a/querycheck/expect_identity_value_matches_query_test.go +++ /dev/null @@ -1,308 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck_test - -import ( - "regexp" - "testing" - - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-go/tftypes" - - r "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" - "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" - "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" - "github.com/hashicorp/terraform-plugin-testing/internal/teststep" - "github.com/hashicorp/terraform-plugin-testing/querycheck" - "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" - "github.com/hashicorp/terraform-plugin-testing/tfversion" -) - -func TestExpectIdentityValueMatchesQuery_CheckQuery_ResourceNotFound(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectIdentityValueMatchesQuery( - "examplecloud_thing.two", - tfjsonpath.New("id"), - ), - }, - ExpectError: regexp.MustCompile("examplecloud_thing.two - Resource not found in query"), - }, - }, - }) -} - -func TestExpectIdentityValueMatchesQuery_CheckQuery_No_Identity(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - // Resource does not support identity - "examplecloud": examplecloudProviderNoIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectIdentityValueMatchesQuery( - "examplecloud_thing.one", - tfjsonpath.New("id"), - ), - }, - ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in query. Either the resource ` + - `does not support identity or the Terraform version running the test does not support identity. \(must be v1.14\+\)`, - ), - }, - }, - }) -} - -func TestExpectIdentityValueMatchesQuery_CheckQuery_String_Matches(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectIdentityValueMatchesQuery( - "examplecloud_thing.one", - tfjsonpath.New("id"), - ), - }, - }, - }, - }) -} - -func TestExpectIdentityValueMatchesQuery_CheckQuery_String_DoesntMatch(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithMismatchedResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectIdentityValueMatchesQuery( - "examplecloud_thing.one", - tfjsonpath.New("id"), - ), - }, - ExpectError: regexp.MustCompile("expected identity and query value at path to match, but they differ: examplecloud_thing.one.id, identity value: id-123, query value: 321-di"), - }, - }, - }) -} - -func TestExpectIdentityValueMatchesQuery_CheckQuery_List(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectIdentityValueMatchesQuery( - "examplecloud_thing.one", - tfjsonpath.New("list_of_numbers"), - ), - }, - }, - }, - }) -} - -func TestExpectIdentityValueMatchesQuery_CheckQuery_List_DoesntMatch(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithMismatchedResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectIdentityValueMatchesQuery( - "examplecloud_thing.one", - tfjsonpath.New("list_of_numbers"), - ), - }, - ExpectError: regexp.MustCompile(`expected identity and query value at path to match, but they differ: examplecloud_thing.one.list_of_numbers, identity value: \[1 2 3 4\], query value: \[4 3 2 1\]`), - }, - }, - }) -} - -func examplecloudProviderWithMismatchedResourceIdentity() func() (tfprotov6.ProviderServer, error) { - return providerserver.NewProviderServer(testprovider.Provider{ - Resources: map[string]testprovider.Resource{ - "examplecloud_thing": { - CreateResponse: &resource.CreateResponse{ - NewQuery: tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "name": tftypes.String, - "id": tftypes.String, - "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, - }, - }, - map[string]tftypes.Value{ - "name": tftypes.NewValue(tftypes.String, "test value"), - "id": tftypes.NewValue(tftypes.String, "321-di"), // doesn't match identity -> id - "list_of_numbers": tftypes.NewValue( - tftypes.List{ElementType: tftypes.Number}, - []tftypes.Value{ - tftypes.NewValue(tftypes.Number, 4), // doesn't match identity -> list_of_numbers[0] - tftypes.NewValue(tftypes.Number, 3), // doesn't match identity -> list_of_numbers[1] - tftypes.NewValue(tftypes.Number, 2), // doesn't match identity -> list_of_numbers[2] - tftypes.NewValue(tftypes.Number, 1), // doesn't match identity -> list_of_numbers[3] - }, - ), - }, - ), - NewIdentity: teststep.Pointer(tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, - }, - }, - map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "id-123"), - "list_of_numbers": tftypes.NewValue( - tftypes.List{ElementType: tftypes.Number}, - []tftypes.Value{ - tftypes.NewValue(tftypes.Number, 1), - tftypes.NewValue(tftypes.Number, 2), - tftypes.NewValue(tftypes.Number, 3), - tftypes.NewValue(tftypes.Number, 4), - }, - ), - }, - )), - }, - ReadResponse: &resource.ReadResponse{ - NewQuery: tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "name": tftypes.String, - "id": tftypes.String, - "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, - }, - }, - map[string]tftypes.Value{ - "name": tftypes.NewValue(tftypes.String, "test value"), - "id": tftypes.NewValue(tftypes.String, "321-di"), // doesn't match identity -> id - "list_of_numbers": tftypes.NewValue( - tftypes.List{ElementType: tftypes.Number}, - []tftypes.Value{ - tftypes.NewValue(tftypes.Number, 4), // doesn't match identity -> list_of_numbers[0] - tftypes.NewValue(tftypes.Number, 3), // doesn't match identity -> list_of_numbers[1] - tftypes.NewValue(tftypes.Number, 2), // doesn't match identity -> list_of_numbers[2] - tftypes.NewValue(tftypes.Number, 1), // doesn't match identity -> list_of_numbers[3] - }, - ), - }, - ), - NewIdentity: teststep.Pointer(tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, - }, - }, - map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "id-123"), - "list_of_numbers": tftypes.NewValue( - tftypes.List{ElementType: tftypes.Number}, - []tftypes.Value{ - tftypes.NewValue(tftypes.Number, 1), - tftypes.NewValue(tftypes.Number, 2), - tftypes.NewValue(tftypes.Number, 3), - tftypes.NewValue(tftypes.Number, 4), - }, - ), - }, - )), - }, - IdentitySchemaResponse: &resource.IdentitySchemaResponse{ - Schema: &tfprotov6.ResourceIdentitySchema{ - IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ - { - Name: "id", - Type: tftypes.String, - RequiredForImport: true, - }, - { - Name: "list_of_numbers", - Type: tftypes.List{ElementType: tftypes.Number}, - OptionalForImport: true, - }, - }, - }, - }, - SchemaResponse: &resource.SchemaResponse{ - Schema: &tfprotov6.Schema{ - Block: &tfprotov6.SchemaBlock{ - Attributes: []*tfprotov6.SchemaAttribute{ - { - Name: "name", - Type: tftypes.String, - Computed: true, - }, - { - Name: "id", - Type: tftypes.String, - Computed: true, - }, - { - Name: "list_of_numbers", - Type: tftypes.List{ElementType: tftypes.Number}, - Computed: true, - }, - }, - }, - }, - }, - }, - }, - }) -} diff --git a/querycheck/expect_identity_value_test.go b/querycheck/expect_identity_value_test.go deleted file mode 100644 index d7042ca87..000000000 --- a/querycheck/expect_identity_value_test.go +++ /dev/null @@ -1,431 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck_test - -import ( - "regexp" - "testing" - - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-go/tftypes" - - r "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" - "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" - "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" - "github.com/hashicorp/terraform-plugin-testing/internal/teststep" - "github.com/hashicorp/terraform-plugin-testing/knownvalue" - "github.com/hashicorp/terraform-plugin-testing/querycheck" - "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" - "github.com/hashicorp/terraform-plugin-testing/tfversion" -) - -func TestExpectIdentityValue_CheckQuery_ResourceNotFound(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectIdentityValue( - "examplecloud_thing.two", - tfjsonpath.New("id"), - knownvalue.StringExact("id-123"), - ), - }, - ExpectError: regexp.MustCompile("examplecloud_thing.two - Resource not found in query"), - }, - }, - }) -} - -func TestExpectIdentityValue_CheckQuery_No_Identity(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - // Resource does not support identity - "examplecloud": examplecloudProviderNoIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectIdentityValue( - "examplecloud_thing.one", - tfjsonpath.New("id"), - knownvalue.StringExact("id-123"), - ), - }, - ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in query. Either the resource ` + - `does not support identity or the Terraform version running the test does not support identity. \(must be v1.14\+\)`, - ), - }, - }, - }) -} - -func TestExpectIdentityValue_CheckQuery_String(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectIdentityValue( - "examplecloud_thing.one", - tfjsonpath.New("id"), - knownvalue.StringExact("id-123")), - }, - }, - }, - }) -} - -func TestExpectIdentityValue_CheckQuery_String_KnownValueWrongType(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectIdentityValue( - "examplecloud_thing.one", - tfjsonpath.New("id"), - knownvalue.Bool(true)), - }, - ExpectError: regexp.MustCompile("expected bool value for Bool check, got: string"), - }, - }, - }) -} - -func TestExpectIdentityValue_CheckQuery_String_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectIdentityValue( - "examplecloud_thing.one", - tfjsonpath.New("id"), - knownvalue.StringExact("321-id")), - }, - ExpectError: regexp.MustCompile("expected value 321-id for StringExact check, got: id-123"), - }, - }, - }) -} - -func TestExpectIdentityValue_CheckQuery_List(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectIdentityValue( - "examplecloud_thing.one", - tfjsonpath.New("list_of_numbers").AtSliceIndex(0), - knownvalue.Int64Exact(1), - ), - querycheck.ExpectIdentityValue( - "examplecloud_thing.one", - tfjsonpath.New("list_of_numbers").AtSliceIndex(1), - knownvalue.Int64Exact(2), - ), - querycheck.ExpectIdentityValue( - "examplecloud_thing.one", - tfjsonpath.New("list_of_numbers").AtSliceIndex(2), - knownvalue.Int64Exact(3), - ), - querycheck.ExpectIdentityValue( - "examplecloud_thing.one", - tfjsonpath.New("list_of_numbers").AtSliceIndex(3), - knownvalue.Int64Exact(4), - ), - }, - }, - }, - }) -} - -func TestExpectIdentityValue_CheckQuery_List_KnownValueWrongType(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {} - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectIdentityValue( - "examplecloud_thing.one", - tfjsonpath.New("list_of_numbers"), - knownvalue.MapExact(map[string]knownvalue.Check{}), - ), - }, - ExpectError: regexp.MustCompile(`expected map\[string\]any value for MapExact check, got: \[\]interface {}`), - }, - }, - }) -} - -func TestExpectIdentityValue_CheckQuery_List_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectIdentityValue( - "examplecloud_thing.one", - tfjsonpath.New("list_of_numbers"), - knownvalue.ListExact([]knownvalue.Check{ - knownvalue.Int64Exact(4), - knownvalue.Int64Exact(3), - knownvalue.Int64Exact(2), - knownvalue.Int64Exact(1), - }), - ), - }, - ExpectError: regexp.MustCompile(`list element index 0: expected value 4 for Int64Exact check, got: 1`), - }, - }, - }) -} - -func examplecloudProviderWithResourceIdentity() func() (tfprotov6.ProviderServer, error) { - return providerserver.NewProviderServer(testprovider.Provider{ - Resources: map[string]testprovider.Resource{ - "examplecloud_thing": { - CreateResponse: &resource.CreateResponse{ - NewQuery: tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "name": tftypes.String, - "id": tftypes.String, - "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, - }, - }, - map[string]tftypes.Value{ - "name": tftypes.NewValue(tftypes.String, "test value"), - "id": tftypes.NewValue(tftypes.String, "id-123"), - "list_of_numbers": tftypes.NewValue( - tftypes.List{ElementType: tftypes.Number}, - []tftypes.Value{ - tftypes.NewValue(tftypes.Number, 1), - tftypes.NewValue(tftypes.Number, 2), - tftypes.NewValue(tftypes.Number, 3), - tftypes.NewValue(tftypes.Number, 4), - }, - ), - }, - ), - NewIdentity: teststep.Pointer(tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, - }, - }, - map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "id-123"), - "list_of_numbers": tftypes.NewValue( - tftypes.List{ElementType: tftypes.Number}, - []tftypes.Value{ - tftypes.NewValue(tftypes.Number, 1), - tftypes.NewValue(tftypes.Number, 2), - tftypes.NewValue(tftypes.Number, 3), - tftypes.NewValue(tftypes.Number, 4), - }, - ), - }, - )), - }, - ReadResponse: &resource.ReadResponse{ - NewQuery: tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "name": tftypes.String, - "id": tftypes.String, - "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, - }, - }, - map[string]tftypes.Value{ - "name": tftypes.NewValue(tftypes.String, "test value"), - "id": tftypes.NewValue(tftypes.String, "id-123"), - "list_of_numbers": tftypes.NewValue( - tftypes.List{ElementType: tftypes.Number}, - []tftypes.Value{ - tftypes.NewValue(tftypes.Number, 1), - tftypes.NewValue(tftypes.Number, 2), - tftypes.NewValue(tftypes.Number, 3), - tftypes.NewValue(tftypes.Number, 4), - }, - ), - }, - ), - NewIdentity: teststep.Pointer(tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, - }, - }, - map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "id-123"), - "list_of_numbers": tftypes.NewValue( - tftypes.List{ElementType: tftypes.Number}, - []tftypes.Value{ - tftypes.NewValue(tftypes.Number, 1), - tftypes.NewValue(tftypes.Number, 2), - tftypes.NewValue(tftypes.Number, 3), - tftypes.NewValue(tftypes.Number, 4), - }, - ), - }, - )), - }, - IdentitySchemaResponse: &resource.IdentitySchemaResponse{ - Schema: &tfprotov6.ResourceIdentitySchema{ - IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ - { - Name: "id", - Type: tftypes.String, - RequiredForImport: true, - }, - { - Name: "list_of_numbers", - Type: tftypes.List{ElementType: tftypes.Number}, - OptionalForImport: true, - }, - }, - }, - }, - SchemaResponse: &resource.SchemaResponse{ - Schema: &tfprotov6.Schema{ - Block: &tfprotov6.SchemaBlock{ - Attributes: []*tfprotov6.SchemaAttribute{ - { - Name: "name", - Type: tftypes.String, - Computed: true, - }, - { - Name: "id", - Type: tftypes.String, - Computed: true, - }, - { - Name: "list_of_numbers", - Type: tftypes.List{ElementType: tftypes.Number}, - Computed: true, - }, - }, - }, - }, - }, - }, - }, - }) -} - -func examplecloudProviderNoIdentity() func() (tfprotov6.ProviderServer, error) { - return providerserver.NewProviderServer(testprovider.Provider{ - Resources: map[string]testprovider.Resource{ - "examplecloud_thing": { - CreateResponse: &resource.CreateResponse{ - NewQuery: tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "name": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "name": tftypes.NewValue(tftypes.String, "test value"), - }, - ), - }, - ReadResponse: &resource.ReadResponse{ - NewQuery: tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "name": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "name": tftypes.NewValue(tftypes.String, "test value"), - }, - ), - }, - SchemaResponse: &resource.SchemaResponse{ - Schema: &tfprotov6.Schema{ - Block: &tfprotov6.SchemaBlock{ - Attributes: []*tfprotov6.SchemaAttribute{ - { - Name: "name", - Type: tftypes.String, - Computed: true, - }, - }, - }, - }, - }, - }, - }, - }) -} diff --git a/querycheck/expect_known_value.go b/querycheck/expect_known_value.go deleted file mode 100644 index 647ef4e5b..000000000 --- a/querycheck/expect_known_value.go +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -import ( - "context" - "fmt" - "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" - - "github.com/hashicorp/terraform-plugin-testing/knownvalue" - "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" -) - -// Resource Query Check -var _ QueryCheck = expectKnownValue{} - -type expectKnownValue struct { - resourceAddress string - attributePath tfjsonpath.Path - knownValue knownvalue.Check -} - -// CheckQuery implements the query check logic. -func (e expectKnownValue) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { - var resource *plugintest.QueryResult - - if req.Query == nil { - resp.Error = fmt.Errorf("query is nil") - - return - } - - if len(req.Query.Address) == 0 { - resp.Error = fmt.Errorf("query does not contain any address values") - - return - } - - if e.resourceAddress == req.Query.Address { - resource = req.Query - } - - if resource == nil { - resp.Error = fmt.Errorf("%s - Resource not found in query", e.resourceAddress) - - return - } - - result, err := tfjsonpath.Traverse(resource.ResourceObject, e.attributePath) - - if err != nil { - resp.Error = err - - return - } - - if err := e.knownValue.CheckValue(result); err != nil { - resp.Error = fmt.Errorf("error checking value for attribute at path: %s.%s, err: %s", e.resourceAddress, e.attributePath.String(), err) - - return - } -} - -// ExpectKnownValue returns a query check that asserts that the specified attribute at the given resource -// has a known type and value. -func ExpectKnownValue(resourceAddress string, attributePath tfjsonpath.Path, knownValue knownvalue.Check) QueryCheck { - return expectKnownValue{ - resourceAddress: resourceAddress, - attributePath: attributePath, - knownValue: knownValue, - } -} diff --git a/querycheck/expect_known_value_example_test.go b/querycheck/expect_known_value_example_test.go deleted file mode 100644 index 3c8d078e3..000000000 --- a/querycheck/expect_known_value_example_test.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck_test - -import ( - "testing" - - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/knownvalue" - "github.com/hashicorp/terraform-plugin-testing/querycheck" - "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" -) - -func ExampleExpectKnownValue() { - // A typical test would accept *testing.T as a function parameter, for instance `func TestSomething(t *testing.T) { ... }`. - t := &testing.T{} - t.Parallel() - - resource.Test(t, resource.TestCase{ - // Provider definition omitted. - Steps: []resource.TestStep{ - { - Config: `resource "test_resource" "one" { - bool_attribute = true - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("bool_attribute"), - knownvalue.Bool(true), - ), - }, - }, - }, - }) -} diff --git a/querycheck/expect_known_value_test.go b/querycheck/expect_known_value_test.go deleted file mode 100644 index 0f5a132df..000000000 --- a/querycheck/expect_known_value_test.go +++ /dev/null @@ -1,1644 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck_test - -import ( - "context" - "fmt" - "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" - "math/big" - "regexp" - "strings" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - - r "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/knownvalue" - "github.com/hashicorp/terraform-plugin-testing/querycheck" - "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" -) - -func TestExpectKnownValue_CheckQuery_ResourceNotFound(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - bool_attribute = true - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.two", - tfjsonpath.New("bool_attribute"), - knownvalue.Bool(true), - ), - }, - ExpectError: regexp.MustCompile("test_resource.two - Resource not found in query"), - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_AttributeValueNull(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("bool_attribute"), - knownvalue.Null(), - ), - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("float_attribute"), - knownvalue.Null(), - ), - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("int_attribute"), - knownvalue.Null(), - ), - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("list_attribute"), - knownvalue.Null(), - ), - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("list_nested_block"), - knownvalue.ListExact([]knownvalue.Check{}), - ), - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("map_attribute"), - knownvalue.Null(), - ), - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("set_attribute"), - knownvalue.Null(), - ), - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("set_nested_block"), - knownvalue.SetExact([]knownvalue.Check{}), - ), - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("string_attribute"), - knownvalue.Null(), - ), - }, - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_AttributeValueNotNull(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - bool_attribute = true - float_attribute = 1.23 - int_attribute = 123 - list_attribute = ["value1", "value2"] - list_nested_block { - list_nested_block_attribute = "str" - } - map_attribute = { - key1 = "value1" - } - set_attribute = ["value1", "value2"] - set_nested_block { - set_nested_block_attribute = "str" - } - string_attribute = "str" - }`, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("bool_attribute"), - knownvalue.NotNull(), - ), - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("float_attribute"), - knownvalue.NotNull(), - ), - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("int_attribute"), - knownvalue.NotNull(), - ), - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("list_attribute"), - knownvalue.NotNull(), - ), - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("list_nested_block"), - knownvalue.ListSizeExact(1), - ), - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("map_attribute"), - knownvalue.NotNull(), - ), - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("set_attribute"), - knownvalue.NotNull(), - ), - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("set_nested_block"), - knownvalue.SetSizeExact(1), - ), - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("string_attribute"), - knownvalue.NotNull(), - ), - }, - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_Bool(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - bool_attribute = true - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("bool_attribute"), - knownvalue.Bool(true), - ), - }, - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_Bool_KnownValueWrongType(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - bool_attribute = true - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("bool_attribute"), - knownvalue.Float64Exact(1.23), - ), - }, - ExpectError: regexp.MustCompile(`expected json\.Number value for Float64Exact check, got: bool`), - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_Bool_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - bool_attribute = true - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("bool_attribute"), - knownvalue.Bool(false), - ), - }, - ExpectError: regexp.MustCompile("expected value false for Bool check, got: true"), - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_Float64(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - float_attribute = 1.23 - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("float_attribute"), - knownvalue.Float64Exact(1.23), - ), - }, - }, - }, - }) -} - -// We do not need equivalent tests for Int64 and Number as they all test the same logic. -func TestExpectKnownValue_CheckQuery_Float64_KnownValueWrongType(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - float_attribute = 1.23 - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("float_attribute"), - knownvalue.StringExact("str"), - ), - }, - ExpectError: regexp.MustCompile(`expected string value for StringExact check, got: json\.Number`), - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_Float64_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - float_attribute = 1.23 - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("float_attribute"), - knownvalue.Float64Exact(3.21), - ), - }, - ExpectError: regexp.MustCompile("expected value 3.21 for Float64Exact check, got: 1.23"), - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_Int64(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - int_attribute = 123 - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("int_attribute"), - knownvalue.Int64Exact(123), - ), - }, - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_Int64_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - int_attribute = 123 - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("int_attribute"), - knownvalue.Int64Exact(321), - ), - }, - ExpectError: regexp.MustCompile("expected value 321 for Int64Exact check, got: 123"), - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_List(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - list_attribute = [ - "value1", - "value2" - ] - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("list_attribute"), - knownvalue.ListExact([]knownvalue.Check{ - knownvalue.StringExact("value1"), - knownvalue.StringExact("value2"), - }), - ), - }, - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_List_KnownValueWrongType(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - list_attribute = [ - "value1", - "value2" - ] - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("list_attribute"), - knownvalue.MapExact(map[string]knownvalue.Check{}), - ), - }, - ExpectError: regexp.MustCompile(`expected map\[string\]any value for MapExact check, got: \[\]interface {}`), - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_List_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - list_attribute = [ - "value1", - "value2" - ] - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("list_attribute"), - knownvalue.ListExact([]knownvalue.Check{ - knownvalue.StringExact("value3"), - knownvalue.StringExact("value4"), - }), - ), - }, - ExpectError: regexp.MustCompile(`list element index 0: expected value value3 for StringExact check, got: value1`), - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_ListPartial(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - list_attribute = [ - "value1", - "value2" - ] - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("list_attribute"), - knownvalue.ListPartial(map[int]knownvalue.Check{ - 0: knownvalue.StringExact("value1"), - }), - ), - }, - }, - }, - }) -} - -// No need to check KnownValueWrongType for ListPartial as all lists, and sets are []any in -// tfjson.Query. -func TestExpectKnownValue_CheckQuery_ListPartial_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - list_attribute = [ - "value1", - "value2" - ] - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("list_attribute"), - knownvalue.ListPartial(map[int]knownvalue.Check{ - 0: knownvalue.StringExact("value3"), - }), - ), - }, - ExpectError: regexp.MustCompile(`list element 0: expected value value3 for StringExact check, got: value1`), - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_ListElements(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - list_attribute = [ - "value1", - "value2" - ] - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("list_attribute"), - knownvalue.ListSizeExact(2), - ), - }, - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_ListElements_WrongNum(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - list_attribute = [ - "value1", - "value2" - ] - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("list_attribute"), - knownvalue.ListSizeExact(3), - ), - }, - ExpectError: regexp.MustCompile("expected 3 elements for ListSizeExact check, got 2 elements"), - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_ListNestedBlock(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - list_nested_block { - list_nested_block_attribute = "str" - } - list_nested_block { - list_nested_block_attribute = "rts" - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("list_nested_block"), - knownvalue.ListExact([]knownvalue.Check{ - knownvalue.MapExact(map[string]knownvalue.Check{ - "list_nested_block_attribute": knownvalue.StringExact("str"), - }), - knownvalue.MapExact(map[string]knownvalue.Check{ - "list_nested_block_attribute": knownvalue.StringExact("rts"), - }), - }), - ), - }, - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_ListNestedBlockPartial(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - list_nested_block { - list_nested_block_attribute = "str" - } - list_nested_block { - list_nested_block_attribute = "rts" - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("list_nested_block"), - knownvalue.ListPartial(map[int]knownvalue.Check{ - 1: knownvalue.MapExact(map[string]knownvalue.Check{ - "list_nested_block_attribute": knownvalue.StringExact("rts"), - }), - }), - ), - }, - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_ListNestedBlockElements(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - list_nested_block { - list_nested_block_attribute = "str" - } - list_nested_block { - list_nested_block_attribute = "rts" - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("list_nested_block"), - knownvalue.ListSizeExact(2), - ), - }, - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_Map(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - map_attribute = { - key1 = "value1" - key2 = "value2" - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("map_attribute"), - knownvalue.MapExact(map[string]knownvalue.Check{ - "key1": knownvalue.StringExact("value1"), - "key2": knownvalue.StringExact("value2"), - }), - ), - }, - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_Map_KnownValueWrongType(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - map_attribute = { - key1 = "value1" - key2 = "value2" - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("map_attribute"), - knownvalue.ListExact([]knownvalue.Check{}), - ), - }, - ExpectError: regexp.MustCompile(`expected \[\]any value for ListExact check, got: map\[string\]interface {}`), - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_Map_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - map_attribute = { - key1 = "value1" - key2 = "value2" - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("map_attribute"), - knownvalue.MapExact(map[string]knownvalue.Check{ - "key3": knownvalue.StringExact("value3"), - "key4": knownvalue.StringExact("value4"), - }), - ), - }, - ExpectError: regexp.MustCompile(`missing element key3 for MapExact check`), - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_MapPartial(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - map_attribute = { - key1 = "value1" - key2 = "value2" - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("map_attribute"), - knownvalue.MapPartial(map[string]knownvalue.Check{ - "key1": knownvalue.StringExact("value1"), - }), - ), - }, - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_MapPartial_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - map_attribute = { - key1 = "value1" - key2 = "value2" - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("map_attribute"), - knownvalue.MapPartial(map[string]knownvalue.Check{ - "key3": knownvalue.StringExact("value1"), - }), - ), - }, - ExpectError: regexp.MustCompile(`missing element key3 for MapPartial check`), - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_MapElements(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - map_attribute = { - key1 = "value1" - key2 = "value2" - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("map_attribute"), - knownvalue.MapSizeExact(2), - ), - }, - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_MapElements_WrongNum(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - map_attribute = { - key1 = "value1" - key2 = "value2" - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("map_attribute"), - knownvalue.MapSizeExact(3), - ), - }, - ExpectError: regexp.MustCompile("expected 3 elements for MapSizeExact check, got 2 elements"), - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_Number(t *testing.T) { - t.Parallel() - - f, _, err := big.ParseFloat("123", 10, 512, big.ToNearestEven) - - if err != nil { - t.Errorf("%s", err) - } - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - int_attribute = 123 - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("int_attribute"), - knownvalue.NumberExact(f), - ), - }, - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_Number_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - f, _, err := big.ParseFloat("321", 10, 512, big.ToNearestEven) - - if err != nil { - t.Errorf("%s", err) - } - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - int_attribute = 123 - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("int_attribute"), - knownvalue.NumberExact(f), - ), - }, - ExpectError: regexp.MustCompile("expected value 321 for NumberExact check, got: 123"), - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_Set(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_attribute = [ - "value1", - "value2" - ] - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("set_attribute"), - knownvalue.SetExact([]knownvalue.Check{ - knownvalue.StringExact("value2"), - knownvalue.StringExact("value1"), - }), - ), - }, - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_Set_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_attribute = [ - "value1", - "value2" - ] - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("set_attribute"), - knownvalue.SetExact([]knownvalue.Check{ - knownvalue.StringExact("value1"), - knownvalue.StringExact("value3"), - }), - ), - }, - ExpectError: regexp.MustCompile(`missing value value3 for SetExact check`), - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_SetPartial(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_attribute = [ - "value1", - "value2" - ] - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("set_attribute"), - knownvalue.SetPartial([]knownvalue.Check{ - knownvalue.StringExact("value2"), - }), - ), - }, - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_SetPartial_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_attribute = [ - "value1", - "value2" - ] - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("set_attribute"), - knownvalue.SetPartial([]knownvalue.Check{ - knownvalue.StringExact("value3"), - }), - ), - }, - ExpectError: regexp.MustCompile(`missing value value3 for SetPartial check`), - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_SetElements(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_attribute = [ - "value1", - "value2" - ] - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("set_attribute"), - knownvalue.SetSizeExact(2), - ), - }, - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_SetNestedBlock(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_nested_block { - set_nested_block_attribute = "str" - } - set_nested_block { - set_nested_block_attribute = "rts" - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("set_nested_block"), - knownvalue.SetExact([]knownvalue.Check{ - knownvalue.MapExact(map[string]knownvalue.Check{ - "set_nested_block_attribute": knownvalue.StringExact("str"), - }), - knownvalue.MapExact(map[string]knownvalue.Check{ - "set_nested_block_attribute": knownvalue.StringExact("rts"), - }), - }), - ), - }, - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_SetNestedBlock_Custom(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_nested_block { - set_nested_block_attribute = "string" - } - set_nested_block { - set_nested_block_attribute = "girts" - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("set_nested_block"), - knownvalue.SetExact([]knownvalue.Check{ - knownvalue.MapExact(map[string]knownvalue.Check{ - "set_nested_block_attribute": StringContains("str"), - }), - knownvalue.MapExact(map[string]knownvalue.Check{ - "set_nested_block_attribute": StringContains("rts"), - }), - }), - ), - }, - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_SetNestedBlockPartial(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_nested_block { - set_nested_block_attribute = "str" - } - set_nested_block { - set_nested_block_attribute = "rts" - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("set_nested_block"), - knownvalue.SetPartial([]knownvalue.Check{ - knownvalue.MapExact(map[string]knownvalue.Check{ - "set_nested_block_attribute": knownvalue.StringExact("rts"), - }), - }), - ), - }, - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_SetNestedBlockElements(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - set_nested_block { - set_nested_block_attribute = "str" - } - set_nested_block { - set_nested_block_attribute = "rts" - } - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("set_nested_block"), - knownvalue.SetSizeExact(2), - ), - }, - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_String(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - string_attribute = "str" - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("string_attribute"), - knownvalue.StringExact("str")), - }, - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_String_Custom(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - string_attribute = "string" - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("string_attribute"), - StringContains("tri")), - }, - }, - }, - }) -} - -var _ knownvalue.Check = stringContains{} - -type stringContains struct { - value string -} - -func (v stringContains) CheckValue(other any) error { - otherVal, ok := other.(string) - - if !ok { - return fmt.Errorf("expected string value for StringContains check, got: %T", other) - } - - if !strings.Contains(otherVal, v.value) { - return fmt.Errorf("expected string %q to contain %q for StringContains check", otherVal, v.value) - } - - return nil -} - -func (v stringContains) String() string { - return v.value -} - -func StringContains(value string) stringContains { - return stringContains{ - value: value, - } -} - -func TestExpectKnownValue_CheckQuery_String_KnownValueWrongType(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - string_attribute = "str" - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("string_attribute"), - knownvalue.Bool(true)), - }, - ExpectError: regexp.MustCompile("expected bool value for Bool check, got: string"), - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_String_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return testProvider(), nil - }, - }, - Steps: []r.TestStep{ - { - Config: `resource "test_resource" "one" { - string_attribute = "str" - } - `, - ConfigQueryChecks: []querycheck.QueryCheck{ - querycheck.ExpectKnownValue( - "test_resource.one", - tfjsonpath.New("string_attribute"), - knownvalue.StringExact("rts")), - }, - ExpectError: regexp.MustCompile("expected value rts for StringExact check, got: str"), - }, - }, - }) -} - -func TestExpectKnownValue_CheckQuery_UnknownAttributeType(t *testing.T) { - t.Parallel() - - testCases := map[string]struct { - knownValue knownvalue.Check - req querycheck.CheckQueryRequest - expectedErr error - }{ - "unrecognised-type": { - knownValue: knownvalue.Int64Exact(123), - req: querycheck.CheckQueryRequest{ - Query: &plugintest.QueryResult{ - ResourceType: "attribute", - }, - }, - expectedErr: fmt.Errorf("query does not contain any address values"), - }, - } - - for name, testCase := range testCases { - t.Run(name, func(t *testing.T) { - t.Parallel() - - e := querycheck.ExpectKnownValue("example_resource.test", tfjsonpath.New("attribute"), testCase.knownValue) - - resp := querycheck.CheckQueryResponse{} - - e.CheckQuery(context.Background(), testCase.req, &resp) - - if diff := cmp.Diff(resp.Error, testCase.expectedErr, equateErrorMessage); diff != "" { - t.Errorf("unexpected difference: %s", diff) - } - }) - } -} - -var equateErrorMessage = cmp.Comparer(func(x, y error) bool { - if x == nil || y == nil { - return x == nil && y == nil - } - - return x.Error() == y.Error() -}) - -func testProvider() *schema.Provider { - return &schema.Provider{ - ResourcesMap: map[string]*schema.Resource{ - "test_resource": { - CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { - d.SetId("test") - - err := d.Set("string_computed_attribute", "computed") - if err != nil { - return diag.Errorf("error setting string_computed_attribute: %s", err) - } - - return nil - }, - UpdateContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { - return nil - }, - DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { - return nil - }, - ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { - return nil - }, - Schema: map[string]*schema.Schema{ - "bool_attribute": { - Optional: true, - Type: schema.TypeBool, - }, - "float_attribute": { - Optional: true, - Type: schema.TypeFloat, - }, - "int_attribute": { - Optional: true, - Type: schema.TypeInt, - }, - "list_attribute": { - Type: schema.TypeList, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - Optional: true, - }, - "list_nested_block": { - Type: schema.TypeList, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "list_nested_block_attribute": { - Type: schema.TypeString, - Optional: true, - }, - }, - }, - }, - "map_attribute": { - Type: schema.TypeMap, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - Optional: true, - }, - "set_attribute": { - Type: schema.TypeSet, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - Optional: true, - }, - "set_nested_block": { - Type: schema.TypeSet, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "set_nested_block_attribute": { - Type: schema.TypeString, - Optional: true, - }, - }, - }, - }, - "set_nested_nested_block": { - Type: schema.TypeSet, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "set_nested_block": { - Type: schema.TypeSet, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "set_nested_block_attribute": { - Type: schema.TypeString, - Optional: true, - }, - }, - }, - }, - }, - }, - }, - "string_attribute": { - Optional: true, - Type: schema.TypeString, - }, - "string_computed_attribute": { - Computed: true, - Type: schema.TypeString, - }, - }, - }, - }, - } -} diff --git a/querycheck/query_check.go b/querycheck/query_check.go index 99e57fd9c..2946b3e60 100644 --- a/querycheck/query_check.go +++ b/querycheck/query_check.go @@ -6,7 +6,7 @@ package querycheck import ( "context" - "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" + tfjson "github.com/hashicorp/terraform-json" ) // QueryCheck defines an interface for implementing test logic that checks a query file and then returns an error @@ -19,7 +19,7 @@ type QueryCheck interface { // CheckQueryRequest is a request for an invoke of the CheckQuery function. type CheckQueryRequest struct { // Query represents a parsed query file, retrieved via the `terraform show -json` command. - Query *plugintest.QueryResult + Query *[]tfjson.LogMsg } // CheckQueryResponse is a response to an invoke of the CheckQuery function. From 884dfffe9a6a46803bd04a1a57c7681c4fee18f6 Mon Sep 17 00:00:00 2001 From: Rain Date: Wed, 3 Sep 2025 11:27:12 -0400 Subject: [PATCH 22/45] Updated terraform exec with new draft and initial query test in helper/resource running --- go.mod | 2 +- go.sum | 4 ++-- internal/plugintest/working_dir.go | 10 +++++++--- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 8f5c74a0e..814ea758f 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/hashicorp/hc-install v0.9.2 github.com/hashicorp/hcl/v2 v2.24.0 github.com/hashicorp/logutils v1.0.0 - github.com/hashicorp/terraform-exec v0.23.1-0.20250902152613-b3bfedb74246 + github.com/hashicorp/terraform-exec v0.23.2-0.20250903143921-c05687bce503 github.com/hashicorp/terraform-json v0.26.1-0.20250829125600-5c1a00f3ccc4 github.com/hashicorp/terraform-plugin-go v0.29.0-beta.1 github.com/hashicorp/terraform-plugin-log v0.9.0 diff --git a/go.sum b/go.sum index 9303cdcec..c65ff4028 100644 --- a/go.sum +++ b/go.sum @@ -76,8 +76,8 @@ github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQx github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/terraform-exec v0.23.1-0.20250902152613-b3bfedb74246 h1:x0J+WssCgSoU+lXLhZceitUlKfq5DvrtO8EwiEUa8Ng= -github.com/hashicorp/terraform-exec v0.23.1-0.20250902152613-b3bfedb74246/go.mod h1:rG9V56jmBbB5hXCo4MpZTr6tYKLaykUONa8mX01yBhg= +github.com/hashicorp/terraform-exec v0.23.2-0.20250903143921-c05687bce503 h1:A/cbqqEjJmPouKHgWlIBQOBPROLO4ubPmHFl7yKZ+As= +github.com/hashicorp/terraform-exec v0.23.2-0.20250903143921-c05687bce503/go.mod h1:rG9V56jmBbB5hXCo4MpZTr6tYKLaykUONa8mX01yBhg= github.com/hashicorp/terraform-json v0.26.1-0.20250829125600-5c1a00f3ccc4 h1:qErXY0TfojskxvAlCiqS4IMmXulQ4TfApiO8IlKkImc= github.com/hashicorp/terraform-json v0.26.1-0.20250829125600-5c1a00f3ccc4/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE= github.com/hashicorp/terraform-plugin-go v0.29.0-beta.1 h1:xeHlRQYev3iMXwX2W7+D1bSfLRBs9jojZXqE6hmNxMI= diff --git a/internal/plugintest/working_dir.go b/internal/plugintest/working_dir.go index 4712720b2..1c7c4e57b 100644 --- a/internal/plugintest/working_dir.go +++ b/internal/plugintest/working_dir.go @@ -567,13 +567,17 @@ func (wd *WorkingDir) Query(ctx context.Context) ([]tfjson.LogMsg, error) { } }*/ + var message tfjson.LogMsg + var related bool + message, related, err = logEmit.NextMessage() - var message tfjson.LogMsg - for message, err = logEmit.NextMessage(); err != nil || message != nil; { + for err != nil || message != nil { + message, related, err = logEmit.NextMessage() messages = append(messages, message) } - if err != nil { + + if related == true { return nil, fmt.Errorf("error running terraform query command: %w", err) } From d5311201d93cac78879a1400d73e24af32788d4e Mon Sep 17 00:00:00 2001 From: Rain Date: Wed, 3 Sep 2025 11:34:41 -0400 Subject: [PATCH 23/45] Updated to add error handling for returned terraform exec boolean --- internal/plugintest/working_dir.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/plugintest/working_dir.go b/internal/plugintest/working_dir.go index 1c7c4e57b..a340efaab 100644 --- a/internal/plugintest/working_dir.go +++ b/internal/plugintest/working_dir.go @@ -572,6 +572,10 @@ func (wd *WorkingDir) Query(ctx context.Context) ([]tfjson.LogMsg, error) { message, related, err = logEmit.NextMessage() + if related == false && err != nil { + return nil, fmt.Errorf("error no messages found from terraform query command: %w", err) + } + for err != nil || message != nil { message, related, err = logEmit.NextMessage() messages = append(messages, message) From d1418a853ac0a021fe111c90863a4483410788de Mon Sep 17 00:00:00 2001 From: Rain Date: Thu, 4 Sep 2025 13:29:55 -0400 Subject: [PATCH 24/45] Pushing for up to dateness --- helper/resource/query/query_test.go | 13 +- internal/plugintest/working_dir.go | 2 + querycheck/expect_identity.go | 147 +++++++ querycheck/expect_identity_test.go | 346 ++++++++++++++++ querycheck/expect_identity_value.go | 90 +++++ querycheck/expect_identity_value_test.go | 482 +++++++++++++++++++++++ 6 files changed, 1076 insertions(+), 4 deletions(-) create mode 100644 querycheck/expect_identity.go create mode 100644 querycheck/expect_identity_test.go create mode 100644 querycheck/expect_identity_value.go create mode 100644 querycheck/expect_identity_value_test.go diff --git a/helper/resource/query/query_test.go b/helper/resource/query/query_test.go index 3ff0d7fdd..18a37c67d 100644 --- a/helper/resource/query/query_test.go +++ b/helper/resource/query/query_test.go @@ -46,7 +46,7 @@ func TestQuery(t *testing.T) { }), }, Steps: []r.TestStep{ - { + { // config mode step 1, creates something we can list later, need tf file with terraform providers block Config: ` resource "examplecloud_containerette" "primary" { id = "westeurope/somevalue" @@ -54,13 +54,18 @@ func TestQuery(t *testing.T) { name = "somevalue" }`, }, - { + { // Query mode step 2, operates on .tfquery.hcl files (needs tf file with terraform providers block) + // ```provider "examplecloud" {}``` has a slightly different syntax for a .tfquery.hcl file + // provider bock simulates a real providers workflow + // "config" in this case means configuration of the list resource/filters + // run query in terraform itself maybe by moving this provider to the corner provider to check if it works + Query: true, Config: ` - provider "examplecloud" {} + provider "examplecloud" {} list "examplecloud_containerette" "test" { provider = examplecloud - + config { id = "bat" } diff --git a/internal/plugintest/working_dir.go b/internal/plugintest/working_dir.go index a340efaab..8ef990ac2 100644 --- a/internal/plugintest/working_dir.go +++ b/internal/plugintest/working_dir.go @@ -576,6 +576,8 @@ func (wd *WorkingDir) Query(ctx context.Context) ([]tfjson.LogMsg, error) { return nil, fmt.Errorf("error no messages found from terraform query command: %w", err) } + // possibly use iterator pattern here + for err != nil || message != nil { message, related, err = logEmit.NextMessage() messages = append(messages, message) diff --git a/querycheck/expect_identity.go b/querycheck/expect_identity.go new file mode 100644 index 000000000..0ddb09ac6 --- /dev/null +++ b/querycheck/expect_identity.go @@ -0,0 +1,147 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "fmt" + "maps" + "slices" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +var _ QueryCheck = expectIdentity{} + +type expectIdentity struct { + resourceAddress string + identity map[string]knownvalue.Check +} + +// CheckQuery implements the query check logic. +func (e expectIdentity) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { + //var resource map[string]json.RawMessage + //var found bool + //var foundIdentity map[string]json.RawMessage + //var collectionIdentity []string + // + //if req.Query == nil { + // resp.Error = fmt.Errorf("query is nil") + // return + //} + // + //// later check for if the list resource object should be included in the query include_resource + //// check if at least one identity matches the identity I have + //// sort with map ??? + //// if we find the identity remove message from slice and if we are at the end then it wasn't found :( + // + //for _, v := range *req.Query { + // switch idk := v.(type) { + // case tfjson.ListResourceFoundMessage: + // if idk.ListResourceFound.Address == e.resourceAddress { + // found = true + // foundIdentity = idk.ListResourceFound.Identity + // if foundIdentity == e.identity { + // remove foundIdentity from collectionIdentity + // } + // + // if idk.ListResourceFound.ResourceObject != nil { + // resource = idk.ListResourceFound.ResourceObject + // } + // + // } + // default: + // fmt.Printf("List resource not found for query check", v) + // continue + // } + // + // + //} + // + // + ///* if resource == nil { + // resp.Error = fmt.Errorf("%s - Resource not found in query", e.resourceAddress) + // + // return + //}*/ + // + ///* if len(foundIdentity.IdentityValues) == 0 { + // resp.Error = fmt.Errorf("%s - Identity not found in query. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.12+)", e.resourceAddress) + // + // return + //}*/ + // + ///* if len(resource.IdentityValues) != len(e.identity) { + // deltaMsg := "" + // if len(resource.IdentityValues) > len(e.identity) { + // deltaMsg = createDeltaString(resource.IdentityValues, e.identity, "actual identity has extra attribute(s): ") + // } else { + // deltaMsg = createDeltaString(e.identity, resource.IdentityValues, "actual identity is missing attribute(s): ") + // } + // + // resp.Error = fmt.Errorf("%s - Expected %d attribute(s) in the actual identity object, got %d attribute(s): %s", e.resourceAddress, len(e.identity), len(resource.IdentityValues), deltaMsg) + // return + //}*/ + // + //var keys []string + // + //for k := range e.identity { + // keys = append(keys, k) + //} + // + //sort.SliceStable(keys, func(i, j int) bool { + // return keys[i] < keys[j] + //}) + // + //for _, k := range keys { + // actualIdentityVal, ok := resource.IdentityValues[k] + // + // if !ok { + // resp.Error = fmt.Errorf("%s - missing attribute %q in actual identity object", e.resourceAddress, k) + // return + // } + // + // if err := e.identity[k].CheckValue(actualIdentityVal); err != nil { + // resp.Error = fmt.Errorf("%s - %q identity attribute: %s", e.resourceAddress, k, err) + // return + // } + //} + return +} + +// ExpectIdentity returns a query check that asserts that the identity at the given resource matches a known object, where each +// map key represents an identity attribute name. The identity in query must exactly match the given object and any missing/extra +// attributes will raise a diagnostic. +// +// This query check can only be used with managed resources that support resource identity. Resource identity is only supported in Terraform v1.12+ +func ExpectIdentity(resourceAddress string, identity map[string]knownvalue.Check) QueryCheck { + return expectIdentity{ + resourceAddress: resourceAddress, + identity: identity, + } +} + +// createDeltaString prints the map keys that are present in mapA and not present in mapB +func createDeltaString[T any, V any](mapA map[string]T, mapB map[string]V, msgPrefix string) string { + deltaMsg := "" + + deltaMap := make(map[string]T, len(mapA)) + maps.Copy(deltaMap, mapA) + for key := range mapB { + delete(deltaMap, key) + } + + deltaKeys := slices.Sorted(maps.Keys(deltaMap)) + + for i, k := range deltaKeys { + if i == 0 { + deltaMsg += msgPrefix + } else { + deltaMsg += ", " + } + deltaMsg += fmt.Sprintf("%q", k) + } + + return deltaMsg +} diff --git a/querycheck/expect_identity_test.go b/querycheck/expect_identity_test.go new file mode 100644 index 000000000..600005145 --- /dev/null +++ b/querycheck/expect_identity_test.go @@ -0,0 +1,346 @@ +// Copyright (c) HashiCorp, Inc. +// 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/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/querycheck" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestExpectIdentity_CheckQuery_ResourceNotFound(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentity( + "examplecloud_thing.two", + 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), + }, + ), + }, + ), + }, + ExpectError: regexp.MustCompile("examplecloud_thing.two - Resource not found in query"), + }, + }, + }) +} + +func TestExpectIdentity_CheckQuery_No_Terraform_Identity_Support(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + tfversion.SkipAbove(tfversion.Version1_11_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + // Resource support identity, but the Terraform versions running will not. + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentity( + "examplecloud_thing.one", + 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), + }, + ), + }, + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in query. Either the resource ` + + `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, + ), + }, + }, + }) +} + +func TestExpectIdentity_CheckQuery_No_Identity(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + // Resource does not support identity + "examplecloud": examplecloudProviderNoIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentity( + "examplecloud_thing.one", + 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), + }, + ), + }, + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in query. Either the resource ` + + `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, + ), + }, + }, + }) +} + +func TestExpectIdentity_CheckQuery(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + }, + { + Query: true, + Config: ` + provider "examplecloud" {} + list "examplecloud_containerette" "test" { + provider = examplecloud + + config { + id = "bat" + } + }`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentity( + "examplecloud_thing.one", + 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), + }, + ), + }, + ), + }, + }, + }, + }) +} + +func TestExpectIdentity_CheckQuery_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + + querycheck.ExpectIdentity( + "examplecloud_thing.one", + map[string]knownvalue.Check{ + "id": knownvalue.Bool(true), + "list_of_numbers": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Int64Exact(1), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(4), + }, + ), + }, + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - "id" identity attribute: expected bool value for Bool check, got: string`), + }, + }, + }) +} + +func TestExpectIdentity_CheckQuery_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + + querycheck.ExpectIdentity( + "examplecloud_thing.one", + map[string]knownvalue.Check{ + "id": knownvalue.StringExact("321-id"), + "list_of_numbers": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Int64Exact(1), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(4), + }, + ), + }, + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - "id" identity attribute: expected value 321-id for StringExact check, got: id-123`), + }, + }, + }) +} + +func TestExpectIdentity_CheckQuery_ExtraAttribute(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + + querycheck.ExpectIdentity( + "examplecloud_thing.one", + map[string]knownvalue.Check{ + "id": knownvalue.StringExact("321-id"), + }, + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Expected 1 attribute\(s\) in the actual identity object, got 2 attribute\(s\): actual identity has extra attribute\(s\): "list_of_numbers"`), + }, + }, + }) +} + +func TestExpectIdentity_CheckQuery_MissingAttribute(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + + querycheck.ExpectIdentity( + "examplecloud_thing.one", + 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), + }, + ), + }, + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Expected 3 attribute\(s\) in the actual identity object, got 2 attribute\(s\): actual identity is missing attribute\(s\): "nonexistent_attr"`), + }, + }, + }) +} + +func TestExpectIdentity_CheckQuery_MismatchedAttribute(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentity( + "examplecloud_thing.one", + map[string]knownvalue.Check{ + "not_id": knownvalue.StringExact("id-123"), + "list_of_numbers": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Int64Exact(1), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(4), + }, + ), + }, + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - missing attribute "not_id" in actual identity object`), + }, + }, + }) +} diff --git a/querycheck/expect_identity_value.go b/querycheck/expect_identity_value.go new file mode 100644 index 000000000..9eba2f145 --- /dev/null +++ b/querycheck/expect_identity_value.go @@ -0,0 +1,90 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +var _ QueryCheck = expectIdentityValue{} + +type expectIdentityValue struct { + resourceAddress string + attributePath tfjsonpath.Path + identityValue knownvalue.Check +} + +// CheckQuery implements the query check logic. +func (e expectIdentityValue) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { + // var resource *tfjson.QueryResource + + if req.Query == nil { + resp.Error = fmt.Errorf("query is nil") + + return + } + + /* if req.Query.Values == nil { + resp.Error = fmt.Errorf("query does not contain any query values") + + return + } + + if req.Query.Values.RootModule == nil { + resp.Error = fmt.Errorf("query does not contain a root module") + + return + } + + for _, r := range req.Query.Values.RootModule.Resources { + if e.resourceAddress == r.Address { + resource = r + + break + } + } + + if resource == nil { + resp.Error = fmt.Errorf("%s - Resource not found in query", e.resourceAddress) + + return + } + + if resource.IdentitySchemaVersion == nil || len(resource.IdentityValues) == 0 { + resp.Error = fmt.Errorf("%s - Identity not found in query. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.12+)", e.resourceAddress) + + return + } + + result, err := tfjsonpath.Traverse(resource.IdentityValues, e.attributePath) + + if err != nil { + resp.Error = err + + return + }*/ + + /* if err := e.identityValue.CheckValue(result); err != nil { + resp.Error = fmt.Errorf("error checking identity value for attribute at path: %s.%s, err: %s", e.resourceAddress, e.attributePath.String(), err) + + return + }*/ + return +} + +// ExpectIdentityValue returns a query check that asserts that the specified identity attribute at the given resource +// matches a known value. This query check can only be used with managed resources that support resource identity. +// +// Resource identity is only supported in Terraform v1.12+ +func ExpectIdentityValue(resourceAddress string, attributePath tfjsonpath.Path, identityValue knownvalue.Check) QueryCheck { + return expectIdentityValue{ + resourceAddress: resourceAddress, + attributePath: attributePath, + identityValue: identityValue, + } +} diff --git a/querycheck/expect_identity_value_test.go b/querycheck/expect_identity_value_test.go new file mode 100644 index 000000000..c284b25ad --- /dev/null +++ b/querycheck/expect_identity_value_test.go @@ -0,0 +1,482 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck_test + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/list" + + 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/internal/testing/testsdk/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/querycheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestExpectIdentityValue_CheckQuery_ResourceNotFound(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentityValue( + "examplecloud_thing.two", + tfjsonpath.New("id"), + knownvalue.StringExact("id-123"), + ), + }, + ExpectError: regexp.MustCompile("examplecloud_thing.two - Resource not found in query"), + }, + }, + }) +} + +func TestExpectIdentityValue_CheckQuery_No_Terraform_Identity_Support(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + tfversion.SkipAbove(tfversion.Version1_11_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + // Resource support identity, but the Terraform versions running will not. + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("id"), + knownvalue.StringExact("id-123"), + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in query. Either the resource ` + + `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, + ), + }, + }, + }) +} + +func TestExpectIdentityValue_CheckQuery_No_Identity(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + // Resource does not support identity + "examplecloud": examplecloudProviderNoIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("id"), + knownvalue.StringExact("id-123"), + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in query. Either the resource ` + + `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, + ), + }, + }, + }) +} + +func TestExpectIdentityValue_CheckQuery_String(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("id"), + knownvalue.StringExact("id-123")), + }, + }, + }, + }) +} + +func TestExpectIdentityValue_CheckQuery_String_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("id"), + knownvalue.Bool(true)), + }, + ExpectError: regexp.MustCompile("expected bool value for Bool check, got: string"), + }, + }, + }) +} + +func TestExpectIdentityValue_CheckQuery_String_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("id"), + knownvalue.StringExact("321-id")), + }, + ExpectError: regexp.MustCompile("expected value 321-id for StringExact check, got: id-123"), + }, + }, + }) +} + +func TestExpectIdentityValue_CheckQuery_List(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("list_of_numbers").AtSliceIndex(0), + knownvalue.Int64Exact(1), + ), + querycheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("list_of_numbers").AtSliceIndex(1), + knownvalue.Int64Exact(2), + ), + querycheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("list_of_numbers").AtSliceIndex(2), + knownvalue.Int64Exact(3), + ), + querycheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("list_of_numbers").AtSliceIndex(3), + knownvalue.Int64Exact(4), + ), + }, + }, + }, + }) +} + +func TestExpectIdentityValue_CheckQuery_List_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {} + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("list_of_numbers"), + knownvalue.MapExact(map[string]knownvalue.Check{}), + ), + }, + ExpectError: regexp.MustCompile(`expected map\[string\]any value for MapExact check, got: \[\]interface {}`), + }, + }, + }) +} + +func TestExpectIdentityValue_CheckQuery_List_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("list_of_numbers"), + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.Int64Exact(4), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(1), + }), + ), + }, + ExpectError: regexp.MustCompile(`list element index 0: expected value 4 for Int64Exact check, got: 1`), + }, + }, + }) +} + +func examplecloudProviderWithResourceIdentity() func() (tfprotov6.ProviderServer, error) { + return providerserver.NewProviderServer(testprovider.Provider{ + ListResources: map[string]testprovider.ListResource{ + "examplecloud_containerette": { + SchemaResponse: &list.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + }, + }, + }, + }, + }, + ListResultsStream: &list.ListResultsStream{ + Results: func(push func(list.ListResult) bool) { + + }, + }, + }, + }, + Resources: map[string]testprovider.Resource{ + "examplecloud_thing": { + CreateResponse: &resource.CreateResponse{ + NewQuery: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + "id": tftypes.String, + "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "test value"), + "id": tftypes.NewValue(tftypes.String, "id-123"), + "list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 1), + tftypes.NewValue(tftypes.Number, 2), + tftypes.NewValue(tftypes.Number, 3), + tftypes.NewValue(tftypes.Number, 4), + }, + ), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "id-123"), + "list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 1), + tftypes.NewValue(tftypes.Number, 2), + tftypes.NewValue(tftypes.Number, 3), + tftypes.NewValue(tftypes.Number, 4), + }, + ), + }, + )), + }, + ReadResponse: &resource.ReadResponse{ + NewQuery: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + "id": tftypes.String, + "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "test value"), + "id": tftypes.NewValue(tftypes.String, "id-123"), + "list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 1), + tftypes.NewValue(tftypes.Number, 2), + tftypes.NewValue(tftypes.Number, 3), + tftypes.NewValue(tftypes.Number, 4), + }, + ), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "id-123"), + "list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 1), + tftypes.NewValue(tftypes.Number, 2), + tftypes.NewValue(tftypes.Number, 3), + tftypes.NewValue(tftypes.Number, 4), + }, + ), + }, + )), + }, + IdentitySchemaResponse: &resource.IdentitySchemaResponse{ + Schema: &tfprotov6.ResourceIdentitySchema{ + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + RequiredForImport: true, + }, + { + Name: "list_of_numbers", + Type: tftypes.List{ElementType: tftypes.Number}, + OptionalForImport: true, + }, + }, + }, + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "name", + Type: tftypes.String, + Computed: true, + }, + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + { + Name: "list_of_numbers", + Type: tftypes.List{ElementType: tftypes.Number}, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }) +} + +func examplecloudProviderNoIdentity() func() (tfprotov6.ProviderServer, error) { + return providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_thing": { + CreateResponse: &resource.CreateResponse{ + NewQuery: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "test value"), + }, + ), + }, + ReadResponse: &resource.ReadResponse{ + NewQuery: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "test value"), + }, + ), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "name", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }) +} From 22e003c0645d71918b83973832697889c39cc5a0 Mon Sep 17 00:00:00 2001 From: Rain Date: Thu, 4 Sep 2025 14:17:08 -0400 Subject: [PATCH 25/45] updating providerserver.go --- internal/plugintest/working_dir.go | 2 +- .../testsdk/providerserver/providerserver.go | 24 +++++++++---------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/internal/plugintest/working_dir.go b/internal/plugintest/working_dir.go index 8ef990ac2..8d64152c1 100644 --- a/internal/plugintest/working_dir.go +++ b/internal/plugintest/working_dir.go @@ -591,7 +591,7 @@ func (wd *WorkingDir) Query(ctx context.Context) ([]tfjson.LogMsg, error) { return messages, nil - // do a type onversion to list start data or list found message + // do a type conversion to list start data or list found message //for _, line := range bufSplit { // if line == "" { diff --git a/internal/testing/testsdk/providerserver/providerserver.go b/internal/testing/testsdk/providerserver/providerserver.go index a55b5c5c1..68499ff12 100644 --- a/internal/testing/testsdk/providerserver/providerserver.go +++ b/internal/testing/testsdk/providerserver/providerserver.go @@ -1046,8 +1046,10 @@ func (s ProviderServer) ListResource(ctx context.Context, req *tfprotov6.ListRes identitySchemaReq := resource.IdentitySchemaRequest{} identitySchemaResp := &resource.IdentitySchemaResponse{} - r, _ := ProviderResource(s.Provider, req.TypeName) - // TODO: diag + r, err := ProviderResource(s.Provider, req.TypeName) + if err != nil { + return nil, fmt.Errorf("failed to retrieve resource: %v", err) + } r.IdentitySchema(ctx, identitySchemaReq, identitySchemaResp) results := func(push func(tfprotov6.ListResourceResult) bool) { @@ -1057,17 +1059,13 @@ func (s ProviderServer) ListResource(ctx context.Context, req *tfprotov6.ListRes return } - identityData := tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), - }, - ) - identity, _ := IdentityValuetoDynamicValue(identitySchemaResp.Schema, identityData) // TODO: diag + var identityData tftypes.Value + identity, diag := IdentityValuetoDynamicValue(identitySchemaResp.Schema, identityData) + if diag != nil { + push(tfprotov6.ListResourceResult{Diagnostics: []*tfprotov6.Diagnostic{diag}}) + return + } + push(tfprotov6.ListResourceResult{ Identity: &tfprotov6.ResourceIdentityData{ IdentityData: identity, From 53ecea9ee22f84e3ad4df8825401585d2157c91c Mon Sep 17 00:00:00 2001 From: Rain Date: Tue, 9 Sep 2025 18:09:27 -0400 Subject: [PATCH 26/45] Updated the query test to return actual identity and start to implement List --- .../resource/query/examplecloud_list_test.go | 82 ++++++++++ helper/resource/query/query_checks.go | 2 +- helper/resource/query/query_test.go | 51 +++--- helper/resource/testing_new.go | 8 +- internal/plugintest/working_dir.go | 51 ------ .../testing/testprovider/list_resource.go | 2 + .../testing/testsdk/list/list_resource.go | 20 +++ .../testsdk/providerserver/providerserver.go | 135 +++++++++++++--- querycheck/expect_identity.go | 151 ++++++++---------- 9 files changed, 324 insertions(+), 178 deletions(-) create mode 100644 helper/resource/query/examplecloud_list_test.go diff --git a/helper/resource/query/examplecloud_list_test.go b/helper/resource/query/examplecloud_list_test.go new file mode 100644 index 000000000..08799567b --- /dev/null +++ b/helper/resource/query/examplecloud_list_test.go @@ -0,0 +1,82 @@ +package query_test + +import ( + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/list" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" +) + +func examplecloudListResource() testprovider.ListResource { + return testprovider.ListResource{ + IncludeResource: true, + SchemaResponse: &list.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + ListResultsStream: &list.ListResultsStream{ + Results: func(push func(list.ListResult) bool) { + push(list.ListResult{ + Resource: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "somevalue"), + }, + )), + Identity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue1"), + }, + )), + }) + push(list.ListResult{ + Identity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue2"), + }, + )), + }) + push(list.ListResult{ + Identity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue3"), + }, + )), + }) + }, + }, + } +} diff --git a/helper/resource/query/query_checks.go b/helper/resource/query/query_checks.go index 20de966e9..75a3d5aa6 100644 --- a/helper/resource/query/query_checks.go +++ b/helper/resource/query/query_checks.go @@ -13,7 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-testing/querycheck" ) -func runQueryChecks(ctx context.Context, t testing.T, query *[]tfjson.LogMsg, queryChecks []querycheck.QueryCheck) error { +func RunQueryChecks(ctx context.Context, t testing.T, query *[]tfjson.LogMsg, queryChecks []querycheck.QueryCheck) error { t.Helper() var result []error diff --git a/helper/resource/query/query_test.go b/helper/resource/query/query_test.go index 18a37c67d..cb8801766 100644 --- a/helper/resource/query/query_test.go +++ b/helper/resource/query/query_test.go @@ -9,8 +9,9 @@ import ( "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/list" "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/tfversion" ) @@ -24,21 +25,7 @@ func TestQuery(t *testing.T) { ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ ListResources: map[string]testprovider.ListResource{ - "examplecloud_containerette": { - SchemaResponse: &list.SchemaResponse{ - Schema: &tfprotov6.Schema{ - Block: &tfprotov6.SchemaBlock{ - Attributes: []*tfprotov6.SchemaAttribute{ - ComputedStringAttribute("id"), - }, - }, - }, - }, - ListResultsStream: &list.ListResultsStream{ - Results: func(push func(list.ListResult) bool) { - }, - }, - }, + "examplecloud_containerette": examplecloudListResource(), }, Resources: map[string]testprovider.Resource{ "examplecloud_containerette": examplecloudResource(), @@ -67,9 +54,37 @@ func TestQuery(t *testing.T) { provider = examplecloud config { - id = "bat" + id = "westeurope/somevalue" } - }`, + } + list "examplecloud_containerette" "test2" { + provider = examplecloud + + config { + id = "foo" + } + } + `, + ConfigQueryChecks: []querycheck.QueryCheck{ + querycheck.ExpectIdentity("examplecloud_containerette.test", map[string]knownvalue.Check{ + "id": knownvalue.StringExact("westeurope/somevalue1"), + }), + querycheck.ExpectIdentity("examplecloud_containerette.test", map[string]knownvalue.Check{ + "id": knownvalue.StringExact("westeurope/somevalue2"), + }), + querycheck.ExpectIdentity("examplecloud_containerette.test", map[string]knownvalue.Check{ + "id": knownvalue.StringExact("westeurope/somevalue3"), + }), + querycheck.ExpectIdentity("examplecloud_containerette.test2", map[string]knownvalue.Check{ + "id": knownvalue.StringExact("westeurope/somevalue1"), + }), + querycheck.ExpectIdentity("examplecloud_containerette.test2", map[string]knownvalue.Check{ + "id": knownvalue.StringExact("westeurope/somevalue2"), + }), + querycheck.ExpectIdentity("examplecloud_containerette.test2", map[string]knownvalue.Check{ + "id": knownvalue.StringExact("westeurope/somevalue3"), + }), + }, }, }, }) diff --git a/helper/resource/testing_new.go b/helper/resource/testing_new.go index d06dd2368..6c01ca3b4 100644 --- a/helper/resource/testing_new.go +++ b/helper/resource/testing_new.go @@ -15,6 +15,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/go-version" tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-plugin-testing/helper/resource/query" "github.com/mitchellh/go-testing-interface" "github.com/hashicorp/terraform-plugin-testing/config" @@ -376,16 +377,21 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest t.Fatalf("Step %d/%d error running init: %s", stepNumber, len(c.Steps), err) } - var queryOut any + var queryOut []tfjson.LogMsg err = runProviderCommand(ctx, t, wd, providers, func() error { var err error queryOut, err = wd.Query(ctx) + return err }) if err != nil { fmt.Printf("Step %d/%d Query Output:\n%s\n", stepNumber, len(c.Steps), queryOut) t.Fatalf("Step %d/%d error running query: %s", stepNumber, len(c.Steps), err) } + err = query.RunQueryChecks(ctx, t, &queryOut, step.ConfigQueryChecks) + if err != nil { + t.Fatalf("Step %d/%d error running query checks: %s", stepNumber, len(c.Steps), err) + } fmt.Printf("Step %d/%d Query Output:\n%s\n", stepNumber, len(c.Steps), queryOut) diff --git a/internal/plugintest/working_dir.go b/internal/plugintest/working_dir.go index 8d64152c1..935155f8a 100644 --- a/internal/plugintest/working_dir.go +++ b/internal/plugintest/working_dir.go @@ -533,11 +533,6 @@ func (wd *WorkingDir) Query(ctx context.Context) ([]tfjson.LogMsg, error) { var messages []tfjson.LogMsg - // Query the provider using the Terraform CLI function - //var buffer bytes.Buffer - - // var unmarshalled map[string]any - // This returns a slice of log messages but is not expressed as a valid JSON array, so we're going to convert the // buffer to a string, split this on new line then process each line individually since we're only interested in // the list/query log messages @@ -550,23 +545,6 @@ func (wd *WorkingDir) Query(ctx context.Context) ([]tfjson.LogMsg, error) { return nil, fmt.Errorf("error running terraform query command: %w", err) } - //bufSplit := strings.Split(string(buffer.Bytes()), "\n") - - /*for _, line := range bufSplit { - if line == "" { - continue - } - err := json.Unmarshal([]byte(line), &unmarshalled) - if err != nil { - return nil, err - } - - traverse, _ := tfjsonpath.Traverse(unmarshalled, tfjsonpath.New("list_resource_found")) - if traverse != nil { - return traverse, nil - } - }*/ - var message tfjson.LogMsg var related bool @@ -590,35 +568,6 @@ func (wd *WorkingDir) Query(ctx context.Context) ([]tfjson.LogMsg, error) { logging.HelperResourceTrace(ctx, "Called Terraform CLI providers query command") return messages, nil - - // do a type conversion to list start data or list found message - - //for _, line := range bufSplit { - // if line == "" { - // continue - // } - // d := json.NewDecoder(bytes.NewReader([]byte(line))) - // - // mt := msgType{} - // err := d.Decode(&mt) - // if err != nil { - // return nil, err - // } - // - // msg, err := unmarshalResult(mt.Type, []byte(line)) - // if err != nil { - // // TODO - // } - // - // if msg != nil { - // returned = append(returned, *msg) - // } - //} - - //returned := make([]string, len(results)) - //for i, r := range results { - // returned[i] = r.Address - //} } // Taken from https://github.com/hashicorp/terraform-json/pull/169/ diff --git a/internal/testing/testprovider/list_resource.go b/internal/testing/testprovider/list_resource.go index f57c95a4a..630723b87 100644 --- a/internal/testing/testprovider/list_resource.go +++ b/internal/testing/testprovider/list_resource.go @@ -12,6 +12,7 @@ import ( var _ list.ListResource = ListResource{} type ListResource struct { + IncludeResource bool SchemaResponse *list.SchemaResponse ListResultsStream *list.ListResultsStream ValidateListConfigResponse *list.ValidateListConfigResponse @@ -24,5 +25,6 @@ func (r ListResource) Schema(ctx context.Context, req list.SchemaRequest, resp * } } func (r ListResource) List(ctx context.Context, req list.ListRequest, stream *list.ListResultsStream) { + req.IncludeResource = r.IncludeResource stream.Results = r.ListResultsStream.Results } diff --git a/internal/testing/testsdk/list/list_resource.go b/internal/testing/testsdk/list/list_resource.go index fb5d7baf7..1660d3426 100644 --- a/internal/testing/testsdk/list/list_resource.go +++ b/internal/testing/testsdk/list/list_resource.go @@ -8,6 +8,7 @@ import ( "iter" "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" ) type ListResource interface { @@ -16,6 +17,21 @@ type ListResource interface { } type ListRequest struct { + TypeName string + // Config is the configuration the user supplied for listing resource + // instances. + Config tftypes.Value + + // IncludeResource indicates whether the provider should populate the + // [ListResult.Resource] field. + IncludeResource bool + + // Limit specifies the maximum number of results that Terraform is + // expecting. + Limit int64 + + ResourceSchema *tfprotov6.Schema + ResourceIdentitySchema *tfprotov6.ResourceIdentitySchema } type ListResultsStream struct { @@ -23,6 +39,10 @@ type ListResultsStream struct { } type ListResult struct { + DisplayName string + Identity *tftypes.Value + Resource *tftypes.Value + Diagnostics []*tfprotov6.Diagnostic } type ValidateListConfigResponse struct { diff --git a/internal/testing/testsdk/providerserver/providerserver.go b/internal/testing/testsdk/providerserver/providerserver.go index 68499ff12..4748102d1 100644 --- a/internal/testing/testsdk/providerserver/providerserver.go +++ b/internal/testing/testsdk/providerserver/providerserver.go @@ -7,6 +7,7 @@ import ( "context" "errors" "fmt" + "iter" "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-go/tftypes" @@ -1040,7 +1041,8 @@ func (s ProviderServer) ValidateEphemeralResourceConfig(ctx context.Context, req } func (s ProviderServer) ListResource(ctx context.Context, req *tfprotov6.ListResourceRequest) (*tfprotov6.ListResourceServerStream, error) { - resp := &tfprotov6.ListResourceServerStream{} + resultStream := &tfprotov6.ListResourceServerStream{} + respStream := &list.ListResultsStream{} // Copy over identity if it's supported identitySchemaReq := resource.IdentitySchemaRequest{} @@ -1051,32 +1053,125 @@ func (s ProviderServer) ListResource(ctx context.Context, req *tfprotov6.ListRes return nil, fmt.Errorf("failed to retrieve resource: %v", err) } r.IdentitySchema(ctx, identitySchemaReq, identitySchemaResp) + if identitySchemaResp.Diagnostics != nil && len(identitySchemaResp.Diagnostics) > 0 { + return nil, fmt.Errorf("failed to retrieve resource schema: %v", identitySchemaResp.Diagnostics) + } - results := func(push func(tfprotov6.ListResourceResult) bool) { - _, diag := ProviderListResource(s.Provider, req.TypeName) - if diag != nil { - push(tfprotov6.ListResourceResult{Diagnostics: []*tfprotov6.Diagnostic{diag}}) - return - } + listresource, diag := ProviderListResource(s.Provider, req.TypeName) + if diag != nil { + return nil, fmt.Errorf("failed to retrieve resource identity schema: %v", err) + } - var identityData tftypes.Value - identity, diag := IdentityValuetoDynamicValue(identitySchemaResp.Schema, identityData) - if diag != nil { - push(tfprotov6.ListResourceResult{Diagnostics: []*tfprotov6.Diagnostic{diag}}) - return - } + schemaReq := list.SchemaRequest{} + schemaResp := &list.SchemaResponse{} - push(tfprotov6.ListResourceResult{ - Identity: &tfprotov6.ResourceIdentityData{ - IdentityData: identity, - }, - }) + listresource.Schema(ctx, schemaReq, schemaResp) + if schemaResp.Diagnostics != nil && len(schemaResp.Diagnostics) > 0 { + return nil, fmt.Errorf("failed to retrieve resource schema: %v", schemaResp.Diagnostics) } - resp.Results = results - return resp, nil + listReq := list.ListRequest{ + TypeName: req.TypeName, + IncludeResource: req.IncludeResource, + Limit: req.Limit, + ResourceSchema: schemaResp.Schema, + } + + listReq.Config, diag = DynamicValueToValue(schemaResp.Schema, req.Config) + if diag != nil { + return nil, fmt.Errorf("failed to convert config to value: %v", err) + } + + if identitySchemaResp.Schema != nil { + listReq.ResourceIdentitySchema = identitySchemaResp.Schema + } + + listresource.List(ctx, listReq, respStream) + + // If the provider returned a nil results stream, we return an empty stream. + if respStream.Results == nil { + resultStream.Results = func(push func(result tfprotov6.ListResourceResult) bool) {} + } + + resultStream.Results = processListResults(listReq, respStream.Results) + return resultStream, nil } func (s ProviderServer) ValidateListResourceConfig(ctx context.Context, req *tfprotov6.ValidateListResourceConfigRequest) (*tfprotov6.ValidateListResourceConfigResponse, error) { return &tfprotov6.ValidateListResourceConfigResponse{}, nil } + +func processListResults(req list.ListRequest, stream iter.Seq[list.ListResult]) iter.Seq[tfprotov6.ListResourceResult] { + return func(push func(tfprotov6.ListResourceResult) bool) { + for result := range stream { + if !push(processListResult(req, result)) { + return + } + } + } +} + +// processListResult validates the content of a list.ListResult and returns a +// ListResult +func processListResult(req list.ListRequest, result list.ListResult) tfprotov6.ListResourceResult { + var listResourceResult tfprotov6.ListResourceResult + listResourceResult.Diagnostics = []*tfprotov6.Diagnostic{} + var diag *tfprotov6.Diagnostic + + // Allow any non-error diags to pass through + if len(result.Diagnostics) > 0 && result.DisplayName == "" && result.Identity == nil && result.Resource == nil { + return tfprotov6.ListResourceResult{ + Diagnostics: result.Diagnostics, + } + } + + if result.Diagnostics != nil { + return tfprotov6.ListResourceResult{ + Diagnostics: result.Diagnostics, + } + } + + if result.Identity == nil { + return tfprotov6.ListResourceResult{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Incomplete List Result", + Detail: "When listing resources, an implementation issue was found. " + + "This is always a problem with the provider. Please report this to the provider developers.\n\n" + + "The \"Identity\" field is nil.\n\n", + }, + }, + } + } + + if req.IncludeResource { + if result.Resource == nil { + listResourceResult.Diagnostics = append(listResourceResult.Diagnostics, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "Incomplete List Result", + Detail: "When listing resources, an implementation issue was found. " + + "This is always a problem with the provider. Please report this to the provider developers.\n\n" + + "The \"IncludeResource\" field in the ListRequest is true, but the \"Resource\" field in the ListResult is nil.\n\n", + }) + } + + listResourceResult.Resource, diag = ValuetoDynamicValue(req.ResourceSchema, *result.Resource) + listResourceResult.Diagnostics = append(listResourceResult.Diagnostics, diag) + return listResourceResult + + } + listResourceResult.Identity = &tfprotov6.ResourceIdentityData{} + + if result.Identity != nil { + listResourceResult.Identity.IdentityData, diag = IdentityValuetoDynamicValue(req.ResourceIdentitySchema, *result.Identity) + if diag != nil { + listResourceResult.Diagnostics = append(listResourceResult.Diagnostics, diag) + return listResourceResult + } + } + + listResourceResult.DisplayName = result.DisplayName + + return listResourceResult +} diff --git a/querycheck/expect_identity.go b/querycheck/expect_identity.go index 0ddb09ac6..ccfe273ac 100644 --- a/querycheck/expect_identity.go +++ b/querycheck/expect_identity.go @@ -5,10 +5,12 @@ package querycheck import ( "context" + "encoding/json" "fmt" "maps" "slices" + tfjson "github.com/hashicorp/terraform-json" "github.com/hashicorp/terraform-plugin-testing/knownvalue" ) @@ -16,97 +18,72 @@ var _ QueryCheck = expectIdentity{} type expectIdentity struct { resourceAddress string - identity map[string]knownvalue.Check + check map[string]knownvalue.Check } // CheckQuery implements the query check logic. func (e expectIdentity) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { - //var resource map[string]json.RawMessage - //var found bool - //var foundIdentity map[string]json.RawMessage - //var collectionIdentity []string - // - //if req.Query == nil { - // resp.Error = fmt.Errorf("query is nil") - // return - //} - // - //// later check for if the list resource object should be included in the query include_resource - //// check if at least one identity matches the identity I have - //// sort with map ??? - //// if we find the identity remove message from slice and if we are at the end then it wasn't found :( - // - //for _, v := range *req.Query { - // switch idk := v.(type) { - // case tfjson.ListResourceFoundMessage: - // if idk.ListResourceFound.Address == e.resourceAddress { - // found = true - // foundIdentity = idk.ListResourceFound.Identity - // if foundIdentity == e.identity { - // remove foundIdentity from collectionIdentity - // } - // - // if idk.ListResourceFound.ResourceObject != nil { - // resource = idk.ListResourceFound.ResourceObject - // } - // - // } - // default: - // fmt.Printf("List resource not found for query check", v) - // continue - // } - // - // - //} - // - // - ///* if resource == nil { - // resp.Error = fmt.Errorf("%s - Resource not found in query", e.resourceAddress) - // - // return - //}*/ - // - ///* if len(foundIdentity.IdentityValues) == 0 { - // resp.Error = fmt.Errorf("%s - Identity not found in query. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.12+)", e.resourceAddress) - // - // return - //}*/ - // - ///* if len(resource.IdentityValues) != len(e.identity) { - // deltaMsg := "" - // if len(resource.IdentityValues) > len(e.identity) { - // deltaMsg = createDeltaString(resource.IdentityValues, e.identity, "actual identity has extra attribute(s): ") - // } else { - // deltaMsg = createDeltaString(e.identity, resource.IdentityValues, "actual identity is missing attribute(s): ") - // } - // - // resp.Error = fmt.Errorf("%s - Expected %d attribute(s) in the actual identity object, got %d attribute(s): %s", e.resourceAddress, len(e.identity), len(resource.IdentityValues), deltaMsg) - // return - //}*/ - // - //var keys []string - // - //for k := range e.identity { - // keys = append(keys, k) - //} - // - //sort.SliceStable(keys, func(i, j int) bool { - // return keys[i] < keys[j] - //}) - // - //for _, k := range keys { - // actualIdentityVal, ok := resource.IdentityValues[k] - // - // if !ok { - // resp.Error = fmt.Errorf("%s - missing attribute %q in actual identity object", e.resourceAddress, k) - // return - // } - // - // if err := e.identity[k].CheckValue(actualIdentityVal); err != nil { - // resp.Error = fmt.Errorf("%s - %q identity attribute: %s", e.resourceAddress, k, err) - // return - // } + var resource map[string]json.RawMessage + var foundIdentities []map[string]json.RawMessage + + if req.Query == nil { + resp.Error = fmt.Errorf("query is nil") + return + } + + // later check for if the list resource object should be included in the query include_resource + // if we find the identity remove message from slice and if we are at the end then it wasn't found :( + + for _, v := range *req.Query { + switch idk := v.(type) { + case tfjson.ListResourceFoundMessage: + if idk.ListResourceFound.Address == e.resourceAddress { + foundIdentities = append(foundIdentities, idk.ListResourceFound.Identity) + } + default: + continue + } + } + + if resource == nil { + resp.Error = fmt.Errorf("%s - Resource not found in query", e.resourceAddress) + + return + } + + if len(foundIdentities) == 0 { + resp.Error = fmt.Errorf("%s - Identity not found in query. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.12+)", e.resourceAddress) + + return + } + + //if len(resource.IdentityValues) != len(e.identity) { + //deltaMsg := "" + //if len(resource.IdentityValues) > len(e.identity) { + // deltaMsg = createDeltaString(resource.IdentityValues, e.identity, "actual identity has extra attribute(s): ") + //} else { + // deltaMsg = createDeltaString(e.identity, resource.IdentityValues, "actual identity is missing attribute(s): ") + //} + + //resp.Error = fmt.Errorf("%s - Expected %d attribute(s) in the actual identity object, got %d attribute(s): %s", e.resourceAddress, len(e.identity), len(resource.IdentityValues), deltaMsg) //} + + for _, resultIdentity := range foundIdentities { + + for attribute := range e.check { + var val any + var ok bool + if val, ok = resultIdentity[attribute]; !ok { + resp.Error = fmt.Errorf("%s - expected attribute %q not in actual identity object", e.resourceAddress, attribute) + return + } + + if err := e.check[attribute].CheckValue(val); err != nil { + resp.Error = fmt.Errorf("%s - %q identity attribute: %s", e.resourceAddress, e.check, err) + return + } + } + } return } @@ -118,7 +95,7 @@ func (e expectIdentity) CheckQuery(ctx context.Context, req CheckQueryRequest, r func ExpectIdentity(resourceAddress string, identity map[string]knownvalue.Check) QueryCheck { return expectIdentity{ resourceAddress: resourceAddress, - identity: identity, + check: identity, } } From 5cbd9f7f911a2f17a3929043d8b41b811aa0a0fe Mon Sep 17 00:00:00 2001 From: Rain Date: Tue, 9 Sep 2025 19:11:48 -0400 Subject: [PATCH 27/45] fixed test so expect_identity.go runs --- helper/resource/query/query_test.go | 16 ++-- helper/resource/testing_new.go | 2 +- internal/plugintest/working_dir.go | 42 ---------- querycheck/expect_identity.go | 115 +++++++++++++++++++++------- 4 files changed, 98 insertions(+), 77 deletions(-) diff --git a/helper/resource/query/query_test.go b/helper/resource/query/query_test.go index cb8801766..2ba76584d 100644 --- a/helper/resource/query/query_test.go +++ b/helper/resource/query/query_test.go @@ -65,24 +65,24 @@ func TestQuery(t *testing.T) { } } `, - ConfigQueryChecks: []querycheck.QueryCheck{ + ConfigQueryChecks: []querycheck.QueryCheck{ querycheck.ExpectIdentity("examplecloud_containerette.test", map[string]knownvalue.Check{ - "id": knownvalue.StringExact("westeurope/somevalue1"), - }), + "id": knownvalue.StringExact("westeurope/somevalue1"), + }), querycheck.ExpectIdentity("examplecloud_containerette.test", map[string]knownvalue.Check{ - "id": knownvalue.StringExact("westeurope/somevalue2"), + "id": knownvalue.StringExact("westeurope/somevalue2"), }), querycheck.ExpectIdentity("examplecloud_containerette.test", map[string]knownvalue.Check{ - "id": knownvalue.StringExact("westeurope/somevalue3"), + "id": knownvalue.StringExact("westeurope/somevalue3"), }), querycheck.ExpectIdentity("examplecloud_containerette.test2", map[string]knownvalue.Check{ - "id": knownvalue.StringExact("westeurope/somevalue1"), + "id": knownvalue.StringExact("westeurope/somevalue1"), }), querycheck.ExpectIdentity("examplecloud_containerette.test2", map[string]knownvalue.Check{ - "id": knownvalue.StringExact("westeurope/somevalue2"), + "id": knownvalue.StringExact("westeurope/somevalue2"), }), querycheck.ExpectIdentity("examplecloud_containerette.test2", map[string]knownvalue.Check{ - "id": knownvalue.StringExact("westeurope/somevalue3"), + "id": knownvalue.StringExact("westeurope/somevalue3"), }), }, }, diff --git a/helper/resource/testing_new.go b/helper/resource/testing_new.go index 6c01ca3b4..121d6d590 100644 --- a/helper/resource/testing_new.go +++ b/helper/resource/testing_new.go @@ -388,6 +388,7 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest fmt.Printf("Step %d/%d Query Output:\n%s\n", stepNumber, len(c.Steps), queryOut) t.Fatalf("Step %d/%d error running query: %s", stepNumber, len(c.Steps), err) } + err = query.RunQueryChecks(ctx, t, &queryOut, step.ConfigQueryChecks) if err != nil { t.Fatalf("Step %d/%d error running query checks: %s", stepNumber, len(c.Steps), err) @@ -396,7 +397,6 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest fmt.Printf("Step %d/%d Query Output:\n%s\n", stepNumber, len(c.Steps), queryOut) continue - } if cfg != nil { diff --git a/internal/plugintest/working_dir.go b/internal/plugintest/working_dir.go index 935155f8a..8631c6eb5 100644 --- a/internal/plugintest/working_dir.go +++ b/internal/plugintest/working_dir.go @@ -5,12 +5,10 @@ package plugintest import ( "context" - "encoding/json" "fmt" "io" "os" "path/filepath" - "time" "github.com/hashicorp/terraform-exec/tfexec" tfjson "github.com/hashicorp/terraform-json" @@ -569,43 +567,3 @@ func (wd *WorkingDir) Query(ctx context.Context) ([]tfjson.LogMsg, error) { return messages, nil } - -// Taken from https://github.com/hashicorp/terraform-json/pull/169/ -const ( - MessageListResourceFound tfjson.LogMessageType = "list_resource_found" -) - -type ListResourceFoundMessage struct { - baseLogMessage - Address string `json:"address"` - DisplayName string `json:"display_name"` - Identity map[string]json.RawMessage `json:"identity"` - ResourceType string `json:"resource_type"` - ResourceObject map[string]json.RawMessage `json:"resource_object,omitempty"` - Config string `json:"config,omitempty"` - ImportConfig string `json:"import_config,omitempty"` -} - -type baseLogMessage struct { - Lvl tfjson.LogMessageLevel `json:"@level"` - Msg string `json:"@message"` - Time time.Time `json:"@timestamp"` -} - -type msgType struct { - Type tfjson.LogMessageType `json:"type"` -} - -type Result struct { - ListResourceFoundMessage `json:"list_resource_found"` -} - -func unmarshalResult(t tfjson.LogMessageType, b []byte) (*tfjson.ListResourceFoundData, error) { - v := tfjson.ListResourceFoundData{} - switch t { - case MessageListResourceFound: - return &v, json.Unmarshal(b, &v) - } - - return nil, nil -} diff --git a/querycheck/expect_identity.go b/querycheck/expect_identity.go index ccfe273ac..46659986e 100644 --- a/querycheck/expect_identity.go +++ b/querycheck/expect_identity.go @@ -9,6 +9,8 @@ import ( "fmt" "maps" "slices" + "strings" + "time" tfjson "github.com/hashicorp/terraform-json" "github.com/hashicorp/terraform-plugin-testing/knownvalue" @@ -18,58 +20,61 @@ var _ QueryCheck = expectIdentity{} type expectIdentity struct { resourceAddress string - check map[string]knownvalue.Check + check map[string]knownvalue.Check } // CheckQuery implements the query check logic. func (e expectIdentity) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { - var resource map[string]json.RawMessage - var foundIdentities []map[string]json.RawMessage + //var resource map[string]json.RawMessage + var foundIdentities []map[string]json.RawMessage + var found bool if req.Query == nil { resp.Error = fmt.Errorf("query is nil") return } - // later check for if the list resource object should be included in the query include_resource + // iterate through query messages and find the identity message // if we find the identity remove message from slice and if we are at the end then it wasn't found :( for _, v := range *req.Query { switch idk := v.(type) { case tfjson.ListResourceFoundMessage: - if idk.ListResourceFound.Address == e.resourceAddress { - foundIdentities = append(foundIdentities, idk.ListResourceFound.Identity) + prefix := "list." + if strings.TrimPrefix(idk.ListResourceFound.Address, prefix) == e.resourceAddress { + foundIdentities = append(foundIdentities, idk.ListResourceFound.Identity) } default: - continue + continue } } - if resource == nil { - resp.Error = fmt.Errorf("%s - Resource not found in query", e.resourceAddress) - - return - } + //if resource == nil { + // resp.Error = fmt.Errorf("%s - Resource not found in query", e.resourceAddress) + // + // return + //} - if len(foundIdentities) == 0 { - resp.Error = fmt.Errorf("%s - Identity not found in query. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.12+)", e.resourceAddress) + if len(foundIdentities) == 0 { + resp.Error = fmt.Errorf("%s - Identity not found in query. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.14+)", e.resourceAddress) return } - //if len(resource.IdentityValues) != len(e.identity) { - //deltaMsg := "" - //if len(resource.IdentityValues) > len(e.identity) { - // deltaMsg = createDeltaString(resource.IdentityValues, e.identity, "actual identity has extra attribute(s): ") - //} else { - // deltaMsg = createDeltaString(e.identity, resource.IdentityValues, "actual identity is missing attribute(s): ") - //} + //if len(resource.IdentityValues) != len(e.identity) { + //deltaMsg := "" + //if len(resource.IdentityValues) > len(e.identity) { + // deltaMsg = createDeltaString(resource.IdentityValues, e.identity, "actual identity has extra attribute(s): ") + //} else { + // deltaMsg = createDeltaString(e.identity, resource.IdentityValues, "actual identity is missing attribute(s): ") + //} - //resp.Error = fmt.Errorf("%s - Expected %d attribute(s) in the actual identity object, got %d attribute(s): %s", e.resourceAddress, len(e.identity), len(resource.IdentityValues), deltaMsg) + //resp.Error = fmt.Errorf("%s - Expected %d attribute(s) in the actual identity object, got %d attribute(s): %s", e.resourceAddress, len(e.identity), len(resource.IdentityValues), deltaMsg) //} - for _, resultIdentity := range foundIdentities { + var err error + for _, resultIdentity := range foundIdentities { for attribute := range e.check { var val any var ok bool @@ -78,12 +83,31 @@ func (e expectIdentity) CheckQuery(ctx context.Context, req CheckQueryRequest, r return } - if err := e.check[attribute].CheckValue(val); err != nil { - resp.Error = fmt.Errorf("%s - %q identity attribute: %s", e.resourceAddress, e.check, err) + var unmarshed string + + rawMessage, ok := val.(json.RawMessage) + if !ok { + resp.Error = fmt.Errorf("%s - expected json.RawMessage but got %T", e.resourceAddress, val) + return + } + err = json.Unmarshal(rawMessage, &unmarshed) + + if err != nil { + resp.Error = fmt.Errorf("%s - Error decoding message type: %s", e.resourceAddress, err) + return + } + + if err = e.check[attribute].CheckValue(unmarshed); err == nil { + found = true return } } } + + if !found { + resp.Error = fmt.Errorf("%s - %q identity attribute: %s", e.resourceAddress, e.check, err) + } + return } @@ -95,7 +119,7 @@ func (e expectIdentity) CheckQuery(ctx context.Context, req CheckQueryRequest, r func ExpectIdentity(resourceAddress string, identity map[string]knownvalue.Check) QueryCheck { return expectIdentity{ resourceAddress: resourceAddress, - check: identity, + check: identity, } } @@ -122,3 +146,42 @@ func createDeltaString[T any, V any](mapA map[string]T, mapB map[string]V, msgPr return deltaMsg } + +const ( + MessageListResourceFound tfjson.LogMessageType = "list_resource_found" +) + +type ListResourceFoundMessage struct { + baseLogMessage + Address string `json:"address"` + DisplayName string `json:"display_name"` + Identity map[string]json.RawMessage `json:"identity"` + ResourceType string `json:"resource_type"` + ResourceObject map[string]json.RawMessage `json:"resource_object,omitempty"` + Config string `json:"config,omitempty"` + ImportConfig string `json:"import_config,omitempty"` +} + +type baseLogMessage struct { + Lvl tfjson.LogMessageLevel `json:"@level"` + Msg string `json:"@message"` + Time time.Time `json:"@timestamp"` +} + +type msgType struct { + Type tfjson.LogMessageType `json:"type"` +} + +type Result struct { + ListResourceFoundMessage `json:"list_resource_found"` +} + +func unmarshalResult(t tfjson.LogMessageType, b []byte) (*tfjson.ListResourceFoundData, error) { + v := tfjson.ListResourceFoundData{} + switch t { + case MessageListResourceFound: + return &v, json.Unmarshal(b, &v) + } + + return nil, nil +} From b6e6c9ce098ad77961ff90a357a2fa64ee7906a0 Mon Sep 17 00:00:00 2001 From: Rain Date: Thu, 11 Sep 2025 10:46:47 -0400 Subject: [PATCH 28/45] Refactored expect_identity.go so it works for multiple identity attributes, started to add length and length exact checks --- .../resource/query/examplecloud_list_test.go | 124 +++++++++--------- helper/resource/query/query_checks.go | 2 +- helper/resource/query/query_checks_test.go | 2 +- helper/resource/query/query_test.go | 58 +++++++- helper/resource/testing.go | 15 ++- helper/resource/testing_new.go | 2 +- querycheck/expect_identity.go | 110 ++-------------- querycheck/expect_identity_test.go | 18 +-- querycheck/expect_identity_value.go | 4 +- querycheck/expect_identity_value_test.go | 18 +-- querycheck/expect_result_length_atleast.go | 59 +++++++++ querycheck/expect_result_length_exact.go | 63 +++++++++ querycheck/query_check.go | 6 +- 13 files changed, 286 insertions(+), 195 deletions(-) create mode 100644 querycheck/expect_result_length_atleast.go create mode 100644 querycheck/expect_result_length_exact.go diff --git a/helper/resource/query/examplecloud_list_test.go b/helper/resource/query/examplecloud_list_test.go index 08799567b..47303dc2a 100644 --- a/helper/resource/query/examplecloud_list_test.go +++ b/helper/resource/query/examplecloud_list_test.go @@ -11,72 +11,78 @@ import ( func examplecloudListResource() testprovider.ListResource { return testprovider.ListResource{ IncludeResource: true, - SchemaResponse: &list.SchemaResponse{ - Schema: &tfprotov6.Schema{ - Block: &tfprotov6.SchemaBlock{ - Attributes: []*tfprotov6.SchemaAttribute{ - { - Name: "id", - Type: tftypes.String, - Computed: true, - }, + SchemaResponse: &list.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, }, }, }, }, - ListResultsStream: &list.ListResultsStream{ - Results: func(push func(list.ListResult) bool) { - push(list.ListResult{ - Resource: teststep.Pointer(tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - "location": tftypes.String, - "name": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), - "location": tftypes.NewValue(tftypes.String, "westeurope"), - "name": tftypes.NewValue(tftypes.String, "somevalue"), - }, - )), - Identity: teststep.Pointer(tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue1"), + }, + ListResultsStream: &list.ListResultsStream{ + Results: func(push func(list.ListResult) bool) { + push(list.ListResult{ + Resource: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, }, - )), - }) - push(list.ListResult{ - Identity: teststep.Pointer(tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue2"), + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "somevalue"), + }, + )), + Identity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, }, - )), - }) - push(list.ListResult{ - Identity: teststep.Pointer(tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue1"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + }, + )), + }) + push(list.ListResult{ + Identity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, }, - map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue3"), + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue2"), + "location": tftypes.NewValue(tftypes.String, "westeurope2"), + }, + )), + }) + push(list.ListResult{ + Identity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, }, - )), - }) - }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue3"), + "location": tftypes.NewValue(tftypes.String, "westeurope3"), + }, + )), + }) }, - } + }, + } } diff --git a/helper/resource/query/query_checks.go b/helper/resource/query/query_checks.go index 75a3d5aa6..5f3583eae 100644 --- a/helper/resource/query/query_checks.go +++ b/helper/resource/query/query_checks.go @@ -13,7 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-testing/querycheck" ) -func RunQueryChecks(ctx context.Context, t testing.T, query *[]tfjson.LogMsg, queryChecks []querycheck.QueryCheck) error { +func RunQueryChecks(ctx context.Context, t testing.T, query *[]tfjson.LogMsg, queryChecks []querycheck.QueryResultCheck) error { t.Helper() var result []error diff --git a/helper/resource/query/query_checks_test.go b/helper/resource/query/query_checks_test.go index ee9242c97..2c17be8d2 100644 --- a/helper/resource/query/query_checks_test.go +++ b/helper/resource/query/query_checks_test.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-testing/querycheck" ) -var _ querycheck.QueryCheck = &queryCheckSpy{} +var _ querycheck.QueryResultCheck = &queryCheckSpy{} type queryCheckSpy struct { err error diff --git a/helper/resource/query/query_test.go b/helper/resource/query/query_test.go index 2ba76584d..6be86c78e 100644 --- a/helper/resource/query/query_test.go +++ b/helper/resource/query/query_test.go @@ -55,7 +55,7 @@ func TestQuery(t *testing.T) { config { id = "westeurope/somevalue" - } + } } list "examplecloud_containerette" "test2" { provider = examplecloud @@ -65,15 +65,17 @@ func TestQuery(t *testing.T) { } } `, - ConfigQueryChecks: []querycheck.QueryCheck{ + ConfigQueryResultChecks: []querycheck.QueryResultCheck{ querycheck.ExpectIdentity("examplecloud_containerette.test", map[string]knownvalue.Check{ "id": knownvalue.StringExact("westeurope/somevalue1"), }), querycheck.ExpectIdentity("examplecloud_containerette.test", map[string]knownvalue.Check{ - "id": knownvalue.StringExact("westeurope/somevalue2"), + "id": knownvalue.StringExact("westeurope/somevalue2"), + "location": knownvalue.StringExact("westeurope2"), }), querycheck.ExpectIdentity("examplecloud_containerette.test", map[string]knownvalue.Check{ - "id": knownvalue.StringExact("westeurope/somevalue3"), + "id": knownvalue.StringExact("westeurope/somevalue3"), + "location": knownvalue.StringExact("westeurope3"), }), querycheck.ExpectIdentity("examplecloud_containerette.test2", map[string]knownvalue.Check{ "id": knownvalue.StringExact("westeurope/somevalue1"), @@ -86,6 +88,54 @@ func TestQuery(t *testing.T) { }), }, }, + { + Query: true, + Config: ` + provider "examplecloud" {} + list "examplecloud_containerette" "test" { + provider = examplecloud + + config { + id = "westeurope/somevalue" + } + } + list "examplecloud_containerette" "test2" { + provider = examplecloud + + config { + id = "foo" + } + } + `, + ConfigQueryResultChecks: []querycheck.QueryResultCheck{ + querycheck.ExpectLength("examplecloud_containerette.test", knownvalue.Int64Exact(3)), + querycheck.ExpectLength("examplecloud_containerette.test2", knownvalue.Int64Exact(3)), + }, + }, + { + Query: true, + Config: ` + provider "examplecloud" {} + list "examplecloud_containerette" "test" { + provider = examplecloud + + config { + id = "westeurope/somevalue" + } + } + list "examplecloud_containerette" "test2" { + provider = examplecloud + + config { + id = "foo" + } + } + `, + ConfigQueryResultChecks: []querycheck.QueryResultCheck{ + querycheck.ExpectLengthAtLeast("examplecloud_containerette.test", 2), + querycheck.ExpectLengthAtLeast("examplecloud_containerette.test2", 1), + }, + }, }, }) } diff --git a/helper/resource/testing.go b/helper/resource/testing.go index 162c17262..5a6566319 100644 --- a/helper/resource/testing.go +++ b/helper/resource/testing.go @@ -8,7 +8,6 @@ import ( "errors" "flag" "fmt" - "github.com/hashicorp/terraform-plugin-testing/querycheck" "log" "os" "regexp" @@ -16,6 +15,8 @@ import ( "strings" "time" + "github.com/hashicorp/terraform-plugin-testing/querycheck" + "github.com/mitchellh/go-testing-interface" "github.com/hashicorp/terraform-plugin-go/tfprotov5" @@ -641,9 +642,9 @@ type TestStep struct { // Custom state checks can be created by implementing the [statecheck.StateCheck] interface, or by using a StateCheck implementation from the provided [statecheck] package. ConfigStateChecks []statecheck.StateCheck - // ConfigQueryChecks allow assertions to be made against the query file during a Config test using a query check. - // Custom query checks can be created by implementing the [querycheck.QueryCheck] interface, or by using a QueryCheck implementation from the provided [querycheck] package. - ConfigQueryChecks []querycheck.QueryCheck + // ConfigQueryResultChecks allow assertions to be made against the query file during a Config test using a query check. + // Custom query checks can be created by implementing the [querycheck.QueryResultCheck] interface, or by using a QueryResultCheck implementation from the provided [querycheck] package. + ConfigQueryResultChecks []querycheck.QueryResultCheck // PlanOnly can be set to only run `plan` with this configuration, and not // actually apply it. This is useful for ensuring config changes result in @@ -864,15 +865,15 @@ type ConfigPlanChecks struct { type ConfigQueryChecks struct { // PreApply runs all query checks in the slice. This occurs before the apply of a Config test is run. This slice cannot be populated // with TestStep.QueryOnly, as there is no PreApply query run with that flag set. All errors by query checks in this slice are aggregated, reported, and will result in a test failure. - PreApply []querycheck.QueryCheck + PreApply []querycheck.QueryResultCheck // PostApplyPreRefresh runs all query checks in the slice. This occurs after the apply and before the refresh of a Config test is run. // All errors by query checks in this slice are aggregated, reported, and will result in a test failure. - PostApplyPreRefresh []querycheck.QueryCheck + PostApplyPreRefresh []querycheck.QueryResultCheck // PostApplyPostRefresh runs all query checks in the slice. This occurs after the apply and refresh of a Config test are run. // All errors by query checks in this slice are aggregated, reported, and will result in a test failure. - PostApplyPostRefresh []querycheck.QueryCheck + PostApplyPostRefresh []querycheck.QueryResultCheck } // ImportPlanChecks defines the different points in an Import TestStep when plan checks can be run. diff --git a/helper/resource/testing_new.go b/helper/resource/testing_new.go index 121d6d590..194f1f122 100644 --- a/helper/resource/testing_new.go +++ b/helper/resource/testing_new.go @@ -389,7 +389,7 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest t.Fatalf("Step %d/%d error running query: %s", stepNumber, len(c.Steps), err) } - err = query.RunQueryChecks(ctx, t, &queryOut, step.ConfigQueryChecks) + err = query.RunQueryChecks(ctx, t, &queryOut, step.ConfigQueryResultChecks) if err != nil { t.Fatalf("Step %d/%d error running query checks: %s", stepNumber, len(c.Steps), err) } diff --git a/querycheck/expect_identity.go b/querycheck/expect_identity.go index 46659986e..cfa8b45cd 100644 --- a/querycheck/expect_identity.go +++ b/querycheck/expect_identity.go @@ -6,17 +6,15 @@ package querycheck import ( "context" "encoding/json" + "errors" "fmt" - "maps" - "slices" "strings" - "time" tfjson "github.com/hashicorp/terraform-json" "github.com/hashicorp/terraform-plugin-testing/knownvalue" ) -var _ QueryCheck = expectIdentity{} +var _ QueryResultCheck = expectIdentity{} type expectIdentity struct { resourceAddress string @@ -25,66 +23,44 @@ type expectIdentity struct { // CheckQuery implements the query check logic. func (e expectIdentity) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { - //var resource map[string]json.RawMessage var foundIdentities []map[string]json.RawMessage - var found bool if req.Query == nil { resp.Error = fmt.Errorf("query is nil") return } - // iterate through query messages and find the identity message - // if we find the identity remove message from slice and if we are at the end then it wasn't found :( - for _, v := range *req.Query { - switch idk := v.(type) { + switch i := v.(type) { case tfjson.ListResourceFoundMessage: prefix := "list." - if strings.TrimPrefix(idk.ListResourceFound.Address, prefix) == e.resourceAddress { - foundIdentities = append(foundIdentities, idk.ListResourceFound.Identity) + if strings.TrimPrefix(i.ListResourceFound.Address, prefix) == e.resourceAddress { + foundIdentities = append(foundIdentities, i.ListResourceFound.Identity) } default: continue } } - //if resource == nil { - // resp.Error = fmt.Errorf("%s - Resource not found in query", e.resourceAddress) - // - // return - //} - if len(foundIdentities) == 0 { - resp.Error = fmt.Errorf("%s - Identity not found in query. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.14+)", e.resourceAddress) + resp.Error = fmt.Errorf("%s - Identity not found in query. Either the resource does not support query or the Terraform version running the test does not support query. (must be v1.14+)", e.resourceAddress) return } - //if len(resource.IdentityValues) != len(e.identity) { - //deltaMsg := "" - //if len(resource.IdentityValues) > len(e.identity) { - // deltaMsg = createDeltaString(resource.IdentityValues, e.identity, "actual identity has extra attribute(s): ") - //} else { - // deltaMsg = createDeltaString(e.identity, resource.IdentityValues, "actual identity is missing attribute(s): ") - //} - - //resp.Error = fmt.Errorf("%s - Expected %d attribute(s) in the actual identity object, got %d attribute(s): %s", e.resourceAddress, len(e.identity), len(resource.IdentityValues), deltaMsg) - //} - var err error for _, resultIdentity := range foundIdentities { for attribute := range e.check { var val any var ok bool + var unmarshalledVal any + if val, ok = resultIdentity[attribute]; !ok { resp.Error = fmt.Errorf("%s - expected attribute %q not in actual identity object", e.resourceAddress, attribute) return } - var unmarshed string - rawMessage, ok := val.(json.RawMessage) if !ok { resp.Error = fmt.Errorf("%s - expected json.RawMessage but got %T", e.resourceAddress, val) @@ -97,9 +73,8 @@ func (e expectIdentity) CheckQuery(ctx context.Context, req CheckQueryRequest, r return } - if err = e.check[attribute].CheckValue(unmarshed); err == nil { - found = true - return + if err = e.check[attribute].CheckValue(unmarshalledVal); err != nil { + errCollection = append(errCollection, fmt.Errorf("%s - %q identity attribute: %s\n", e.resourceAddress, e.check, err)) } } } @@ -116,72 +91,9 @@ func (e expectIdentity) CheckQuery(ctx context.Context, req CheckQueryRequest, r // attributes will raise a diagnostic. // // This query check can only be used with managed resources that support resource identity. Resource identity is only supported in Terraform v1.12+ -func ExpectIdentity(resourceAddress string, identity map[string]knownvalue.Check) QueryCheck { +func ExpectIdentity(resourceAddress string, identity map[string]knownvalue.Check) QueryResultCheck { return expectIdentity{ resourceAddress: resourceAddress, check: identity, } } - -// createDeltaString prints the map keys that are present in mapA and not present in mapB -func createDeltaString[T any, V any](mapA map[string]T, mapB map[string]V, msgPrefix string) string { - deltaMsg := "" - - deltaMap := make(map[string]T, len(mapA)) - maps.Copy(deltaMap, mapA) - for key := range mapB { - delete(deltaMap, key) - } - - deltaKeys := slices.Sorted(maps.Keys(deltaMap)) - - for i, k := range deltaKeys { - if i == 0 { - deltaMsg += msgPrefix - } else { - deltaMsg += ", " - } - deltaMsg += fmt.Sprintf("%q", k) - } - - return deltaMsg -} - -const ( - MessageListResourceFound tfjson.LogMessageType = "list_resource_found" -) - -type ListResourceFoundMessage struct { - baseLogMessage - Address string `json:"address"` - DisplayName string `json:"display_name"` - Identity map[string]json.RawMessage `json:"identity"` - ResourceType string `json:"resource_type"` - ResourceObject map[string]json.RawMessage `json:"resource_object,omitempty"` - Config string `json:"config,omitempty"` - ImportConfig string `json:"import_config,omitempty"` -} - -type baseLogMessage struct { - Lvl tfjson.LogMessageLevel `json:"@level"` - Msg string `json:"@message"` - Time time.Time `json:"@timestamp"` -} - -type msgType struct { - Type tfjson.LogMessageType `json:"type"` -} - -type Result struct { - ListResourceFoundMessage `json:"list_resource_found"` -} - -func unmarshalResult(t tfjson.LogMessageType, b []byte) (*tfjson.ListResourceFoundData, error) { - v := tfjson.ListResourceFoundData{} - switch t { - case MessageListResourceFound: - return &v, json.Unmarshal(b, &v) - } - - return nil, nil -} diff --git a/querycheck/expect_identity_test.go b/querycheck/expect_identity_test.go index 600005145..4134945bc 100644 --- a/querycheck/expect_identity_test.go +++ b/querycheck/expect_identity_test.go @@ -28,7 +28,7 @@ func TestExpectIdentity_CheckQuery_ResourceNotFound(t *testing.T) { Steps: []r.TestStep{ { Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ + ConfigQueryResultChecks: []querycheck.QueryResultCheck{ querycheck.ExpectIdentity( "examplecloud_thing.two", map[string]knownvalue.Check{ @@ -65,7 +65,7 @@ func TestExpectIdentity_CheckQuery_No_Terraform_Identity_Support(t *testing.T) { Steps: []r.TestStep{ { Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ + ConfigQueryResultChecks: []querycheck.QueryResultCheck{ querycheck.ExpectIdentity( "examplecloud_thing.one", map[string]knownvalue.Check{ @@ -103,7 +103,7 @@ func TestExpectIdentity_CheckQuery_No_Identity(t *testing.T) { Steps: []r.TestStep{ { Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ + ConfigQueryResultChecks: []querycheck.QueryResultCheck{ querycheck.ExpectIdentity( "examplecloud_thing.one", map[string]knownvalue.Check{ @@ -152,7 +152,7 @@ func TestExpectIdentity_CheckQuery(t *testing.T) { id = "bat" } }`, - ConfigQueryChecks: []querycheck.QueryCheck{ + ConfigQueryResultChecks: []querycheck.QueryResultCheck{ querycheck.ExpectIdentity( "examplecloud_thing.one", map[string]knownvalue.Check{ @@ -186,7 +186,7 @@ func TestExpectIdentity_CheckQuery_KnownValueWrongType(t *testing.T) { Steps: []r.TestStep{ { Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ + ConfigQueryResultChecks: []querycheck.QueryResultCheck{ querycheck.ExpectIdentity( "examplecloud_thing.one", @@ -222,7 +222,7 @@ func TestExpectIdentity_CheckQuery_KnownValueWrongValue(t *testing.T) { Steps: []r.TestStep{ { Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ + ConfigQueryResultChecks: []querycheck.QueryResultCheck{ querycheck.ExpectIdentity( "examplecloud_thing.one", @@ -258,7 +258,7 @@ func TestExpectIdentity_CheckQuery_ExtraAttribute(t *testing.T) { Steps: []r.TestStep{ { Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ + ConfigQueryResultChecks: []querycheck.QueryResultCheck{ querycheck.ExpectIdentity( "examplecloud_thing.one", @@ -286,7 +286,7 @@ func TestExpectIdentity_CheckQuery_MissingAttribute(t *testing.T) { Steps: []r.TestStep{ { Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ + ConfigQueryResultChecks: []querycheck.QueryResultCheck{ querycheck.ExpectIdentity( "examplecloud_thing.one", @@ -323,7 +323,7 @@ func TestExpectIdentity_CheckQuery_MismatchedAttribute(t *testing.T) { Steps: []r.TestStep{ { Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ + ConfigQueryResultChecks: []querycheck.QueryResultCheck{ querycheck.ExpectIdentity( "examplecloud_thing.one", map[string]knownvalue.Check{ diff --git a/querycheck/expect_identity_value.go b/querycheck/expect_identity_value.go index 9eba2f145..3b940f6b7 100644 --- a/querycheck/expect_identity_value.go +++ b/querycheck/expect_identity_value.go @@ -11,7 +11,7 @@ import ( "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" ) -var _ QueryCheck = expectIdentityValue{} +var _ QueryResultCheck = expectIdentityValue{} type expectIdentityValue struct { resourceAddress string @@ -81,7 +81,7 @@ func (e expectIdentityValue) CheckQuery(ctx context.Context, req CheckQueryReque // matches a known value. This query check can only be used with managed resources that support resource identity. // // Resource identity is only supported in Terraform v1.12+ -func ExpectIdentityValue(resourceAddress string, attributePath tfjsonpath.Path, identityValue knownvalue.Check) QueryCheck { +func ExpectIdentityValue(resourceAddress string, attributePath tfjsonpath.Path, identityValue knownvalue.Check) QueryResultCheck { return expectIdentityValue{ resourceAddress: resourceAddress, attributePath: attributePath, diff --git a/querycheck/expect_identity_value_test.go b/querycheck/expect_identity_value_test.go index c284b25ad..f999ff1b6 100644 --- a/querycheck/expect_identity_value_test.go +++ b/querycheck/expect_identity_value_test.go @@ -35,7 +35,7 @@ func TestExpectIdentityValue_CheckQuery_ResourceNotFound(t *testing.T) { Steps: []r.TestStep{ { Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ + ConfigQueryResultChecks: []querycheck.QueryResultCheck{ querycheck.ExpectIdentityValue( "examplecloud_thing.two", tfjsonpath.New("id"), @@ -63,7 +63,7 @@ func TestExpectIdentityValue_CheckQuery_No_Terraform_Identity_Support(t *testing Steps: []r.TestStep{ { Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ + ConfigQueryResultChecks: []querycheck.QueryResultCheck{ querycheck.ExpectIdentityValue( "examplecloud_thing.one", tfjsonpath.New("id"), @@ -92,7 +92,7 @@ func TestExpectIdentityValue_CheckQuery_No_Identity(t *testing.T) { Steps: []r.TestStep{ { Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ + ConfigQueryResultChecks: []querycheck.QueryResultCheck{ querycheck.ExpectIdentityValue( "examplecloud_thing.one", tfjsonpath.New("id"), @@ -120,7 +120,7 @@ func TestExpectIdentityValue_CheckQuery_String(t *testing.T) { Steps: []r.TestStep{ { Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ + ConfigQueryResultChecks: []querycheck.QueryResultCheck{ querycheck.ExpectIdentityValue( "examplecloud_thing.one", tfjsonpath.New("id"), @@ -144,7 +144,7 @@ func TestExpectIdentityValue_CheckQuery_String_KnownValueWrongType(t *testing.T) Steps: []r.TestStep{ { Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ + ConfigQueryResultChecks: []querycheck.QueryResultCheck{ querycheck.ExpectIdentityValue( "examplecloud_thing.one", tfjsonpath.New("id"), @@ -169,7 +169,7 @@ func TestExpectIdentityValue_CheckQuery_String_KnownValueWrongValue(t *testing.T Steps: []r.TestStep{ { Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ + ConfigQueryResultChecks: []querycheck.QueryResultCheck{ querycheck.ExpectIdentityValue( "examplecloud_thing.one", tfjsonpath.New("id"), @@ -194,7 +194,7 @@ func TestExpectIdentityValue_CheckQuery_List(t *testing.T) { Steps: []r.TestStep{ { Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ + ConfigQueryResultChecks: []querycheck.QueryResultCheck{ querycheck.ExpectIdentityValue( "examplecloud_thing.one", tfjsonpath.New("list_of_numbers").AtSliceIndex(0), @@ -235,7 +235,7 @@ func TestExpectIdentityValue_CheckQuery_List_KnownValueWrongType(t *testing.T) { { Config: `resource "examplecloud_thing" "one" {} `, - ConfigQueryChecks: []querycheck.QueryCheck{ + ConfigQueryResultChecks: []querycheck.QueryResultCheck{ querycheck.ExpectIdentityValue( "examplecloud_thing.one", tfjsonpath.New("list_of_numbers"), @@ -261,7 +261,7 @@ func TestExpectIdentityValue_CheckQuery_List_KnownValueWrongValue(t *testing.T) Steps: []r.TestStep{ { Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryChecks: []querycheck.QueryCheck{ + ConfigQueryResultChecks: []querycheck.QueryResultCheck{ querycheck.ExpectIdentityValue( "examplecloud_thing.one", tfjsonpath.New("list_of_numbers"), diff --git a/querycheck/expect_result_length_atleast.go b/querycheck/expect_result_length_atleast.go new file mode 100644 index 000000000..9b1c27249 --- /dev/null +++ b/querycheck/expect_result_length_atleast.go @@ -0,0 +1,59 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "fmt" + "strings" + + tfjson "github.com/hashicorp/terraform-json" +) + +var _ QueryResultCheck = expectLengthAtLeast{} + +type expectLengthAtLeast struct { + resourceAddress string + check int +} + +// CheckQuery implements the query check logic. +func (e expectLengthAtLeast) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { + if req.Query == nil { + resp.Error = fmt.Errorf("query is nil") + return + } + + for _, v := range *req.Query { + switch i := v.(type) { + case tfjson.ListCompleteMessage: + prefix := "list." + + if strings.TrimPrefix(i.ListComplete.Address, prefix) == e.resourceAddress { + if i.ListComplete.Total < e.check { + resp.Error = fmt.Errorf("Query result of at least length %v - expected but got %v.", e.check, i.ListComplete.Total) + return + } else { + return + } + } + default: + continue + } + } + + resp.Error = fmt.Errorf("%s - Address not found in query result.", e.resourceAddress) + + return +} + +// ExpectLengthAtLeast returns a query check that asserts that the length of the query result is at least the given value. +// +// This query check can only be used with managed resources that support query. Query is only supported in Terraform v1.14+ +func ExpectLengthAtLeast(resourceAddress string, length int) QueryResultCheck { + return expectLengthAtLeast{ + resourceAddress: resourceAddress, + check: length, + } +} diff --git a/querycheck/expect_result_length_exact.go b/querycheck/expect_result_length_exact.go new file mode 100644 index 000000000..e7e3a7c8e --- /dev/null +++ b/querycheck/expect_result_length_exact.go @@ -0,0 +1,63 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + "strings" + + tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +var _ QueryResultCheck = expectLength{} + +type expectLength struct { + resourceAddress string + check knownvalue.Check +} + +// CheckQuery implements the query check logic. +func (e expectLength) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { + if req.Query == nil { + resp.Error = fmt.Errorf("query is nil") + return + } + + for _, v := range *req.Query { + switch i := v.(type) { + case tfjson.ListCompleteMessage: + prefix := "list." + lengthCheck := json.Number((strconv.Itoa(i.ListComplete.Total))) + + if strings.TrimPrefix(i.ListComplete.Address, prefix) == e.resourceAddress { + if err := e.check.CheckValue(lengthCheck); err != nil { + resp.Error = fmt.Errorf("Query result of length %v - expected but got %v.", e.check, i.ListComplete.Total) + return + } else { + return + } + } + default: + continue + } + } + + resp.Error = fmt.Errorf("%s - Address not found in query result.", e.resourceAddress) + + return +} + +// ExpectLength returns a query check that asserts that the length of the query result is exactly the given value. +// +// This query check can only be used with managed resources that support query. Query is only supported in Terraform v1.14+ +func ExpectLength(resourceAddress string, length knownvalue.Check) QueryResultCheck { + return expectLength{ + resourceAddress: resourceAddress, + check: length, + } +} diff --git a/querycheck/query_check.go b/querycheck/query_check.go index 2946b3e60..bdb7fb234 100644 --- a/querycheck/query_check.go +++ b/querycheck/query_check.go @@ -9,9 +9,9 @@ import ( tfjson "github.com/hashicorp/terraform-json" ) -// QueryCheck defines an interface for implementing test logic that checks a query file and then returns an error +// QueryResultCheck defines an interface for implementing test logic that checks a query file and then returns an error // if the query file does not match what is expected. -type QueryCheck interface { +type QueryResultCheck interface { // CheckQuery should perform the query check. CheckQuery(context.Context, CheckQueryRequest, *CheckQueryResponse) } @@ -24,7 +24,7 @@ type CheckQueryRequest struct { // CheckQueryResponse is a response to an invoke of the CheckQuery function. type CheckQueryResponse struct { - // Error is used to report the failure of a query check assertion and is combined with other QueryCheck errors + // Error is used to report the failure of a query check assertion and is combined with other QueryResultCheck errors // to be reported as a test failure. Error error } From a6c11c21c9e254f786305cc2e13c96351ef06a6f Mon Sep 17 00:00:00 2001 From: Rain Date: Thu, 11 Sep 2025 10:47:05 -0400 Subject: [PATCH 29/45] Changes by sgoods --- helper/resource/query/examplecloud_test.go | 23 ++++++++++++++++------ helper/resource/query/query_test.go | 5 +++-- querycheck/expect_identity.go | 20 ++++++++++++++++--- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/helper/resource/query/examplecloud_test.go b/helper/resource/query/examplecloud_test.go index 1cae8e95d..3ad86c169 100644 --- a/helper/resource/query/examplecloud_test.go +++ b/helper/resource/query/examplecloud_test.go @@ -31,11 +31,13 @@ func examplecloudResource() testprovider.Resource { NewIdentity: teststep.Pointer(tftypes.NewValue( tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, + "id": tftypes.String, + "location": tftypes.String, }, }, map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "location": tftypes.NewValue(tftypes.String, "somelocation"), }, )), }, @@ -57,11 +59,13 @@ func examplecloudResource() testprovider.Resource { NewIdentity: teststep.Pointer(tftypes.NewValue( tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, + "id": tftypes.String, + "location": tftypes.String, }, }, map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), }, )), }, @@ -83,11 +87,13 @@ func examplecloudResource() testprovider.Resource { Identity: teststep.Pointer(tftypes.NewValue( tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, + "id": tftypes.String, + "location": tftypes.String, }, }, map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "location": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), }, )), }, @@ -111,6 +117,11 @@ func examplecloudResource() testprovider.Resource { Type: tftypes.String, RequiredForImport: true, }, + { + Name: "location", + Type: tftypes.String, + RequiredForImport: true, + }, }, }, }, diff --git a/helper/resource/query/query_test.go b/helper/resource/query/query_test.go index 6be86c78e..67bb4f7e7 100644 --- a/helper/resource/query/query_test.go +++ b/helper/resource/query/query_test.go @@ -37,7 +37,7 @@ func TestQuery(t *testing.T) { Config: ` resource "examplecloud_containerette" "primary" { id = "westeurope/somevalue" - location = "westeurope" + location = "westeurope" name = "somevalue" }`, }, @@ -67,7 +67,8 @@ func TestQuery(t *testing.T) { `, ConfigQueryResultChecks: []querycheck.QueryResultCheck{ querycheck.ExpectIdentity("examplecloud_containerette.test", map[string]knownvalue.Check{ - "id": knownvalue.StringExact("westeurope/somevalue1"), + "id": knownvalue.StringExact("westeurope/somevalue1"), + "location": knownvalue.StringExact("westeurope"), }), querycheck.ExpectIdentity("examplecloud_containerette.test", map[string]knownvalue.Check{ "id": knownvalue.StringExact("westeurope/somevalue2"), diff --git a/querycheck/expect_identity.go b/querycheck/expect_identity.go index cfa8b45cd..27d0d1231 100644 --- a/querycheck/expect_identity.go +++ b/querycheck/expect_identity.go @@ -51,6 +51,8 @@ func (e expectIdentity) CheckQuery(ctx context.Context, req CheckQueryRequest, r var err error for _, resultIdentity := range foundIdentities { + var errCollection []error + for attribute := range e.check { var val any var ok bool @@ -66,7 +68,7 @@ func (e expectIdentity) CheckQuery(ctx context.Context, req CheckQueryRequest, r resp.Error = fmt.Errorf("%s - expected json.RawMessage but got %T", e.resourceAddress, val) return } - err = json.Unmarshal(rawMessage, &unmarshed) + err = json.Unmarshal(rawMessage, &unmarshalledVal) if err != nil { resp.Error = fmt.Errorf("%s - Error decoding message type: %s", e.resourceAddress, err) @@ -77,11 +79,23 @@ func (e expectIdentity) CheckQuery(ctx context.Context, req CheckQueryRequest, r errCollection = append(errCollection, fmt.Errorf("%s - %q identity attribute: %s\n", e.resourceAddress, e.check, err)) } } + + if errCollection == nil { + return + } } - if !found { - resp.Error = fmt.Errorf("%s - %q identity attribute: %s", e.resourceAddress, e.check, err) + var errCollection []error + + errCollection = append(errCollection, fmt.Errorf("An identity with all the following attributes was not found:")) + + // wrap errors for each check + for attr, check := range e.check { + errCollection = append(errCollection, fmt.Errorf("Attribute %s: %s", attr, check)) } + errCollection = append(errCollection, fmt.Errorf("Address: %s\n", e.resourceAddress)) + + resp.Error = errors.Join(errCollection...) return } From 3d77c5baf50e2d829d5e19c989969d2b7a3a2838 Mon Sep 17 00:00:00 2001 From: Rain Date: Thu, 11 Sep 2025 13:03:32 -0400 Subject: [PATCH 30/45] Updated error messages --- querycheck/expect_identity.go | 9 +- querycheck/expect_identity_test.go | 346 ---------------- querycheck/expect_identity_value.go | 90 ----- querycheck/expect_identity_value_test.go | 482 ----------------------- 4 files changed, 4 insertions(+), 923 deletions(-) delete mode 100644 querycheck/expect_identity_test.go delete mode 100644 querycheck/expect_identity_value.go delete mode 100644 querycheck/expect_identity_value_test.go diff --git a/querycheck/expect_identity.go b/querycheck/expect_identity.go index 27d0d1231..28c885cbf 100644 --- a/querycheck/expect_identity.go +++ b/querycheck/expect_identity.go @@ -43,7 +43,7 @@ func (e expectIdentity) CheckQuery(ctx context.Context, req CheckQueryRequest, r } if len(foundIdentities) == 0 { - resp.Error = fmt.Errorf("%s - Identity not found in query. Either the resource does not support query or the Terraform version running the test does not support query. (must be v1.14+)", e.resourceAddress) + resp.Error = fmt.Errorf("%s - Identity not found in query.", e.resourceAddress) return } @@ -87,7 +87,7 @@ func (e expectIdentity) CheckQuery(ctx context.Context, req CheckQueryRequest, r var errCollection []error - errCollection = append(errCollection, fmt.Errorf("An identity with all the following attributes was not found:")) + errCollection = append(errCollection, fmt.Errorf("An identity with the following attributes was not found:")) // wrap errors for each check for attr, check := range e.check { @@ -101,10 +101,9 @@ func (e expectIdentity) CheckQuery(ctx context.Context, req CheckQueryRequest, r } // ExpectIdentity returns a query check that asserts that the identity at the given resource matches a known object, where each -// map key represents an identity attribute name. The identity in query must exactly match the given object and any missing/extra -// attributes will raise a diagnostic. +// map key represents an identity attribute name. The identity in query must exactly match the given object. // -// This query check can only be used with managed resources that support resource identity. Resource identity is only supported in Terraform v1.12+ +// 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 ExpectIdentity(resourceAddress string, identity map[string]knownvalue.Check) QueryResultCheck { return expectIdentity{ resourceAddress: resourceAddress, diff --git a/querycheck/expect_identity_test.go b/querycheck/expect_identity_test.go deleted file mode 100644 index 4134945bc..000000000 --- a/querycheck/expect_identity_test.go +++ /dev/null @@ -1,346 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// 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/knownvalue" - "github.com/hashicorp/terraform-plugin-testing/querycheck" - "github.com/hashicorp/terraform-plugin-testing/tfversion" -) - -func TestExpectIdentity_CheckQuery_ResourceNotFound(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryResultChecks: []querycheck.QueryResultCheck{ - querycheck.ExpectIdentity( - "examplecloud_thing.two", - 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), - }, - ), - }, - ), - }, - ExpectError: regexp.MustCompile("examplecloud_thing.two - Resource not found in query"), - }, - }, - }) -} - -func TestExpectIdentity_CheckQuery_No_Terraform_Identity_Support(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories - tfversion.SkipAbove(tfversion.Version1_11_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - // Resource support identity, but the Terraform versions running will not. - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryResultChecks: []querycheck.QueryResultCheck{ - querycheck.ExpectIdentity( - "examplecloud_thing.one", - 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), - }, - ), - }, - ), - }, - ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in query. Either the resource ` + - `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, - ), - }, - }, - }) -} - -func TestExpectIdentity_CheckQuery_No_Identity(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - // Resource does not support identity - "examplecloud": examplecloudProviderNoIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryResultChecks: []querycheck.QueryResultCheck{ - querycheck.ExpectIdentity( - "examplecloud_thing.one", - 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), - }, - ), - }, - ), - }, - ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in query. Either the resource ` + - `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, - ), - }, - }, - }) -} - -func TestExpectIdentity_CheckQuery(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - }, - { - Query: true, - Config: ` - provider "examplecloud" {} - list "examplecloud_containerette" "test" { - provider = examplecloud - - config { - id = "bat" - } - }`, - ConfigQueryResultChecks: []querycheck.QueryResultCheck{ - querycheck.ExpectIdentity( - "examplecloud_thing.one", - 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), - }, - ), - }, - ), - }, - }, - }, - }) -} - -func TestExpectIdentity_CheckQuery_KnownValueWrongType(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryResultChecks: []querycheck.QueryResultCheck{ - - querycheck.ExpectIdentity( - "examplecloud_thing.one", - map[string]knownvalue.Check{ - "id": knownvalue.Bool(true), - "list_of_numbers": knownvalue.ListExact( - []knownvalue.Check{ - knownvalue.Int64Exact(1), - knownvalue.Int64Exact(2), - knownvalue.Int64Exact(3), - knownvalue.Int64Exact(4), - }, - ), - }, - ), - }, - ExpectError: regexp.MustCompile(`examplecloud_thing.one - "id" identity attribute: expected bool value for Bool check, got: string`), - }, - }, - }) -} - -func TestExpectIdentity_CheckQuery_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryResultChecks: []querycheck.QueryResultCheck{ - - querycheck.ExpectIdentity( - "examplecloud_thing.one", - map[string]knownvalue.Check{ - "id": knownvalue.StringExact("321-id"), - "list_of_numbers": knownvalue.ListExact( - []knownvalue.Check{ - knownvalue.Int64Exact(1), - knownvalue.Int64Exact(2), - knownvalue.Int64Exact(3), - knownvalue.Int64Exact(4), - }, - ), - }, - ), - }, - ExpectError: regexp.MustCompile(`examplecloud_thing.one - "id" identity attribute: expected value 321-id for StringExact check, got: id-123`), - }, - }, - }) -} - -func TestExpectIdentity_CheckQuery_ExtraAttribute(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryResultChecks: []querycheck.QueryResultCheck{ - - querycheck.ExpectIdentity( - "examplecloud_thing.one", - map[string]knownvalue.Check{ - "id": knownvalue.StringExact("321-id"), - }, - ), - }, - ExpectError: regexp.MustCompile(`examplecloud_thing.one - Expected 1 attribute\(s\) in the actual identity object, got 2 attribute\(s\): actual identity has extra attribute\(s\): "list_of_numbers"`), - }, - }, - }) -} - -func TestExpectIdentity_CheckQuery_MissingAttribute(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryResultChecks: []querycheck.QueryResultCheck{ - - querycheck.ExpectIdentity( - "examplecloud_thing.one", - 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), - }, - ), - }, - ), - }, - ExpectError: regexp.MustCompile(`examplecloud_thing.one - Expected 3 attribute\(s\) in the actual identity object, got 2 attribute\(s\): actual identity is missing attribute\(s\): "nonexistent_attr"`), - }, - }, - }) -} - -func TestExpectIdentity_CheckQuery_MismatchedAttribute(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryResultChecks: []querycheck.QueryResultCheck{ - querycheck.ExpectIdentity( - "examplecloud_thing.one", - map[string]knownvalue.Check{ - "not_id": knownvalue.StringExact("id-123"), - "list_of_numbers": knownvalue.ListExact( - []knownvalue.Check{ - knownvalue.Int64Exact(1), - knownvalue.Int64Exact(2), - knownvalue.Int64Exact(3), - knownvalue.Int64Exact(4), - }, - ), - }, - ), - }, - ExpectError: regexp.MustCompile(`examplecloud_thing.one - missing attribute "not_id" in actual identity object`), - }, - }, - }) -} diff --git a/querycheck/expect_identity_value.go b/querycheck/expect_identity_value.go deleted file mode 100644 index 3b940f6b7..000000000 --- a/querycheck/expect_identity_value.go +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-testing/knownvalue" - "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" -) - -var _ QueryResultCheck = expectIdentityValue{} - -type expectIdentityValue struct { - resourceAddress string - attributePath tfjsonpath.Path - identityValue knownvalue.Check -} - -// CheckQuery implements the query check logic. -func (e expectIdentityValue) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { - // var resource *tfjson.QueryResource - - if req.Query == nil { - resp.Error = fmt.Errorf("query is nil") - - return - } - - /* if req.Query.Values == nil { - resp.Error = fmt.Errorf("query does not contain any query values") - - return - } - - if req.Query.Values.RootModule == nil { - resp.Error = fmt.Errorf("query does not contain a root module") - - return - } - - for _, r := range req.Query.Values.RootModule.Resources { - if e.resourceAddress == r.Address { - resource = r - - break - } - } - - if resource == nil { - resp.Error = fmt.Errorf("%s - Resource not found in query", e.resourceAddress) - - return - } - - if resource.IdentitySchemaVersion == nil || len(resource.IdentityValues) == 0 { - resp.Error = fmt.Errorf("%s - Identity not found in query. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.12+)", e.resourceAddress) - - return - } - - result, err := tfjsonpath.Traverse(resource.IdentityValues, e.attributePath) - - if err != nil { - resp.Error = err - - return - }*/ - - /* if err := e.identityValue.CheckValue(result); err != nil { - resp.Error = fmt.Errorf("error checking identity value for attribute at path: %s.%s, err: %s", e.resourceAddress, e.attributePath.String(), err) - - return - }*/ - return -} - -// ExpectIdentityValue returns a query check that asserts that the specified identity attribute at the given resource -// matches a known value. This query check can only be used with managed resources that support resource identity. -// -// Resource identity is only supported in Terraform v1.12+ -func ExpectIdentityValue(resourceAddress string, attributePath tfjsonpath.Path, identityValue knownvalue.Check) QueryResultCheck { - return expectIdentityValue{ - resourceAddress: resourceAddress, - attributePath: attributePath, - identityValue: identityValue, - } -} diff --git a/querycheck/expect_identity_value_test.go b/querycheck/expect_identity_value_test.go deleted file mode 100644 index f999ff1b6..000000000 --- a/querycheck/expect_identity_value_test.go +++ /dev/null @@ -1,482 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package querycheck_test - -import ( - "regexp" - "testing" - - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-go/tftypes" - "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/list" - - 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/internal/testing/testsdk/resource" - "github.com/hashicorp/terraform-plugin-testing/internal/teststep" - "github.com/hashicorp/terraform-plugin-testing/knownvalue" - "github.com/hashicorp/terraform-plugin-testing/querycheck" - "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" - "github.com/hashicorp/terraform-plugin-testing/tfversion" -) - -func TestExpectIdentityValue_CheckQuery_ResourceNotFound(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryResultChecks: []querycheck.QueryResultCheck{ - querycheck.ExpectIdentityValue( - "examplecloud_thing.two", - tfjsonpath.New("id"), - knownvalue.StringExact("id-123"), - ), - }, - ExpectError: regexp.MustCompile("examplecloud_thing.two - Resource not found in query"), - }, - }, - }) -} - -func TestExpectIdentityValue_CheckQuery_No_Terraform_Identity_Support(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories - tfversion.SkipAbove(tfversion.Version1_11_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - // Resource support identity, but the Terraform versions running will not. - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryResultChecks: []querycheck.QueryResultCheck{ - querycheck.ExpectIdentityValue( - "examplecloud_thing.one", - tfjsonpath.New("id"), - knownvalue.StringExact("id-123"), - ), - }, - ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in query. Either the resource ` + - `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, - ), - }, - }, - }) -} - -func TestExpectIdentityValue_CheckQuery_No_Identity(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - // Resource does not support identity - "examplecloud": examplecloudProviderNoIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryResultChecks: []querycheck.QueryResultCheck{ - querycheck.ExpectIdentityValue( - "examplecloud_thing.one", - tfjsonpath.New("id"), - knownvalue.StringExact("id-123"), - ), - }, - ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in query. Either the resource ` + - `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, - ), - }, - }, - }) -} - -func TestExpectIdentityValue_CheckQuery_String(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryResultChecks: []querycheck.QueryResultCheck{ - querycheck.ExpectIdentityValue( - "examplecloud_thing.one", - tfjsonpath.New("id"), - knownvalue.StringExact("id-123")), - }, - }, - }, - }) -} - -func TestExpectIdentityValue_CheckQuery_String_KnownValueWrongType(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryResultChecks: []querycheck.QueryResultCheck{ - querycheck.ExpectIdentityValue( - "examplecloud_thing.one", - tfjsonpath.New("id"), - knownvalue.Bool(true)), - }, - ExpectError: regexp.MustCompile("expected bool value for Bool check, got: string"), - }, - }, - }) -} - -func TestExpectIdentityValue_CheckQuery_String_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryResultChecks: []querycheck.QueryResultCheck{ - querycheck.ExpectIdentityValue( - "examplecloud_thing.one", - tfjsonpath.New("id"), - knownvalue.StringExact("321-id")), - }, - ExpectError: regexp.MustCompile("expected value 321-id for StringExact check, got: id-123"), - }, - }, - }) -} - -func TestExpectIdentityValue_CheckQuery_List(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryResultChecks: []querycheck.QueryResultCheck{ - querycheck.ExpectIdentityValue( - "examplecloud_thing.one", - tfjsonpath.New("list_of_numbers").AtSliceIndex(0), - knownvalue.Int64Exact(1), - ), - querycheck.ExpectIdentityValue( - "examplecloud_thing.one", - tfjsonpath.New("list_of_numbers").AtSliceIndex(1), - knownvalue.Int64Exact(2), - ), - querycheck.ExpectIdentityValue( - "examplecloud_thing.one", - tfjsonpath.New("list_of_numbers").AtSliceIndex(2), - knownvalue.Int64Exact(3), - ), - querycheck.ExpectIdentityValue( - "examplecloud_thing.one", - tfjsonpath.New("list_of_numbers").AtSliceIndex(3), - knownvalue.Int64Exact(4), - ), - }, - }, - }, - }) -} - -func TestExpectIdentityValue_CheckQuery_List_KnownValueWrongType(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {} - `, - ConfigQueryResultChecks: []querycheck.QueryResultCheck{ - querycheck.ExpectIdentityValue( - "examplecloud_thing.one", - tfjsonpath.New("list_of_numbers"), - knownvalue.MapExact(map[string]knownvalue.Check{}), - ), - }, - ExpectError: regexp.MustCompile(`expected map\[string\]any value for MapExact check, got: \[\]interface {}`), - }, - }, - }) -} - -func TestExpectIdentityValue_CheckQuery_List_KnownValueWrongValue(t *testing.T) { - t.Parallel() - - r.Test(t, r.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_12_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "examplecloud": examplecloudProviderWithResourceIdentity(), - }, - Steps: []r.TestStep{ - { - Config: `resource "examplecloud_thing" "one" {}`, - ConfigQueryResultChecks: []querycheck.QueryResultCheck{ - querycheck.ExpectIdentityValue( - "examplecloud_thing.one", - tfjsonpath.New("list_of_numbers"), - knownvalue.ListExact([]knownvalue.Check{ - knownvalue.Int64Exact(4), - knownvalue.Int64Exact(3), - knownvalue.Int64Exact(2), - knownvalue.Int64Exact(1), - }), - ), - }, - ExpectError: regexp.MustCompile(`list element index 0: expected value 4 for Int64Exact check, got: 1`), - }, - }, - }) -} - -func examplecloudProviderWithResourceIdentity() func() (tfprotov6.ProviderServer, error) { - return providerserver.NewProviderServer(testprovider.Provider{ - ListResources: map[string]testprovider.ListResource{ - "examplecloud_containerette": { - SchemaResponse: &list.SchemaResponse{ - Schema: &tfprotov6.Schema{ - Block: &tfprotov6.SchemaBlock{ - Attributes: []*tfprotov6.SchemaAttribute{ - { - Name: "id", - }, - }, - }, - }, - }, - ListResultsStream: &list.ListResultsStream{ - Results: func(push func(list.ListResult) bool) { - - }, - }, - }, - }, - Resources: map[string]testprovider.Resource{ - "examplecloud_thing": { - CreateResponse: &resource.CreateResponse{ - NewQuery: tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "name": tftypes.String, - "id": tftypes.String, - "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, - }, - }, - map[string]tftypes.Value{ - "name": tftypes.NewValue(tftypes.String, "test value"), - "id": tftypes.NewValue(tftypes.String, "id-123"), - "list_of_numbers": tftypes.NewValue( - tftypes.List{ElementType: tftypes.Number}, - []tftypes.Value{ - tftypes.NewValue(tftypes.Number, 1), - tftypes.NewValue(tftypes.Number, 2), - tftypes.NewValue(tftypes.Number, 3), - tftypes.NewValue(tftypes.Number, 4), - }, - ), - }, - ), - NewIdentity: teststep.Pointer(tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, - }, - }, - map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "id-123"), - "list_of_numbers": tftypes.NewValue( - tftypes.List{ElementType: tftypes.Number}, - []tftypes.Value{ - tftypes.NewValue(tftypes.Number, 1), - tftypes.NewValue(tftypes.Number, 2), - tftypes.NewValue(tftypes.Number, 3), - tftypes.NewValue(tftypes.Number, 4), - }, - ), - }, - )), - }, - ReadResponse: &resource.ReadResponse{ - NewQuery: tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "name": tftypes.String, - "id": tftypes.String, - "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, - }, - }, - map[string]tftypes.Value{ - "name": tftypes.NewValue(tftypes.String, "test value"), - "id": tftypes.NewValue(tftypes.String, "id-123"), - "list_of_numbers": tftypes.NewValue( - tftypes.List{ElementType: tftypes.Number}, - []tftypes.Value{ - tftypes.NewValue(tftypes.Number, 1), - tftypes.NewValue(tftypes.Number, 2), - tftypes.NewValue(tftypes.Number, 3), - tftypes.NewValue(tftypes.Number, 4), - }, - ), - }, - ), - NewIdentity: teststep.Pointer(tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, - }, - }, - map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "id-123"), - "list_of_numbers": tftypes.NewValue( - tftypes.List{ElementType: tftypes.Number}, - []tftypes.Value{ - tftypes.NewValue(tftypes.Number, 1), - tftypes.NewValue(tftypes.Number, 2), - tftypes.NewValue(tftypes.Number, 3), - tftypes.NewValue(tftypes.Number, 4), - }, - ), - }, - )), - }, - IdentitySchemaResponse: &resource.IdentitySchemaResponse{ - Schema: &tfprotov6.ResourceIdentitySchema{ - IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ - { - Name: "id", - Type: tftypes.String, - RequiredForImport: true, - }, - { - Name: "list_of_numbers", - Type: tftypes.List{ElementType: tftypes.Number}, - OptionalForImport: true, - }, - }, - }, - }, - SchemaResponse: &resource.SchemaResponse{ - Schema: &tfprotov6.Schema{ - Block: &tfprotov6.SchemaBlock{ - Attributes: []*tfprotov6.SchemaAttribute{ - { - Name: "name", - Type: tftypes.String, - Computed: true, - }, - { - Name: "id", - Type: tftypes.String, - Computed: true, - }, - { - Name: "list_of_numbers", - Type: tftypes.List{ElementType: tftypes.Number}, - Computed: true, - }, - }, - }, - }, - }, - }, - }, - }) -} - -func examplecloudProviderNoIdentity() func() (tfprotov6.ProviderServer, error) { - return providerserver.NewProviderServer(testprovider.Provider{ - Resources: map[string]testprovider.Resource{ - "examplecloud_thing": { - CreateResponse: &resource.CreateResponse{ - NewQuery: tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "name": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "name": tftypes.NewValue(tftypes.String, "test value"), - }, - ), - }, - ReadResponse: &resource.ReadResponse{ - NewQuery: tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "name": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "name": tftypes.NewValue(tftypes.String, "test value"), - }, - ), - }, - SchemaResponse: &resource.SchemaResponse{ - Schema: &tfprotov6.Schema{ - Block: &tfprotov6.SchemaBlock{ - Attributes: []*tfprotov6.SchemaAttribute{ - { - Name: "name", - Type: tftypes.String, - Computed: true, - }, - }, - }, - }, - }, - }, - }, - }) -} From fd46aa779e365c8fd1054df5ea7544b8af463c6f Mon Sep 17 00:00:00 2001 From: Rain Date: Thu, 11 Sep 2025 14:49:31 -0400 Subject: [PATCH 31/45] Updated error messages --- internal/plugintest/working_dir.go | 30 ++++++++++------------ querycheck/expect_identity.go | 2 +- querycheck/expect_result_length_atleast.go | 2 +- querycheck/expect_result_length_exact.go | 2 +- 4 files changed, 17 insertions(+), 19 deletions(-) diff --git a/internal/plugintest/working_dir.go b/internal/plugintest/working_dir.go index 8631c6eb5..875d98113 100644 --- a/internal/plugintest/working_dir.go +++ b/internal/plugintest/working_dir.go @@ -9,6 +9,7 @@ import ( "io" "os" "path/filepath" + "strings" "github.com/hashicorp/terraform-exec/tfexec" tfjson "github.com/hashicorp/terraform-json" @@ -525,42 +526,39 @@ func (wd *WorkingDir) Schemas(ctx context.Context) (*tfjson.ProviderSchemas, err } func (wd *WorkingDir) Query(ctx context.Context) ([]tfjson.LogMsg, error) { - logging.HelperResourceTrace(ctx, "Calling Terraform CLI providers query command") - - args := []tfexec.QueryOption{tfexec.Reattach(wd.reattachInfo)} - var messages []tfjson.LogMsg - - // This returns a slice of log messages but is not expressed as a valid JSON array, so we're going to convert the - // buffer to a string, split this on new line then process each line individually since we're only interested in - // the list/query log messages var logEmit *tfexec.LogMsgEmitter var execErr, err error + var message tfjson.LogMsg + var related bool + + logging.HelperResourceTrace(ctx, "Calling Terraform CLI providers query command") + + args := []tfexec.QueryOption{tfexec.Reattach(wd.reattachInfo)} logEmit, execErr = wd.tf.QueryJSON(context.Background(), args...) if execErr != nil { - return nil, fmt.Errorf("error running terraform query command: %w", err) + return nil, fmt.Errorf("Error running terraform query command: %w", err) } - var message tfjson.LogMsg - var related bool - message, related, err = logEmit.NextMessage() if related == false && err != nil { - return nil, fmt.Errorf("error no messages found from terraform query command: %w", err) + return nil, fmt.Errorf("Error no messages found from terraform query command: %w", err) } - // possibly use iterator pattern here - for err != nil || message != nil { message, related, err = logEmit.NextMessage() messages = append(messages, message) + + if message != nil && strings.Contains(message.Message(), "Invalid provider configuration") { + return nil, fmt.Errorf("Provider requires explicit configuration. Add a provider block to the root module and configure the provider's required arguments as described in the provider documentation.") + } } if related == true { - return nil, fmt.Errorf("error running terraform query command: %w", err) + return nil, fmt.Errorf("Error running terraform query command: %w", err) } logging.HelperResourceTrace(ctx, "Called Terraform CLI providers query command") diff --git a/querycheck/expect_identity.go b/querycheck/expect_identity.go index 28c885cbf..837c0dc3f 100644 --- a/querycheck/expect_identity.go +++ b/querycheck/expect_identity.go @@ -26,7 +26,7 @@ func (e expectIdentity) CheckQuery(ctx context.Context, req CheckQueryRequest, r var foundIdentities []map[string]json.RawMessage if req.Query == nil { - resp.Error = fmt.Errorf("query is nil") + resp.Error = fmt.Errorf("Query is nil") return } diff --git a/querycheck/expect_result_length_atleast.go b/querycheck/expect_result_length_atleast.go index 9b1c27249..18ebfc1b6 100644 --- a/querycheck/expect_result_length_atleast.go +++ b/querycheck/expect_result_length_atleast.go @@ -21,7 +21,7 @@ type expectLengthAtLeast struct { // CheckQuery implements the query check logic. func (e expectLengthAtLeast) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { if req.Query == nil { - resp.Error = fmt.Errorf("query is nil") + resp.Error = fmt.Errorf("Query is nil") return } diff --git a/querycheck/expect_result_length_exact.go b/querycheck/expect_result_length_exact.go index e7e3a7c8e..e0a13a6c4 100644 --- a/querycheck/expect_result_length_exact.go +++ b/querycheck/expect_result_length_exact.go @@ -24,7 +24,7 @@ type expectLength struct { // CheckQuery implements the query check logic. func (e expectLength) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { if req.Query == nil { - resp.Error = fmt.Errorf("query is nil") + resp.Error = fmt.Errorf("Query is nil") return } From 02d6caaa0feea7800849fe5bcca8e1de1abff5dd Mon Sep 17 00:00:00 2001 From: Rain Date: Thu, 11 Sep 2025 14:51:19 -0400 Subject: [PATCH 32/45] Updated comment --- helper/resource/query/query_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/helper/resource/query/query_test.go b/helper/resource/query/query_test.go index 67bb4f7e7..2c480e72a 100644 --- a/helper/resource/query/query_test.go +++ b/helper/resource/query/query_test.go @@ -33,7 +33,7 @@ func TestQuery(t *testing.T) { }), }, Steps: []r.TestStep{ - { // config mode step 1, creates something we can list later, need tf file with terraform providers block + { // config mode step 1 needs tf file with terraform providers block Config: ` resource "examplecloud_containerette" "primary" { id = "westeurope/somevalue" @@ -45,7 +45,6 @@ func TestQuery(t *testing.T) { // ```provider "examplecloud" {}``` has a slightly different syntax for a .tfquery.hcl file // provider bock simulates a real providers workflow // "config" in this case means configuration of the list resource/filters - // run query in terraform itself maybe by moving this provider to the corner provider to check if it works Query: true, Config: ` From 3331b2b5438f2caac55df2b893a63d1ab692ad6e Mon Sep 17 00:00:00 2001 From: Steph Date: Fri, 12 Sep 2025 13:27:44 +0200 Subject: [PATCH 33/45] add contains query check for checking if a given resource exists in the query results --- helper/resource/query/query_test.go | 1 + querycheck/contains.go | 65 +++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 querycheck/contains.go diff --git a/helper/resource/query/query_test.go b/helper/resource/query/query_test.go index 2c480e72a..717db84ef 100644 --- a/helper/resource/query/query_test.go +++ b/helper/resource/query/query_test.go @@ -134,6 +134,7 @@ func TestQuery(t *testing.T) { ConfigQueryResultChecks: []querycheck.QueryResultCheck{ querycheck.ExpectLengthAtLeast("examplecloud_containerette.test", 2), querycheck.ExpectLengthAtLeast("examplecloud_containerette.test2", 1), + querycheck.Contains(), }, }, }, diff --git a/querycheck/contains.go b/querycheck/contains.go new file mode 100644 index 000000000..af567a90e --- /dev/null +++ b/querycheck/contains.go @@ -0,0 +1,65 @@ +package querycheck + +import ( + "context" + "fmt" + "strings" + + tfjson "github.com/hashicorp/terraform-json" +) + +var _ QueryResultCheck = contains{} + +type contains struct { + resourceAddress string + check string +} + +func (c contains) CheckQuery(_ context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { + // TODO refactor below + foundResources := make([]string, 0) + + if req.Query == nil { + resp.Error = fmt.Errorf("Query is nil") + return + } + + for _, v := range *req.Query { + switch i := v.(type) { + case tfjson.ListResourceFoundMessage: + prefix := "list." + if strings.TrimPrefix(i.ListResourceFound.Address, prefix) == c.resourceAddress { + foundResources = append(foundResources, i.ListResourceFound.DisplayName) + } + default: + continue + } + } + + if len(foundResources) == 0 { + resp.Error = fmt.Errorf("%s - no resources found by query.", c.resourceAddress) + + return + } + // TODO refactor above + + for _, res := range foundResources { + if c.check == res { + return + } + } + + resp.Error = fmt.Errorf("expected to find resource with display name %q in results but resource was not found", c.check) + + return +} + +// Contains 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 Contains(resourceAddress string, displayName string) QueryResultCheck { + return contains{ + resourceAddress: resourceAddress, + check: displayName, + } +} From c465a33e8c6de724b76b40f5c08e9c9d05ad7bed Mon Sep 17 00:00:00 2001 From: Steph Date: Fri, 12 Sep 2025 14:05:42 +0200 Subject: [PATCH 34/45] update CheckQueryRequest to provide ListResourceFoundData and ListCompleteData and refactor a little --- helper/resource/query/query_checks.go | 29 ++++++++++++++++- helper/resource/query/query_test.go | 1 - querycheck/contains.go | 34 ++----------------- querycheck/expect_identity.go | 38 +++------------------- querycheck/expect_result_length_atleast.go | 30 ++++------------- querycheck/expect_result_length_exact.go | 32 +++++------------- querycheck/query_check.go | 5 ++- 7 files changed, 53 insertions(+), 116 deletions(-) diff --git a/helper/resource/query/query_checks.go b/helper/resource/query/query_checks.go index 5f3583eae..ca60258c3 100644 --- a/helper/resource/query/query_checks.go +++ b/helper/resource/query/query_checks.go @@ -6,6 +6,7 @@ package query import ( "context" "errors" + "fmt" tfjson "github.com/hashicorp/terraform-json" "github.com/mitchellh/go-testing-interface" @@ -18,9 +19,35 @@ func RunQueryChecks(ctx context.Context, t testing.T, query *[]tfjson.LogMsg, qu var result []error + if query == nil || len(*query) == 0 { + // TODO return error + } + + found := make([]tfjson.ListResourceFoundData, 0) + complete := tfjson.ListCompleteData{} + + for _, msg := range *query { + switch v := msg.(type) { + case tfjson.ListResourceFoundMessage: + found = append(found, v.ListResourceFound) + case tfjson.ListCompleteMessage: + complete = v.ListComplete + // TODO diagnostics and errors? + } + } + + // TODO check diagnostics in LogMsg to see if there are any errors we can return here? + var err error + if len(found) == 0 { + return fmt.Errorf("no resources found by query: %+v", err) + } + for _, queryCheck := range queryChecks { resp := querycheck.CheckQueryResponse{} - queryCheck.CheckQuery(ctx, querycheck.CheckQueryRequest{Query: query}, &resp) + queryCheck.CheckQuery(ctx, querycheck.CheckQueryRequest{ + Query: &found, + CompletedQuery: &complete, + }, &resp) result = append(result, resp.Error) } diff --git a/helper/resource/query/query_test.go b/helper/resource/query/query_test.go index 717db84ef..2c480e72a 100644 --- a/helper/resource/query/query_test.go +++ b/helper/resource/query/query_test.go @@ -134,7 +134,6 @@ func TestQuery(t *testing.T) { ConfigQueryResultChecks: []querycheck.QueryResultCheck{ querycheck.ExpectLengthAtLeast("examplecloud_containerette.test", 2), querycheck.ExpectLengthAtLeast("examplecloud_containerette.test2", 1), - querycheck.Contains(), }, }, }, diff --git a/querycheck/contains.go b/querycheck/contains.go index af567a90e..06c8f6010 100644 --- a/querycheck/contains.go +++ b/querycheck/contains.go @@ -4,8 +4,6 @@ import ( "context" "fmt" "strings" - - tfjson "github.com/hashicorp/terraform-json" ) var _ QueryResultCheck = contains{} @@ -16,37 +14,11 @@ type contains struct { } func (c contains) CheckQuery(_ context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { - // TODO refactor below - foundResources := make([]string, 0) - - if req.Query == nil { - resp.Error = fmt.Errorf("Query is nil") - return - } - - for _, v := range *req.Query { - switch i := v.(type) { - case tfjson.ListResourceFoundMessage: - prefix := "list." - if strings.TrimPrefix(i.ListResourceFound.Address, prefix) == c.resourceAddress { - foundResources = append(foundResources, i.ListResourceFound.DisplayName) - } - default: - continue - } - } - - if len(foundResources) == 0 { - resp.Error = fmt.Errorf("%s - no resources found by query.", c.resourceAddress) - - return - } - // TODO refactor above - - for _, res := range foundResources { - if c.check == res { + 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) diff --git a/querycheck/expect_identity.go b/querycheck/expect_identity.go index 837c0dc3f..db08b8874 100644 --- a/querycheck/expect_identity.go +++ b/querycheck/expect_identity.go @@ -8,9 +8,7 @@ import ( "encoding/json" "errors" "fmt" - "strings" - tfjson "github.com/hashicorp/terraform-json" "github.com/hashicorp/terraform-plugin-testing/knownvalue" ) @@ -23,42 +21,15 @@ type expectIdentity struct { // CheckQuery implements the query check logic. func (e expectIdentity) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { - var foundIdentities []map[string]json.RawMessage - - if req.Query == nil { - resp.Error = fmt.Errorf("Query is nil") - return - } - - for _, v := range *req.Query { - switch i := v.(type) { - case tfjson.ListResourceFoundMessage: - prefix := "list." - if strings.TrimPrefix(i.ListResourceFound.Address, prefix) == e.resourceAddress { - foundIdentities = append(foundIdentities, i.ListResourceFound.Identity) - } - default: - continue - } - } - - if len(foundIdentities) == 0 { - resp.Error = fmt.Errorf("%s - Identity not found in query.", e.resourceAddress) - - return - } - - var err error - - for _, resultIdentity := range foundIdentities { + for _, res := range *req.Query { var errCollection []error for attribute := range e.check { var val any - var ok bool var unmarshalledVal any - if val, ok = resultIdentity[attribute]; !ok { + val, ok := res.Identity[attribute] + if !ok { resp.Error = fmt.Errorf("%s - expected attribute %q not in actual identity object", e.resourceAddress, attribute) return } @@ -68,7 +39,7 @@ func (e expectIdentity) CheckQuery(ctx context.Context, req CheckQueryRequest, r resp.Error = fmt.Errorf("%s - expected json.RawMessage but got %T", e.resourceAddress, val) return } - err = json.Unmarshal(rawMessage, &unmarshalledVal) + err := json.Unmarshal(rawMessage, &unmarshalledVal) if err != nil { resp.Error = fmt.Errorf("%s - Error decoding message type: %s", e.resourceAddress, err) @@ -79,7 +50,6 @@ func (e expectIdentity) CheckQuery(ctx context.Context, req CheckQueryRequest, r errCollection = append(errCollection, fmt.Errorf("%s - %q identity attribute: %s\n", e.resourceAddress, e.check, err)) } } - if errCollection == nil { return } diff --git a/querycheck/expect_result_length_atleast.go b/querycheck/expect_result_length_atleast.go index 18ebfc1b6..2a2ec1cc7 100644 --- a/querycheck/expect_result_length_atleast.go +++ b/querycheck/expect_result_length_atleast.go @@ -6,9 +6,6 @@ package querycheck import ( "context" "fmt" - "strings" - - tfjson "github.com/hashicorp/terraform-json" ) var _ QueryResultCheck = expectLengthAtLeast{} @@ -19,32 +16,17 @@ type expectLengthAtLeast struct { } // CheckQuery implements the query check logic. -func (e expectLengthAtLeast) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { - if req.Query == nil { - resp.Error = fmt.Errorf("Query is nil") +func (e expectLengthAtLeast) CheckQuery(_ context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { + if req.CompletedQuery == nil { + resp.Error = fmt.Errorf("no completed query information available") return } - for _, v := range *req.Query { - switch i := v.(type) { - case tfjson.ListCompleteMessage: - prefix := "list." - - if strings.TrimPrefix(i.ListComplete.Address, prefix) == e.resourceAddress { - if i.ListComplete.Total < e.check { - resp.Error = fmt.Errorf("Query result of at least length %v - expected but got %v.", e.check, i.ListComplete.Total) - return - } else { - return - } - } - default: - continue - } + if req.CompletedQuery.Total < e.check { + resp.Error = fmt.Errorf("Query result of at least length %v - expected but got %v.", e.check, req.CompletedQuery.Total) + return } - resp.Error = fmt.Errorf("%s - Address not found in query result.", e.resourceAddress) - return } diff --git a/querycheck/expect_result_length_exact.go b/querycheck/expect_result_length_exact.go index e0a13a6c4..0ddda2abc 100644 --- a/querycheck/expect_result_length_exact.go +++ b/querycheck/expect_result_length_exact.go @@ -8,9 +8,7 @@ import ( "encoding/json" "fmt" "strconv" - "strings" - tfjson "github.com/hashicorp/terraform-json" "github.com/hashicorp/terraform-plugin-testing/knownvalue" ) @@ -22,32 +20,18 @@ type expectLength struct { } // CheckQuery implements the query check logic. -func (e expectLength) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { - if req.Query == nil { - resp.Error = fmt.Errorf("Query is nil") +func (e expectLength) CheckQuery(_ context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { + if req.CompletedQuery == nil { + resp.Error = fmt.Errorf("no completed query information available") return } - for _, v := range *req.Query { - switch i := v.(type) { - case tfjson.ListCompleteMessage: - prefix := "list." - lengthCheck := json.Number((strconv.Itoa(i.ListComplete.Total))) - - if strings.TrimPrefix(i.ListComplete.Address, prefix) == e.resourceAddress { - if err := e.check.CheckValue(lengthCheck); err != nil { - resp.Error = fmt.Errorf("Query result of length %v - expected but got %v.", e.check, i.ListComplete.Total) - return - } else { - return - } - } - default: - continue - } - } + lengthCheck := json.Number(strconv.Itoa(req.CompletedQuery.Total)) - resp.Error = fmt.Errorf("%s - Address not found in query result.", e.resourceAddress) + if err := e.check.CheckValue(lengthCheck); err != nil { + resp.Error = fmt.Errorf("Query result of length %v - expected but got %v.", e.check, req.CompletedQuery.Total) + return + } return } diff --git a/querycheck/query_check.go b/querycheck/query_check.go index bdb7fb234..8c60cabc7 100644 --- a/querycheck/query_check.go +++ b/querycheck/query_check.go @@ -19,7 +19,10 @@ type QueryResultCheck interface { // CheckQueryRequest is a request for an invoke of the CheckQuery function. type CheckQueryRequest struct { // Query represents a parsed query file, retrieved via the `terraform show -json` command. - Query *[]tfjson.LogMsg + Query *[]tfjson.ListResourceFoundData + + // CompletedQuery contains a summary of the completed query operation. + CompletedQuery *tfjson.ListCompleteData } // CheckQueryResponse is a response to an invoke of the CheckQuery function. From 10dd78cd0d5f26293ebb9337d440a7e3f3f070b4 Mon Sep 17 00:00:00 2001 From: Steph Date: Tue, 16 Sep 2025 16:21:23 +0200 Subject: [PATCH 35/45] add expect known value check --- querycheck/expect_known_value.go | 77 ++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 querycheck/expect_known_value.go diff --git a/querycheck/expect_known_value.go b/querycheck/expect_known_value.go new file mode 100644 index 000000000..5391ca4bf --- /dev/null +++ b/querycheck/expect_known_value.go @@ -0,0 +1,77 @@ +package querycheck + +import ( + "context" + "encoding/json" + "fmt" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "strings" +) + +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 { + diags := make([]error, 0) + + 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 + } + + // Ideally we can do the check like below which is identical to the expect known value state check but... terraform-json hasn't + // defined the resource object as a map[string]interface, we so we need to iterate over it manually + //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 + //} + + for k, v := range res.ResourceObject { + if k == e.attributePath.String() { + var val any + + err := json.Unmarshal(v, &val) + if err != nil { + resp.Error = fmt.Errorf("%s - Error decoding message type: %s", e.listResourceAddress, err) + return + } + + if err := e.knownValue.CheckValue(val); 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)) + } + } + } + } + } + + resp.Error = fmt.Errorf("%s - the resource %s was not found", e.listResourceAddress, e.resourceName) + + return +} + +func ExpectKnownValue(listResourceAddress string, resourceName string, attributePath tfjsonpath.Path, knownValue knownvalue.Check) QueryResultCheck { + return expectKnownValue{ + listResourceAddress: listResourceAddress, + resourceName: resourceName, + attributePath: attributePath, + knownValue: knownValue, + } +} From 2858c75ac5d071c297d3a60706078c3b5b6417f6 Mon Sep 17 00:00:00 2001 From: Rain Date: Tue, 16 Sep 2025 12:15:42 -0400 Subject: [PATCH 36/45] Updated comment --- internal/plugintest/working_dir.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/plugintest/working_dir.go b/internal/plugintest/working_dir.go index 5ee97db2b..875d98113 100644 --- a/internal/plugintest/working_dir.go +++ b/internal/plugintest/working_dir.go @@ -4,7 +4,6 @@ package plugintest import ( - "bytes" "context" "fmt" "io" From 9e1d04107a89dac38a5899999f79d5ea88031f26 Mon Sep 17 00:00:00 2001 From: Rain Date: Tue, 16 Sep 2025 12:19:29 -0400 Subject: [PATCH 37/45] Ran make generate --- helper/resource/query/examplecloud_list_test.go | 3 +++ querycheck/contains.go | 3 +++ querycheck/expect_known_value.go | 3 +++ 3 files changed, 9 insertions(+) diff --git a/helper/resource/query/examplecloud_list_test.go b/helper/resource/query/examplecloud_list_test.go index 47303dc2a..7b18943ef 100644 --- a/helper/resource/query/examplecloud_list_test.go +++ b/helper/resource/query/examplecloud_list_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package query_test import ( diff --git a/querycheck/contains.go b/querycheck/contains.go index 06c8f6010..59de62c92 100644 --- a/querycheck/contains.go +++ b/querycheck/contains.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package querycheck import ( diff --git a/querycheck/expect_known_value.go b/querycheck/expect_known_value.go index 5391ca4bf..1ac0c8945 100644 --- a/querycheck/expect_known_value.go +++ b/querycheck/expect_known_value.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package querycheck import ( From 3da6e28c5d118d340c8345039c05e88ddc4fd8a3 Mon Sep 17 00:00:00 2001 From: Rain Date: Tue, 16 Sep 2025 12:56:57 -0400 Subject: [PATCH 38/45] Fixed lintier --- helper/resource/query/query_checks.go | 4 +++- internal/plugintest/working_dir.go | 4 ++-- .../testsdk/providerserver/providerserver.go | 4 ++-- querycheck/contains.go | 2 -- querycheck/expect_identity.go | 2 -- querycheck/expect_known_value.go | 14 +++++++++++--- querycheck/expect_result_length_atleast.go | 2 -- querycheck/expect_result_length_exact.go | 2 -- 8 files changed, 18 insertions(+), 16 deletions(-) diff --git a/helper/resource/query/query_checks.go b/helper/resource/query/query_checks.go index ca60258c3..10beadb83 100644 --- a/helper/resource/query/query_checks.go +++ b/helper/resource/query/query_checks.go @@ -20,7 +20,7 @@ func RunQueryChecks(ctx context.Context, t testing.T, query *[]tfjson.LogMsg, qu var result []error if query == nil || len(*query) == 0 { - // TODO return error + result = append(result, fmt.Errorf("No query results found")) } found := make([]tfjson.ListResourceFoundData, 0) @@ -33,6 +33,8 @@ func RunQueryChecks(ctx context.Context, t testing.T, query *[]tfjson.LogMsg, qu case tfjson.ListCompleteMessage: complete = v.ListComplete // TODO diagnostics and errors? + default: + // ignore other message types } } diff --git a/internal/plugintest/working_dir.go b/internal/plugintest/working_dir.go index 875d98113..09a285ae6 100644 --- a/internal/plugintest/working_dir.go +++ b/internal/plugintest/working_dir.go @@ -544,7 +544,7 @@ func (wd *WorkingDir) Query(ctx context.Context) ([]tfjson.LogMsg, error) { message, related, err = logEmit.NextMessage() - if related == false && err != nil { + if !related && err != nil { return nil, fmt.Errorf("Error no messages found from terraform query command: %w", err) } @@ -557,7 +557,7 @@ func (wd *WorkingDir) Query(ctx context.Context) ([]tfjson.LogMsg, error) { } } - if related == true { + if related { return nil, fmt.Errorf("Error running terraform query command: %w", err) } diff --git a/internal/testing/testsdk/providerserver/providerserver.go b/internal/testing/testsdk/providerserver/providerserver.go index d7b4abb5f..0f14d0dac 100644 --- a/internal/testing/testsdk/providerserver/providerserver.go +++ b/internal/testing/testsdk/providerserver/providerserver.go @@ -1053,7 +1053,7 @@ func (s ProviderServer) ListResource(ctx context.Context, req *tfprotov6.ListRes return nil, fmt.Errorf("failed to retrieve resource: %v", err) } r.IdentitySchema(ctx, identitySchemaReq, identitySchemaResp) - if identitySchemaResp.Diagnostics != nil && len(identitySchemaResp.Diagnostics) > 0 { + if len(identitySchemaResp.Diagnostics) > 0 { return nil, fmt.Errorf("failed to retrieve resource schema: %v", identitySchemaResp.Diagnostics) } @@ -1066,7 +1066,7 @@ func (s ProviderServer) ListResource(ctx context.Context, req *tfprotov6.ListRes schemaResp := &list.SchemaResponse{} listresource.Schema(ctx, schemaReq, schemaResp) - if schemaResp.Diagnostics != nil && len(schemaResp.Diagnostics) > 0 { + if len(schemaResp.Diagnostics) > 0 { return nil, fmt.Errorf("failed to retrieve resource schema: %v", schemaResp.Diagnostics) } diff --git a/querycheck/contains.go b/querycheck/contains.go index 59de62c92..10df580fd 100644 --- a/querycheck/contains.go +++ b/querycheck/contains.go @@ -21,12 +21,10 @@ func (c contains) CheckQuery(_ context.Context, req CheckQueryRequest, resp *Che 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) - return } // Contains returns a query check that asserts that a resource with a given display name exists within the returned results of the query. diff --git a/querycheck/expect_identity.go b/querycheck/expect_identity.go index db08b8874..d79b74424 100644 --- a/querycheck/expect_identity.go +++ b/querycheck/expect_identity.go @@ -66,8 +66,6 @@ func (e expectIdentity) CheckQuery(ctx context.Context, req CheckQueryRequest, r errCollection = append(errCollection, fmt.Errorf("Address: %s\n", e.resourceAddress)) resp.Error = errors.Join(errCollection...) - - return } // ExpectIdentity returns a query check that asserts that the identity at the given resource matches a known object, where each diff --git a/querycheck/expect_known_value.go b/querycheck/expect_known_value.go index 1ac0c8945..390d19867 100644 --- a/querycheck/expect_known_value.go +++ b/querycheck/expect_known_value.go @@ -7,9 +7,10 @@ import ( "context" "encoding/json" "fmt" + "strings" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" - "strings" ) var _ QueryResultCheck = expectKnownValue{} @@ -63,11 +64,18 @@ func (e expectKnownValue) CheckQuery(_ context.Context, req CheckQueryRequest, r } } } + + if diags != nil { + var diagsStr string + for _, diag := range diags { + diagsStr += diag.Error() + "; " + } + resp.Error = fmt.Errorf(diagsStr) + return + } } resp.Error = fmt.Errorf("%s - the resource %s was not found", e.listResourceAddress, e.resourceName) - - return } func ExpectKnownValue(listResourceAddress string, resourceName string, attributePath tfjsonpath.Path, knownValue knownvalue.Check) QueryResultCheck { diff --git a/querycheck/expect_result_length_atleast.go b/querycheck/expect_result_length_atleast.go index 2a2ec1cc7..ebbb6ed4a 100644 --- a/querycheck/expect_result_length_atleast.go +++ b/querycheck/expect_result_length_atleast.go @@ -26,8 +26,6 @@ func (e expectLengthAtLeast) CheckQuery(_ context.Context, req CheckQueryRequest resp.Error = fmt.Errorf("Query result of at least length %v - expected but got %v.", e.check, req.CompletedQuery.Total) return } - - return } // ExpectLengthAtLeast returns a query check that asserts that the length of the query result is at least the given value. diff --git a/querycheck/expect_result_length_exact.go b/querycheck/expect_result_length_exact.go index 0ddda2abc..278ada1ff 100644 --- a/querycheck/expect_result_length_exact.go +++ b/querycheck/expect_result_length_exact.go @@ -32,8 +32,6 @@ func (e expectLength) CheckQuery(_ context.Context, req CheckQueryRequest, resp resp.Error = fmt.Errorf("Query result of length %v - expected but got %v.", e.check, req.CompletedQuery.Total) return } - - return } // ExpectLength returns a query check that asserts that the length of the query result is exactly the given value. From 695a600649da23d57ffcd5df195fd18d4009c537 Mon Sep 17 00:00:00 2001 From: Rain Date: Tue, 16 Sep 2025 13:02:54 -0400 Subject: [PATCH 39/45] Fixed lintier --- querycheck/expect_known_value.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/querycheck/expect_known_value.go b/querycheck/expect_known_value.go index 390d19867..9b09b3e5d 100644 --- a/querycheck/expect_known_value.go +++ b/querycheck/expect_known_value.go @@ -70,7 +70,7 @@ func (e expectKnownValue) CheckQuery(_ context.Context, req CheckQueryRequest, r for _, diag := range diags { diagsStr += diag.Error() + "; " } - resp.Error = fmt.Errorf(diagsStr) + resp.Error = fmt.Errorf("The following errors were found while checking values: %s" + diagsStr) return } } From 0bebb21af5031516609076e89fee0798057f4f38 Mon Sep 17 00:00:00 2001 From: Steph Date: Wed, 17 Sep 2025 13:47:35 +0200 Subject: [PATCH 40/45] update terraform-json and terraform-exec dependencies and update query checks to be consistent with existing statechecks --- go.mod | 4 +- go.sum | 8 ++-- helper/resource/query/query_test.go | 9 ++-- internal/plugintest/working_dir.go | 42 +++++++---------- querycheck/expect_identity.go | 71 +++++++++++++++++------------ querycheck/expect_known_value.go | 44 ++++++------------ statecheck/expect_identity.go | 8 ++-- 7 files changed, 91 insertions(+), 95 deletions(-) diff --git a/go.mod b/go.mod index 774712fed..f82f0a060 100644 --- a/go.mod +++ b/go.mod @@ -13,8 +13,8 @@ require ( github.com/hashicorp/hc-install v0.9.2 github.com/hashicorp/hcl/v2 v2.24.0 github.com/hashicorp/logutils v1.0.0 - github.com/hashicorp/terraform-exec v0.23.2-0.20250903143921-c05687bce503 - github.com/hashicorp/terraform-json v0.26.1-0.20250829125600-5c1a00f3ccc4 + github.com/hashicorp/terraform-exec v0.24.0 + github.com/hashicorp/terraform-json v0.27.2 github.com/hashicorp/terraform-plugin-go v0.29.0-beta.1 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0 diff --git a/go.sum b/go.sum index ac3edcbd6..9a1d32d19 100644 --- a/go.sum +++ b/go.sum @@ -76,10 +76,10 @@ github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQx github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/terraform-exec v0.23.2-0.20250903143921-c05687bce503 h1:A/cbqqEjJmPouKHgWlIBQOBPROLO4ubPmHFl7yKZ+As= -github.com/hashicorp/terraform-exec v0.23.2-0.20250903143921-c05687bce503/go.mod h1:rG9V56jmBbB5hXCo4MpZTr6tYKLaykUONa8mX01yBhg= -github.com/hashicorp/terraform-json v0.26.1-0.20250829125600-5c1a00f3ccc4 h1:qErXY0TfojskxvAlCiqS4IMmXulQ4TfApiO8IlKkImc= -github.com/hashicorp/terraform-json v0.26.1-0.20250829125600-5c1a00f3ccc4/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE= +github.com/hashicorp/terraform-exec v0.24.0 h1:mL0xlk9H5g2bn0pPF6JQZk5YlByqSqrO5VoaNtAf8OE= +github.com/hashicorp/terraform-exec v0.24.0/go.mod h1:lluc/rDYfAhYdslLJQg3J0oDqo88oGQAdHR+wDqFvo4= +github.com/hashicorp/terraform-json v0.27.2 h1:BwGuzM6iUPqf9JYM/Z4AF1OJ5VVJEEzoKST/tRDBJKU= +github.com/hashicorp/terraform-json v0.27.2/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE= github.com/hashicorp/terraform-plugin-go v0.29.0-beta.1 h1:xeHlRQYev3iMXwX2W7+D1bSfLRBs9jojZXqE6hmNxMI= github.com/hashicorp/terraform-plugin-go v0.29.0-beta.1/go.mod h1:5pww/UULn9C2tItq6o5sbScEkJxBUt9X9kI4DkeRsIw= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= diff --git a/helper/resource/query/query_test.go b/helper/resource/query/query_test.go index 2c480e72a..c738d1c5c 100644 --- a/helper/resource/query/query_test.go +++ b/helper/resource/query/query_test.go @@ -78,13 +78,16 @@ func TestQuery(t *testing.T) { "location": knownvalue.StringExact("westeurope3"), }), querycheck.ExpectIdentity("examplecloud_containerette.test2", map[string]knownvalue.Check{ - "id": knownvalue.StringExact("westeurope/somevalue1"), + "id": knownvalue.StringExact("westeurope/somevalue1"), + "location": knownvalue.StringExact("westeurope"), }), querycheck.ExpectIdentity("examplecloud_containerette.test2", map[string]knownvalue.Check{ - "id": knownvalue.StringExact("westeurope/somevalue2"), + "id": knownvalue.StringExact("westeurope/somevalue2"), + "location": knownvalue.StringExact("westeurope"), }), querycheck.ExpectIdentity("examplecloud_containerette.test2", map[string]knownvalue.Check{ - "id": knownvalue.StringExact("westeurope/somevalue3"), + "id": knownvalue.StringExact("westeurope/somevalue3"), + "location": knownvalue.StringExact("westeurope"), }), }, }, diff --git a/internal/plugintest/working_dir.go b/internal/plugintest/working_dir.go index 09a285ae6..332da11ee 100644 --- a/internal/plugintest/working_dir.go +++ b/internal/plugintest/working_dir.go @@ -6,13 +6,11 @@ package plugintest import ( "context" "fmt" + "github.com/hashicorp/terraform-exec/tfexec" + tfjson "github.com/hashicorp/terraform-json" "io" "os" "path/filepath" - "strings" - - "github.com/hashicorp/terraform-exec/tfexec" - tfjson "github.com/hashicorp/terraform-json" "github.com/hashicorp/terraform-plugin-testing/config" "github.com/hashicorp/terraform-plugin-testing/internal/logging" @@ -527,38 +525,32 @@ func (wd *WorkingDir) Schemas(ctx context.Context) (*tfjson.ProviderSchemas, err func (wd *WorkingDir) Query(ctx context.Context) ([]tfjson.LogMsg, error) { var messages []tfjson.LogMsg - var logEmit *tfexec.LogMsgEmitter - var execErr, err error - var message tfjson.LogMsg - var related bool + var diags []tfjson.LogMsg logging.HelperResourceTrace(ctx, "Calling Terraform CLI providers query command") args := []tfexec.QueryOption{tfexec.Reattach(wd.reattachInfo)} - logEmit, execErr = wd.tf.QueryJSON(context.Background(), args...) + logs, err := wd.tf.QueryJSON(context.Background(), args...) - if execErr != nil { - return nil, fmt.Errorf("Error running terraform query command: %w", err) - } - - message, related, err = logEmit.NextMessage() - - if !related && err != nil { - return nil, fmt.Errorf("Error no messages found from terraform query command: %w", err) + if err != nil { + return nil, fmt.Errorf("running terraform query command: %w", err) } - for err != nil || message != nil { - message, related, err = logEmit.NextMessage() - messages = append(messages, message) - - if message != nil && strings.Contains(message.Message(), "Invalid provider configuration") { - return nil, fmt.Errorf("Provider requires explicit configuration. Add a provider block to the root module and configure the provider's required arguments as described in the provider documentation.") + for msg := range logs { + if msg.Err != nil { + return nil, fmt.Errorf("retrieving next message: %w", msg.Err) + } + if msg.Msg.Level() == tfjson.Error { + // TODO reimplement missing .tf config error + diags = append(diags, msg.Msg) + continue } + messages = append(messages, msg.Msg) } - if related { - return nil, fmt.Errorf("Error running terraform query command: %w", err) + if len(diags) > 0 { + return nil, fmt.Errorf("running terraform query command returned diagnostics: %+v", diags) } logging.HelperResourceTrace(ctx, "Called Terraform CLI providers query command") diff --git a/querycheck/expect_identity.go b/querycheck/expect_identity.go index d79b74424..dd06bf817 100644 --- a/querycheck/expect_identity.go +++ b/querycheck/expect_identity.go @@ -5,67 +5,82 @@ package querycheck import ( "context" - "encoding/json" "errors" "fmt" + "sort" + "strings" "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" ) var _ QueryResultCheck = expectIdentity{} type expectIdentity struct { - resourceAddress string - check map[string]knownvalue.Check + listResourceAddress string + check map[string]knownvalue.Check } // CheckQuery implements the query check logic. -func (e expectIdentity) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { +func (e expectIdentity) CheckQuery(_ context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { for _, res := range *req.Query { var errCollection []error - for attribute := range e.check { - var val any - var unmarshalledVal any + if e.listResourceAddress != strings.TrimPrefix(res.Address, "list.") { + continue + } - val, ok := res.Identity[attribute] - if !ok { - resp.Error = fmt.Errorf("%s - expected attribute %q not in actual identity object", e.resourceAddress, attribute) - return + if len(res.Identity) != len(e.check) { + deltaMsg := "" + if len(res.Identity) > len(e.check) { + deltaMsg = statecheck.CreateDeltaString(res.Identity, e.check, "actual identity has extra attribute(s): ") + } else { + deltaMsg = statecheck.CreateDeltaString(e.check, res.Identity, "actual identity is missing attribute(s): ") } - rawMessage, ok := val.(json.RawMessage) - if !ok { - resp.Error = fmt.Errorf("%s - expected json.RawMessage but got %T", e.resourceAddress, val) - return - } - err := json.Unmarshal(rawMessage, &unmarshalledVal) + resp.Error = fmt.Errorf("%s - Expected %d attribute(s) in the actual identity object, got %d attribute(s): %s", e.listResourceAddress, len(e.check), len(res.Identity), deltaMsg) + return + } + + var keys []string - if err != nil { - resp.Error = fmt.Errorf("%s - Error decoding message type: %s", e.resourceAddress, err) + for k := range e.check { + keys = append(keys, k) + } + + sort.SliceStable(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + for _, k := range keys { + actualIdentityVal, ok := res.Identity[k] + + if !ok { + resp.Error = fmt.Errorf("%s - missing attribute %q in actual identity object", e.listResourceAddress, k) return } - if err = e.check[attribute].CheckValue(unmarshalledVal); err != nil { - errCollection = append(errCollection, fmt.Errorf("%s - %q identity attribute: %s\n", e.resourceAddress, e.check, err)) + if err := e.check[k].CheckValue(actualIdentityVal); err != nil { + errCollection = append(errCollection, fmt.Errorf("%s - %q identity attribute: %s", e.listResourceAddress, k, err)) } } + if errCollection == nil { return } } var errCollection []error - - errCollection = append(errCollection, fmt.Errorf("An identity with the following attributes was not found:")) + errCollection = append(errCollection, fmt.Errorf("an identity with the following attributes was not found")) // wrap errors for each check for attr, check := range e.check { - errCollection = append(errCollection, fmt.Errorf("Attribute %s: %s", attr, check)) + errCollection = append(errCollection, fmt.Errorf("attribute %q: %s", attr, check)) } - errCollection = append(errCollection, fmt.Errorf("Address: %s\n", e.resourceAddress)) - + errCollection = append(errCollection, fmt.Errorf("address: %s\n", e.listResourceAddress)) resp.Error = errors.Join(errCollection...) + + return } // ExpectIdentity returns a query check that asserts that the identity at the given resource matches a known object, where each @@ -74,7 +89,7 @@ func (e expectIdentity) CheckQuery(ctx context.Context, req CheckQueryRequest, r // 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 ExpectIdentity(resourceAddress string, identity map[string]knownvalue.Check) QueryResultCheck { return expectIdentity{ - resourceAddress: resourceAddress, - check: identity, + listResourceAddress: resourceAddress, + check: identity, } } diff --git a/querycheck/expect_known_value.go b/querycheck/expect_known_value.go index 9b09b3e5d..e09f86065 100644 --- a/querycheck/expect_known_value.go +++ b/querycheck/expect_known_value.go @@ -5,7 +5,6 @@ package querycheck import ( "context" - "encoding/json" "fmt" "strings" @@ -32,36 +31,18 @@ func (e expectKnownValue) CheckQuery(_ context.Context, req CheckQueryRequest, r return } - // Ideally we can do the check like below which is identical to the expect known value state check but... terraform-json hasn't - // defined the resource object as a map[string]interface, we so we need to iterate over it manually - //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 - //} - - for k, v := range res.ResourceObject { - if k == e.attributePath.String() { - var val any + resource, err := tfjsonpath.Traverse(res.ResourceObject, e.attributePath) + if err != nil { + resp.Error = err + return + } - err := json.Unmarshal(v, &val) - if err != nil { - resp.Error = fmt.Errorf("%s - Error decoding message type: %s", e.listResourceAddress, 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 err := e.knownValue.CheckValue(val); 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 } } @@ -78,6 +59,11 @@ func (e expectKnownValue) CheckQuery(_ context.Context, req CheckQueryRequest, r 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, diff --git a/statecheck/expect_identity.go b/statecheck/expect_identity.go index df5147b23..a89a06e6d 100644 --- a/statecheck/expect_identity.go +++ b/statecheck/expect_identity.go @@ -67,9 +67,9 @@ func (e expectIdentity) CheckState(ctx context.Context, req CheckStateRequest, r if len(resource.IdentityValues) != len(e.identity) { deltaMsg := "" if len(resource.IdentityValues) > len(e.identity) { - deltaMsg = createDeltaString(resource.IdentityValues, e.identity, "actual identity has extra attribute(s): ") + deltaMsg = CreateDeltaString(resource.IdentityValues, e.identity, "actual identity has extra attribute(s): ") } else { - deltaMsg = createDeltaString(e.identity, resource.IdentityValues, "actual identity is missing attribute(s): ") + deltaMsg = CreateDeltaString(e.identity, resource.IdentityValues, "actual identity is missing attribute(s): ") } resp.Error = fmt.Errorf("%s - Expected %d attribute(s) in the actual identity object, got %d attribute(s): %s", e.resourceAddress, len(e.identity), len(resource.IdentityValues), deltaMsg) @@ -113,8 +113,8 @@ func ExpectIdentity(resourceAddress string, identity map[string]knownvalue.Check } } -// createDeltaString prints the map keys that are present in mapA and not present in mapB -func createDeltaString[T any, V any](mapA map[string]T, mapB map[string]V, msgPrefix string) string { +// CreateDeltaString prints the map keys that are present in mapA and not present in mapB +func CreateDeltaString[T any, V any](mapA map[string]T, mapB map[string]V, msgPrefix string) string { deltaMsg := "" deltaMap := make(map[string]T, len(mapA)) From e5bd162c2e14093defbdfbbf2e0bf53ebc5c8ffd Mon Sep 17 00:00:00 2001 From: Steph Date: Wed, 17 Sep 2025 14:13:07 +0200 Subject: [PATCH 41/45] remove redundant return statement and skip over nil messages --- internal/plugintest/working_dir.go | 5 +++++ querycheck/expect_identity.go | 2 -- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/internal/plugintest/working_dir.go b/internal/plugintest/working_dir.go index 332da11ee..d84d88231 100644 --- a/internal/plugintest/working_dir.go +++ b/internal/plugintest/working_dir.go @@ -538,9 +538,14 @@ func (wd *WorkingDir) Query(ctx context.Context) ([]tfjson.LogMsg, error) { } for msg := range logs { + if msg.Msg == nil { + continue + } + if msg.Err != nil { return nil, fmt.Errorf("retrieving next message: %w", msg.Err) } + if msg.Msg.Level() == tfjson.Error { // TODO reimplement missing .tf config error diags = append(diags, msg.Msg) diff --git a/querycheck/expect_identity.go b/querycheck/expect_identity.go index dd06bf817..4fa3abf5f 100644 --- a/querycheck/expect_identity.go +++ b/querycheck/expect_identity.go @@ -79,8 +79,6 @@ func (e expectIdentity) CheckQuery(_ context.Context, req CheckQueryRequest, res } errCollection = append(errCollection, fmt.Errorf("address: %s\n", e.listResourceAddress)) resp.Error = errors.Join(errCollection...) - - return } // ExpectIdentity returns a query check that asserts that the identity at the given resource matches a known object, where each From cca15217581acb629fd085d0e46d07dd8b186d44 Mon Sep 17 00:00:00 2001 From: Steph Date: Wed, 17 Sep 2025 14:19:07 +0200 Subject: [PATCH 42/45] fix query test --- helper/resource/query/query_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helper/resource/query/query_test.go b/helper/resource/query/query_test.go index c738d1c5c..eca8a8058 100644 --- a/helper/resource/query/query_test.go +++ b/helper/resource/query/query_test.go @@ -83,11 +83,11 @@ func TestQuery(t *testing.T) { }), querycheck.ExpectIdentity("examplecloud_containerette.test2", map[string]knownvalue.Check{ "id": knownvalue.StringExact("westeurope/somevalue2"), - "location": knownvalue.StringExact("westeurope"), + "location": knownvalue.StringExact("westeurope2"), }), querycheck.ExpectIdentity("examplecloud_containerette.test2", map[string]knownvalue.Check{ "id": knownvalue.StringExact("westeurope/somevalue3"), - "location": knownvalue.StringExact("westeurope"), + "location": knownvalue.StringExact("westeurope3"), }), }, }, From 79b21894ace54f58f204bb64a4fc38f8177af43d Mon Sep 17 00:00:00 2001 From: Steph Date: Thu, 18 Sep 2025 08:34:21 +0200 Subject: [PATCH 43/45] review comments --- helper/resource/query/query_checks.go | 24 ++++++++-------------- helper/resource/query/query_test.go | 6 +++--- helper/resource/testing.go | 19 ++--------------- helper/resource/testing_new.go | 2 +- internal/plugintest/working_dir.go | 2 +- querycheck/contains.go | 6 +++--- querycheck/expect_identity.go | 2 +- querycheck/expect_known_value.go | 2 +- querycheck/expect_result_length_atleast.go | 6 +++--- querycheck/expect_result_length_exact.go | 18 ++++++---------- querycheck/query_check.go | 12 +++++------ 11 files changed, 36 insertions(+), 63 deletions(-) diff --git a/helper/resource/query/query_checks.go b/helper/resource/query/query_checks.go index 10beadb83..8ef9a3e09 100644 --- a/helper/resource/query/query_checks.go +++ b/helper/resource/query/query_checks.go @@ -14,41 +14,35 @@ import ( "github.com/hashicorp/terraform-plugin-testing/querycheck" ) -func RunQueryChecks(ctx context.Context, t testing.T, query *[]tfjson.LogMsg, queryChecks []querycheck.QueryResultCheck) error { +func RunQueryChecks(ctx context.Context, t testing.T, query []tfjson.LogMsg, queryChecks []querycheck.QueryResultCheck) error { t.Helper() var result []error - if query == nil || len(*query) == 0 { - result = append(result, fmt.Errorf("No query results found")) + if query == nil { + result = append(result, fmt.Errorf("no query results found")) } found := make([]tfjson.ListResourceFoundData, 0) - complete := tfjson.ListCompleteData{} + summary := tfjson.ListCompleteData{} - for _, msg := range *query { + for _, msg := range query { switch v := msg.(type) { case tfjson.ListResourceFoundMessage: found = append(found, v.ListResourceFound) case tfjson.ListCompleteMessage: - complete = v.ListComplete + summary = v.ListComplete // TODO diagnostics and errors? default: - // ignore other message types + continue } } - // TODO check diagnostics in LogMsg to see if there are any errors we can return here? - var err error - if len(found) == 0 { - return fmt.Errorf("no resources found by query: %+v", err) - } - for _, queryCheck := range queryChecks { resp := querycheck.CheckQueryResponse{} queryCheck.CheckQuery(ctx, querycheck.CheckQueryRequest{ - Query: &found, - CompletedQuery: &complete, + Query: found, + QuerySummary: &summary, }, &resp) result = append(result, resp.Error) diff --git a/helper/resource/query/query_test.go b/helper/resource/query/query_test.go index eca8a8058..87979d1b1 100644 --- a/helper/resource/query/query_test.go +++ b/helper/resource/query/query_test.go @@ -64,7 +64,7 @@ func TestQuery(t *testing.T) { } } `, - ConfigQueryResultChecks: []querycheck.QueryResultCheck{ + QueryResultChecks: []querycheck.QueryResultCheck{ querycheck.ExpectIdentity("examplecloud_containerette.test", map[string]knownvalue.Check{ "id": knownvalue.StringExact("westeurope/somevalue1"), "location": knownvalue.StringExact("westeurope"), @@ -110,7 +110,7 @@ func TestQuery(t *testing.T) { } } `, - ConfigQueryResultChecks: []querycheck.QueryResultCheck{ + QueryResultChecks: []querycheck.QueryResultCheck{ querycheck.ExpectLength("examplecloud_containerette.test", knownvalue.Int64Exact(3)), querycheck.ExpectLength("examplecloud_containerette.test2", knownvalue.Int64Exact(3)), }, @@ -134,7 +134,7 @@ func TestQuery(t *testing.T) { } } `, - ConfigQueryResultChecks: []querycheck.QueryResultCheck{ + QueryResultChecks: []querycheck.QueryResultCheck{ querycheck.ExpectLengthAtLeast("examplecloud_containerette.test", 2), querycheck.ExpectLengthAtLeast("examplecloud_containerette.test2", 1), }, diff --git a/helper/resource/testing.go b/helper/resource/testing.go index 5a6566319..61c03ff72 100644 --- a/helper/resource/testing.go +++ b/helper/resource/testing.go @@ -642,9 +642,9 @@ type TestStep struct { // Custom state checks can be created by implementing the [statecheck.StateCheck] interface, or by using a StateCheck implementation from the provided [statecheck] package. ConfigStateChecks []statecheck.StateCheck - // ConfigQueryResultChecks allow assertions to be made against the query file during a Config test using a query check. + // QueryResultChecks allow assertions to be made against a collection of found resources that were returned by a query using a query check. // Custom query checks can be created by implementing the [querycheck.QueryResultCheck] interface, or by using a QueryResultCheck implementation from the provided [querycheck] package. - ConfigQueryResultChecks []querycheck.QueryResultCheck + QueryResultChecks []querycheck.QueryResultCheck // PlanOnly can be set to only run `plan` with this configuration, and not // actually apply it. This is useful for ensuring config changes result in @@ -861,21 +861,6 @@ type ConfigPlanChecks struct { PostApplyPostRefresh []plancheck.PlanCheck } -// ConfigQueryChecks defines the different points in a Config TestStep when query checks can be run. -type ConfigQueryChecks struct { - // PreApply runs all query checks in the slice. This occurs before the apply of a Config test is run. This slice cannot be populated - // with TestStep.QueryOnly, as there is no PreApply query run with that flag set. All errors by query checks in this slice are aggregated, reported, and will result in a test failure. - PreApply []querycheck.QueryResultCheck - - // PostApplyPreRefresh runs all query checks in the slice. This occurs after the apply and before the refresh of a Config test is run. - // All errors by query checks in this slice are aggregated, reported, and will result in a test failure. - PostApplyPreRefresh []querycheck.QueryResultCheck - - // PostApplyPostRefresh runs all query checks in the slice. This occurs after the apply and refresh of a Config test are run. - // All errors by query checks in this slice are aggregated, reported, and will result in a test failure. - PostApplyPostRefresh []querycheck.QueryResultCheck -} - // ImportPlanChecks defines the different points in an Import TestStep when plan checks can be run. type ImportPlanChecks struct { // PreApply runs all plan checks in the slice. This occurs after the plan of an Import test is computed. This slice cannot be populated diff --git a/helper/resource/testing_new.go b/helper/resource/testing_new.go index f46fc8d13..c94a97c05 100644 --- a/helper/resource/testing_new.go +++ b/helper/resource/testing_new.go @@ -388,7 +388,7 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest t.Fatalf("Step %d/%d error running query: %s", stepNumber, len(c.Steps), err) } - err = query.RunQueryChecks(ctx, t, &queryOut, step.ConfigQueryResultChecks) + err = query.RunQueryChecks(ctx, t, queryOut, step.QueryResultChecks) if err != nil { t.Fatalf("Step %d/%d error running query checks: %s", stepNumber, len(c.Steps), err) } diff --git a/internal/plugintest/working_dir.go b/internal/plugintest/working_dir.go index d84d88231..6494125f9 100644 --- a/internal/plugintest/working_dir.go +++ b/internal/plugintest/working_dir.go @@ -543,7 +543,7 @@ func (wd *WorkingDir) Query(ctx context.Context) ([]tfjson.LogMsg, error) { } if msg.Err != nil { - return nil, fmt.Errorf("retrieving next message: %w", msg.Err) + return nil, fmt.Errorf("retrieving message: %w", msg.Err) } if msg.Msg.Level() == tfjson.Error { diff --git a/querycheck/contains.go b/querycheck/contains.go index 10df580fd..6cef57012 100644 --- a/querycheck/contains.go +++ b/querycheck/contains.go @@ -17,7 +17,7 @@ type contains struct { } func (c contains) CheckQuery(_ context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { - for _, res := range *req.Query { + for _, res := range req.Query { if strings.EqualFold(c.check, res.DisplayName) { return } @@ -27,10 +27,10 @@ func (c contains) CheckQuery(_ context.Context, req CheckQueryRequest, resp *Che } -// Contains returns a query check that asserts that a resource with a given display name exists within the returned results of the query. +// 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 Contains(resourceAddress string, displayName string) QueryResultCheck { +func ContainsResourceWithName(resourceAddress string, displayName string) QueryResultCheck { return contains{ resourceAddress: resourceAddress, check: displayName, diff --git a/querycheck/expect_identity.go b/querycheck/expect_identity.go index 4fa3abf5f..2d8777f7c 100644 --- a/querycheck/expect_identity.go +++ b/querycheck/expect_identity.go @@ -23,7 +23,7 @@ type expectIdentity struct { // CheckQuery implements the query check logic. func (e expectIdentity) CheckQuery(_ context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { - for _, res := range *req.Query { + for _, res := range req.Query { var errCollection []error if e.listResourceAddress != strings.TrimPrefix(res.Address, "list.") { diff --git a/querycheck/expect_known_value.go b/querycheck/expect_known_value.go index e09f86065..9e8a8f83f 100644 --- a/querycheck/expect_known_value.go +++ b/querycheck/expect_known_value.go @@ -22,7 +22,7 @@ type expectKnownValue struct { } func (e expectKnownValue) CheckQuery(_ context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { - for _, res := range *req.Query { + for _, res := range req.Query { diags := make([]error, 0) if e.listResourceAddress == strings.TrimPrefix(res.Address, "list.") && e.resourceName == res.DisplayName { diff --git a/querycheck/expect_result_length_atleast.go b/querycheck/expect_result_length_atleast.go index ebbb6ed4a..6b3dbde8a 100644 --- a/querycheck/expect_result_length_atleast.go +++ b/querycheck/expect_result_length_atleast.go @@ -17,13 +17,13 @@ type expectLengthAtLeast struct { // CheckQuery implements the query check logic. func (e expectLengthAtLeast) CheckQuery(_ context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { - if req.CompletedQuery == nil { + if req.QuerySummary == nil { resp.Error = fmt.Errorf("no completed query information available") return } - if req.CompletedQuery.Total < e.check { - resp.Error = fmt.Errorf("Query result of at least length %v - expected but got %v.", e.check, req.CompletedQuery.Total) + if req.QuerySummary.Total < e.check { + resp.Error = fmt.Errorf("Query result of at least length %v - expected but got %v.", e.check, req.QuerySummary.Total) return } } diff --git a/querycheck/expect_result_length_exact.go b/querycheck/expect_result_length_exact.go index 278ada1ff..f10546ac2 100644 --- a/querycheck/expect_result_length_exact.go +++ b/querycheck/expect_result_length_exact.go @@ -5,31 +5,25 @@ package querycheck import ( "context" - "encoding/json" "fmt" - "strconv" - - "github.com/hashicorp/terraform-plugin-testing/knownvalue" ) var _ QueryResultCheck = expectLength{} type expectLength struct { resourceAddress string - check knownvalue.Check + check int } // CheckQuery implements the query check logic. func (e expectLength) CheckQuery(_ context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { - if req.CompletedQuery == nil { - resp.Error = fmt.Errorf("no completed query information available") + if req.QuerySummary == nil { + resp.Error = fmt.Errorf("no query summary information available") return } - lengthCheck := json.Number(strconv.Itoa(req.CompletedQuery.Total)) - - if err := e.check.CheckValue(lengthCheck); err != nil { - resp.Error = fmt.Errorf("Query result of length %v - expected but got %v.", e.check, req.CompletedQuery.Total) + if e.check != req.QuerySummary.Total { + resp.Error = fmt.Errorf("number of found resources %v - expected but got %v.", e.check, req.QuerySummary.Total) return } } @@ -37,7 +31,7 @@ func (e expectLength) CheckQuery(_ context.Context, req CheckQueryRequest, resp // ExpectLength returns a query check that asserts that the length of the query result is exactly the given value. // // This query check can only be used with managed resources that support query. Query is only supported in Terraform v1.14+ -func ExpectLength(resourceAddress string, length knownvalue.Check) QueryResultCheck { +func ExpectLength(resourceAddress string, length int) QueryResultCheck { return expectLength{ resourceAddress: resourceAddress, check: length, diff --git a/querycheck/query_check.go b/querycheck/query_check.go index 8c60cabc7..66d326287 100644 --- a/querycheck/query_check.go +++ b/querycheck/query_check.go @@ -9,8 +9,8 @@ import ( tfjson "github.com/hashicorp/terraform-json" ) -// QueryResultCheck defines an interface for implementing test logic that checks a query file and then returns an error -// if the query file does not match what is expected. +// QueryResultCheck defines an interface for implementing test logic to apply an assertion against a collection of found +// resources that were returned by a query. It returns an error if the query results do not match what is expected. type QueryResultCheck interface { // CheckQuery should perform the query check. CheckQuery(context.Context, CheckQueryRequest, *CheckQueryResponse) @@ -18,11 +18,11 @@ type QueryResultCheck interface { // CheckQueryRequest is a request for an invoke of the CheckQuery function. type CheckQueryRequest struct { - // Query represents a parsed query file, retrieved via the `terraform show -json` command. - Query *[]tfjson.ListResourceFoundData + // Query represents the parsed log messages relating to found resources returned by the `terraform query -json` command. + Query []tfjson.ListResourceFoundData - // CompletedQuery contains a summary of the completed query operation. - CompletedQuery *tfjson.ListCompleteData + // QuerySummary contains a summary of the completed query operation + QuerySummary *tfjson.ListCompleteData } // CheckQueryResponse is a response to an invoke of the CheckQuery function. From 460e17610bc8e7e576d0358d8e894d532ab7cc3d Mon Sep 17 00:00:00 2001 From: Steph Date: Thu, 18 Sep 2025 08:36:16 +0200 Subject: [PATCH 44/45] update query test --- helper/resource/query/query_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helper/resource/query/query_test.go b/helper/resource/query/query_test.go index 87979d1b1..1fa76381d 100644 --- a/helper/resource/query/query_test.go +++ b/helper/resource/query/query_test.go @@ -111,8 +111,8 @@ func TestQuery(t *testing.T) { } `, QueryResultChecks: []querycheck.QueryResultCheck{ - querycheck.ExpectLength("examplecloud_containerette.test", knownvalue.Int64Exact(3)), - querycheck.ExpectLength("examplecloud_containerette.test2", knownvalue.Int64Exact(3)), + querycheck.ExpectLength("examplecloud_containerette.test", 3), + querycheck.ExpectLength("examplecloud_containerette.test2", 3), }, }, { From e7db617a8034f4d446d94aaa9d7b502e71db2049 Mon Sep 17 00:00:00 2001 From: Steph Date: Thu, 18 Sep 2025 09:16:19 +0200 Subject: [PATCH 45/45] minor fixes --- querycheck/expect_known_value.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/querycheck/expect_known_value.go b/querycheck/expect_known_value.go index 9e8a8f83f..faa046eb5 100644 --- a/querycheck/expect_known_value.go +++ b/querycheck/expect_known_value.go @@ -23,7 +23,7 @@ type expectKnownValue struct { func (e expectKnownValue) CheckQuery(_ context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { for _, res := range req.Query { - diags := make([]error, 0) + var diags []error if e.listResourceAddress == strings.TrimPrefix(res.Address, "list.") && e.resourceName == res.DisplayName { if res.ResourceObject == nil { @@ -51,7 +51,7 @@ func (e expectKnownValue) CheckQuery(_ context.Context, req CheckQueryRequest, r for _, diag := range diags { diagsStr += diag.Error() + "; " } - resp.Error = fmt.Errorf("The following errors were found while checking values: %s" + diagsStr) + resp.Error = fmt.Errorf("the following errors were found while checking values: %s", diagsStr) return } }