From 23a9124b59be2685af2f953ceeb09eb0222824ce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Jul 2024 14:49:40 -0400 Subject: [PATCH 1/8] build(deps): Bump github.com/zclconf/go-cty from 1.14.4 to 1.15.0 (#359) Bumps [github.com/zclconf/go-cty](https://github.com/zclconf/go-cty) from 1.14.4 to 1.15.0. - [Release notes](https://github.com/zclconf/go-cty/releases) - [Changelog](https://github.com/zclconf/go-cty/blob/main/CHANGELOG.md) - [Commits](https://github.com/zclconf/go-cty/compare/v1.14.4...v1.15.0) --- updated-dependencies: - dependency-name: github.com/zclconf/go-cty dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 83494db6a..fabe1f4e0 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 github.com/mitchellh/go-testing-interface v1.14.1 - github.com/zclconf/go-cty v1.14.4 + github.com/zclconf/go-cty v1.15.0 golang.org/x/crypto v0.25.0 ) diff --git a/go.sum b/go.sum index 5feb4a5b2..bda3ceb87 100644 --- a/go.sum +++ b/go.sum @@ -137,8 +137,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.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8= -github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +github.com/zclconf/go-cty v1.15.0 h1:tTCRWxsexYUmtt/wVxgDClUe+uQusuI443uL6e+5sXQ= +github.com/zclconf/go-cty v1.15.0/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= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= From 6dacd4f01f7992c73945126f40294f3b9345ac46 Mon Sep 17 00:00:00 2001 From: "hashicorp-tsccr[bot]" <129506189+hashicorp-tsccr[bot]@users.noreply.github.com> Date: Fri, 19 Jul 2024 10:24:41 -0400 Subject: [PATCH 2/8] Result of tsccr-helper -log-level=info gha update -latest . (#360) Co-authored-by: hashicorp-tsccr[bot] --- .github/workflows/ci-go.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-go.yml b/.github/workflows/ci-go.yml index 2af391c85..f5ee37e53 100644 --- a/.github/workflows/ci-go.yml +++ b/.github/workflows/ci-go.yml @@ -49,7 +49,7 @@ jobs: wildcard=".*" echo "version=${orginal_version%"$wildcard"}" >> "$GITHUB_OUTPUT" - run: go tool cover -html=coverage.out -o coverage.html - - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + - uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 with: name: go-${{ matrix.go-version }}-terraform-${{ steps.tf_version.outputs.version }}-coverage path: coverage.html From f9ca4c365abff60c826dd84855b2bff3d1466a17 Mon Sep 17 00:00:00 2001 From: "hashicorp-tsccr[bot]" <129506189+hashicorp-tsccr[bot]@users.noreply.github.com> Date: Tue, 23 Jul 2024 13:05:45 -0400 Subject: [PATCH 3/8] Result of tsccr-helper -log-level=info gha update -latest . (#361) Co-authored-by: hashicorp-tsccr[bot] --- .github/workflows/ci-github-actions.yml | 2 +- .github/workflows/ci-go.yml | 4 ++-- .github/workflows/ci-goreleaser.yml | 2 +- .github/workflows/release.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-github-actions.yml b/.github/workflows/ci-github-actions.yml index cf96ddbdc..ce2d45e52 100644 --- a/.github/workflows/ci-github-actions.yml +++ b/.github/workflows/ci-github-actions.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: go-version-file: 'go.mod' - run: go install github.com/rhysd/actionlint/cmd/actionlint@latest diff --git a/.github/workflows/ci-go.yml b/.github/workflows/ci-go.yml index f5ee37e53..1fdda6a2d 100644 --- a/.github/workflows/ci-go.yml +++ b/.github/workflows/ci-go.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: go-version-file: 'go.mod' - run: go mod download @@ -31,7 +31,7 @@ jobs: terraform: ${{ fromJSON(vars.TF_VERSIONS_PROTOCOL_V5) }} steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: go-version: ${{ matrix.go-version }} - uses: hashicorp/setup-terraform@651471c36a6092792c552e8b1bef71e592b462d8 # v3.1.1 diff --git a/.github/workflows/ci-goreleaser.yml b/.github/workflows/ci-goreleaser.yml index 398bd1558..0603e6b35 100644 --- a/.github/workflows/ci-goreleaser.yml +++ b/.github/workflows/ci-goreleaser.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: go-version-file: 'go.mod' - uses: goreleaser/goreleaser-action@286f3b13b1b49da4ac219696163fb8c1c93e1200 # v6.0.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3a6ec9c87..054217462 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -84,7 +84,7 @@ jobs: ref: ${{ inputs.versionNumber }} fetch-depth: 0 - - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: go-version-file: 'go.mod' From 4910d5236f5dd824dec00ad90bf1ba1be74680f7 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 6 Aug 2024 12:47:55 -0400 Subject: [PATCH 4/8] docs: Update `Check` function examples with `statecheck` package implementations (#363) --- .../plugin/testing/acceptance-tests/index.mdx | 8 +- .../testing/acceptance-tests/testcase.mdx | 38 ++--- .../testing/acceptance-tests/teststep.mdx | 58 ++++--- website/docs/plugin/testing/index.mdx | 2 +- .../docs/plugin/testing/testing-patterns.mdx | 144 ++++++------------ 5 files changed, 106 insertions(+), 144 deletions(-) diff --git a/website/docs/plugin/testing/acceptance-tests/index.mdx b/website/docs/plugin/testing/acceptance-tests/index.mdx index adb9c6c5d..4703a1f51 100644 --- a/website/docs/plugin/testing/acceptance-tests/index.mdx +++ b/website/docs/plugin/testing/acceptance-tests/index.mdx @@ -17,11 +17,11 @@ or more configuration files, allowing multiple scenarios to be tested. Terraform acceptance tests use real Terraform configurations to exercise the code in real plan, apply, refresh, and destroy life cycles. When run from the -root of a Terraform Provider codebase, Terraform’s testing framework compiles +root of a Terraform Provider codebase, Terraform's testing framework compiles the current provider in-memory and executes the provided configuration in developer defined steps, creating infrastructure along the way. At the conclusion of all the steps, Terraform automatically destroys the -infrastructure. It’s important to note that during development, it’s possible +infrastructure. It's important to note that during development, it's possible for Terraform to leave orphaned or “dangling” resources behind, depending on the correctness of the code in development. The testing framework provides means to validate all resources are destroyed, alerting developers if any fail to @@ -42,7 +42,7 @@ While the test framework provides a reasonable simulation of real-world usage, t Terraform follows many of the Go programming language conventions with regards to testing, with both acceptance tests and unit tests being placed in a file -that matches the file under test, with an added `_test.go` suffix. Here’s an +that matches the file under test, with an added `_test.go` suffix. Here's an example file structure: ``` @@ -302,6 +302,6 @@ This error indicates that the provider server could not connect to Terraform Cor Terraform relies heavily on acceptance tests to ensure we keep our promise of helping users safely and predictably create, change, and improve infrastructure. In our next section we detail how to create “Test Cases”, -individual acceptance tests using Terraform’s testing framework, in order to +individual acceptance tests using Terraform's testing framework, in order to build and verify real infrastructure. [Proceed to Test Cases](/terraform/plugin/testing/acceptance-tests/testcase) diff --git a/website/docs/plugin/testing/acceptance-tests/testcase.mdx b/website/docs/plugin/testing/acceptance-tests/testcase.mdx index 98aa9586c..57ddd888f 100644 --- a/website/docs/plugin/testing/acceptance-tests/testcase.mdx +++ b/website/docs/plugin/testing/acceptance-tests/testcase.mdx @@ -9,13 +9,13 @@ description: |- Acceptance tests are expressed in terms of **Test Cases**, each using one or more Terraform configurations designed to create a set of resources under test, -and then verify the actual infrastructure created. Terraform’s `resource` +and then verify the actual infrastructure created. Terraform's `resource` package offers a method `Test()`, accepting two parameters and acting as the -entry point to Terraform’s acceptance test framework. The first parameter is the -standard [\*testing.T struct from Golang’s Testing package][3], and the second is +entry point to Terraform's acceptance test framework. The first parameter is the +standard [\*testing.T struct from Golang's Testing package][3], and the second is [TestCase][1], a Go struct that developers use to setup the acceptance tests. -Here’s an example acceptance test. Here the Provider is named `Example`, and the +Here's an example acceptance test. Here the Provider is named `Example`, and the Resource under test is `Widget`. The parts of this test are explained below the example. @@ -34,15 +34,15 @@ func TestAccExampleWidget_basic(t *testing.T) { Steps: []resource.TestStep{ { Config: testAccExampleResource(rName), - Check: resource.ComposeTestCheckFunc( - testAccCheckExampleResourceExists("example_widget.foo", &widgetBefore), - ), + ConfigStateChecks: []statecheck.StateCheck{ + stateCheckExampleResourceExists("example_widget.foo", &widgetBefore), + }, }, { Config: testAccExampleResource_removedPolicy(rName), - Check: resource.ComposeTestCheckFunc( - testAccCheckExampleResourceExists("example_widget.foo", &widgetAfter), - ), + ConfigStateChecks: []statecheck.StateCheck{ + stateCheckExampleResourceExists("example_widget.foo", &widgetAfter), + }, }, }, }) @@ -81,7 +81,7 @@ func TestAccExampleWidget_basic(t *testing.T) { The majority of acceptance tests will only invoke `resource.Test()` and exit. If at any point this method encounters an error, either in executing the provided Terraform configurations or subsequent developer defined checks, `Test()` will -invoke the `t.Error` method of Go’s standard testing framework and the test will +invoke the `t.Error` method of Go's standard testing framework and the test will fail. A failed test will not halt or otherwise interrupt any other tests currently running. @@ -190,7 +190,7 @@ a configuration file for testing must be represented in this map or the test will fail during initialization. This map is most commonly constructed once in a common `init()` method of the -Provider’s main test file, and includes an object of the current Provider type. +Provider's main test file, and includes an object of the current Provider type. **Example usage:** (note the different files `widget_test.go` and `provider_test.go`) @@ -326,15 +326,15 @@ func TestAccExampleWidget_basic(t *testing.T) { Steps: []resource.TestStep{ { Config: testAccExampleResource(rName), - Check: resource.ComposeTestCheckFunc( - testAccCheckExampleResourceExists("example_widget.foo", &widgetBefore), - ), + ConfigStateChecks: []statecheck.StateCheck{ + stateCheckExampleResourceExists("example_widget.foo", &widgetBefore), + }, }, { Config: testAccExampleResource_removedPolicy(rName), - Check: resource.ComposeTestCheckFunc( - testAccCheckExampleResourceExists("example_widget.foo", &widgetAfter), - ), + ConfigStateChecks: []statecheck.StateCheck{ + stateCheckExampleResourceExists("example_widget.foo", &widgetAfter), + }, }, }, }) @@ -346,7 +346,7 @@ func TestAccExampleWidget_basic(t *testing.T) { `TestCases` are used to verify the features of a given part of a plugin. Each case should represent a scenario of normal usage of the plugin, from simple creation to creating, adding, and removing specific properties. In the next -Section [`TestSteps`][2], we’ll detail `Steps` portion of `TestCase` and see how +Section [`TestSteps`][2], we'll detail `Steps` portion of `TestCase` and see how to create these scenarios by iterating on Terraform configurations. [1]: https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/helper/resource#TestCase diff --git a/website/docs/plugin/testing/acceptance-tests/teststep.mdx b/website/docs/plugin/testing/acceptance-tests/teststep.mdx index 5f820fa07..01e791999 100644 --- a/website/docs/plugin/testing/acceptance-tests/teststep.mdx +++ b/website/docs/plugin/testing/acceptance-tests/teststep.mdx @@ -64,22 +64,22 @@ func TestAccExampleWidget_basic(t *testing.T) { Steps: []resource.TestStep{ { Config: testAccExampleResource(rName), - Check: resource.ComposeTestCheckFunc( - testAccCheckExampleResourceExists("example_widget.foo", &widgetBefore), - ), + ConfigStateChecks: []statecheck.StateCheck{ + stateCheckExampleResourceExists("example_widget.foo", &widgetBefore), + }, }, { Config: testAccExampleResource_removedPolicy(rName), - Check: resource.ComposeTestCheckFunc( - testAccCheckExampleResourceExists("example_widget.foo", &widgetAfter), - ), + ConfigStateChecks: []statecheck.StateCheck{ + stateCheckExampleResourceExists("example_widget.foo", &widgetAfter), + }, }, }, }) } ``` -In the above example each `TestCase` invokes a function to retrieve it’s desired +In the above example each `TestCase` invokes a function to retrieve it's desired configuration, based on a randomized name provided, however an in-line string or constant string would work as well, so long as they contain valid Terraform configuration for the plugin or resource under test. This pattern of first @@ -87,19 +87,36 @@ applying and checking a basic configuration, followed by applying a modified configuration with updated or additional checks is a common pattern used to test update functionality. -## State Check Functions +## Plan Checks +Before and after the configuration for a `TestStep` is applied, Terraform's testing framework provides developers an opportunity to make test assertions against `terraform plan` results via the plan file. This is provided via [Plan Checks](/terraform/plugin/testing/acceptance-tests/plan-checks), which provide both built-in plan checks and an interface to implement custom plan checks. + +## State Checks + +After the configuration for a `TestStep` is applied, Terraform's testing +framework provides developers an opportunity to check the results by providing one +or more [state check implementations](/terraform/plugin/testing/acceptance-tests/state-checks). +While possible to only supply a single state check, it is recommended you use multiple state checks +to validate specific information about the results of the `terraform apply` ran in each `TestStep`. -After the configuration for a `TestStep` is applied, Terraform’s testing -framework provides developers an opportunity to check the results by providing a -“Check” function. While possible to only supply a single function, it is -recommended you use multiple functions to validate specific information about -the results of the `terraform apply` ran in each `TestStep`. The `Check` +See the [State Checks](/terraform/plugin/testing/acceptance-tests/state-checks) section for more information about the built-in state checks for resources, data sources, +output values, and how to write custom state checks. + +### Legacy Check function + + + +Use the new `ConfigStateChecks` attribute and [State Check implementations](/terraform/plugin/testing/acceptance-tests/state-checks) +instead of the `Check` function. + + + +The `Check` function is used to check results of a Terraform operation. The `Check` attribute of `TestStep` is singular, so in order to include multiple checks developers should use either `ComposeTestCheckFunc` or `ComposeAggregateTestCheckFunc` (defined below) to group multiple check functions, defined below: -### ComposeTestCheckFunc +#### ComposeTestCheckFunc ComposeTestCheckFunc lets you compose multiple TestCheckFunc functions into a single check. As a user testing their provider, this lets you decompose your @@ -124,10 +141,10 @@ Steps: []resource.TestStep{ }, ``` -### ComposeAggregateTestCheckFunc +#### ComposeAggregateTestCheckFunc ComposeAggregateTestCheckFunc lets you compose multiple TestCheckFunc functions -into a single check. It’s purpose and usage is identical to +into a single check. It's purpose and usage is identical to ComposeTestCheckFunc, however each check is ran in order even if a previous check failed, collecting the errors returned from any checks and returning a single aggregate error. The entire `TestCase` is still stopped, and Terraform @@ -149,7 +166,7 @@ Steps: []resource.TestStep{ }, ``` -## Builtin check functions +#### Built-in check functions Terraform has several TestCheckFunc functions built in for developers to use for common checks, such as verifying the status and value of a specific attribute in @@ -204,7 +221,7 @@ All of these functions also accept the below syntax in attribute keys to enable | `.#` | Number of elements in list or set | `TestCheckResourceAttr("example_widget.foo", "some_list.#", "2")` | | `.%` | Number of keys in map | `TestCheckResourceAttr("example_widget.foo", "some_map.%", "2")` | -## Custom check functions +### Custom check functions The `Check` field of `TestStep` accepts any function of type [TestCheckFunc](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/helper/resource#TestCheckFunc). @@ -299,9 +316,6 @@ func testAccCheckExampleWidgetExists(resourceName string, widget *example.Widget } ``` -## Plan Checks -Before and after the configuration for a `TestStep` is applied, Terraform's testing framework provides developers an opportunity to make test assertions against `terraform plan` results via the plan file. This is provided via [Plan Checks](/terraform/plugin/testing/acceptance-tests/plan-checks), which provide both built-in plan checks and an interface to implement custom plan checks. - ## Sweepers -Acceptance Testing is an essential approach to validating the implementation of a Terraform Provider. Using actual APIs to provision resources for testing can leave behind real infrastructure that costs money between tests. The reasons for these leaks can vary, regardless Terraform provides a mechanism known as [Sweepers](/terraform/plugin/testing/acceptance-tests/sweepers) to help keep the testing account clean. +Acceptance Testing is an essential approach to validating the implementation of a Terraform Provider. Using actual APIs to provision resources for testing can leave behind real infrastructure that costs money between tests. The reasons for these leaks can vary, regardless Terraform provides a mechanism known as [Sweepers](/terraform/plugin/testing/acceptance-tests/sweepers) to help keep the testing account clean. \ No newline at end of file diff --git a/website/docs/plugin/testing/index.mdx b/website/docs/plugin/testing/index.mdx index 34824c81e..2ed9cb366 100644 --- a/website/docs/plugin/testing/index.mdx +++ b/website/docs/plugin/testing/index.mdx @@ -26,7 +26,7 @@ verified. Terraform includes a framework for constructing acceptance tests that imitate the execution of one or more steps of applying one or more configuration files, allowing multiple scenarios to be tested. -It’s important to reiterate that acceptance tests in resources _create actual +It's important to reiterate that acceptance tests in resources _create actual cloud infrastructure_, with possible expenses incurred, and are the responsibility of the user running the tests. Creating real infrastructure in tests verifies the described behavior of Terraform Plugins in real world use diff --git a/website/docs/plugin/testing/testing-patterns.mdx b/website/docs/plugin/testing/testing-patterns.mdx index 985a42288..662e51d4c 100644 --- a/website/docs/plugin/testing/testing-patterns.mdx +++ b/website/docs/plugin/testing/testing-patterns.mdx @@ -7,9 +7,9 @@ description: |- # Testing Patterns -In [Testing Terraform Plugins][1] we introduce Terraform’s Testing Framework, +In [Testing Terraform Plugins][1] we introduce Terraform's Testing Framework, providing reference for its functionality and introducing the basic parts of -writing acceptance tests. In this section we’ll cover some test patterns that +writing acceptance tests. In this section we'll cover some test patterns that are common and considered a best practice to have when developing and verifying your Terraform plugins. At time of writing these guides are particular to Terraform Resources, but other testing best practices may be added later. @@ -25,7 +25,7 @@ Terraform Resources, but other testing best practices may be added later. ## Built-in Patterns Acceptance tests use [TestCases][2] to construct scenarios that can be evaluated -with Terraform’s lifecycle of plan, apply, refresh, and destroy. The test +with Terraform's lifecycle of plan, apply, refresh, and destroy. The test framework has some behaviors built in that provide very basic workflow assurance tests, such as verifying configurations apply with no diff generated by the next plan. @@ -68,21 +68,21 @@ establish the following: The first and last item are provided by the test framework as described above in **Built-in Patterns**. The middle items are implemented by composing a series of -Check Functions as described in [Acceptance Tests: TestSteps][8]. +State Check implementations as described in [Acceptance Tests: State Checks][8]. To verify attributes are saved to the state file correctly, use a combination of -the built-in check functions provided by the testing framework. See [Built-in -Check Functions][9] to see available functions. +the built-in [`statecheck`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/statecheck) +implementations provided by the testing framework. See [Resource State Checks][9] to see available +state checks for managed resource and data source attributes. Checking the values in a remote API generally consists of two parts: a function -to verify the corresponding object exists remotely, and a separate function to +to verify the corresponding object exists remotely, and a state check implementation to verify the values of the object. By separating the check used to verify the object exists into its own function, developers are free to re-use it for all -TestCases as a means of retrieving it’s values, and can provide custom check -functions per TestCase to verify different attributes or scenarios specific to -that TestCase. +TestSteps as a means of retrieving it's values, and can provide [custom state checks][10] +functions per TestStep to verify remote values or scenarios specific to that TestStep. -Here’s an example test, with in-line comments to demonstrate the key parts of a +Here's an example test, with in-line comments to demonstrate the key parts of a basic test. ```go @@ -107,62 +107,20 @@ func TestAccExampleWidget_basic(t *testing.T) { // use a dynamic configuration with the random name from above Config: testAccExampleResource(rName), // compose a basic test, checking both remote and local values - Check: resource.ComposeTestCheckFunc( - // query the API to retrieve the widget object - testAccCheckExampleResourceExists("example_widget.foo", &widget), - // verify remote values - testAccCheckExampleWidgetValues(widget, rName), - // verify local values - resource.TestCheckResourceAttr("example_widget.foo", "active", "true"), - resource.TestCheckResourceAttr("example_widget.foo", "name", rName), - ), + ConfigStateChecks: []statecheck.StateCheck{ + // custom state check - query the API to retrieve the widget object + stateCheckExampleResourceExists("example_widget.foo", &widget), + // custom state check - verify remote values + stateCheckExampleWidgetValues(widget, rName), + // built-in state checks - verify local (state) values + statecheck.ExpectKnownValue("example_widget.foo", tfjsonpath.New("active"), knownvalue.Bool(true)), + statecheck.ExpectKnownValue("example_widget.foo", tfjsonpath.New("name"), knownvalue.StringExact(rName)), + }, }, }, }) } -func testAccCheckExampleWidgetValues(widget *example.Widget, name string) resource.TestCheckFunc { - return func(s *terraform.State) error { - if *widget.Active != true { - return fmt.Errorf("bad active state, expected \"true\", got: %#v", *widget.Active) - } - if *widget.Name != name { - return fmt.Errorf("bad name, expected \"%s\", got: %#v", name, *widget.Name) - } - return nil - } -} - -// testAccCheckExampleResourceExists queries the API and retrieves the matching Widget. -func testAccCheckExampleResourceExists(n string, widget *example.Widget) resource.TestCheckFunc { - return func(s *terraform.State) error { - // find the corresponding state object - rs, ok := s.RootModule().Resources[n] - if !ok { - return fmt.Errorf("Not found: %s", n) - } - - // retrieve the configured client from the test setup - conn := testAccProvider.Meta().(*ExampleClient) - resp, err := conn.DescribeWidget(&example.DescribeWidgetsInput{ - WidgetIdentifier: rs.Primary.ID, - }) - - if err != nil { - return err - } - - if resp.Widget == nil { - return fmt.Errorf("Widget (%s) not found", rs.Primary.ID) - } - - // assign the response Widget attribute to the widget pointer - *widget = *resp.Widget - - return nil - } -} - // testAccExampleResource returns an configuration for an Example Widget with the provided name func testAccExampleResource(name string) string { return fmt.Sprintf(` @@ -189,8 +147,8 @@ configuration. Below is an example test, copied and modified from the basic test. Here we preserve the `TestStep` from the basic test, but we add an additional `TestStep`, changing the configuration and rechecking the values, with a -different configuration function `testAccExampleResourceUpdated` and check -function `testAccCheckExampleWidgetValuesUpdated` for verifying the values. +different configuration function `testAccExampleResourceUpdated` and state check +implementation `stateCheckExampleWidgetValuesUpdated` for verifying the values. ```go func TestAccExampleWidget_update(t *testing.T) { @@ -205,39 +163,27 @@ func TestAccExampleWidget_update(t *testing.T) { { // use a dynamic configuration with the random name from above Config: testAccExampleResource(rName), - Check: resource.ComposeTestCheckFunc( - testAccCheckExampleResourceExists("example_widget.foo", &widget), - testAccCheckExampleWidgetValues(widget, rName), - resource.TestCheckResourceAttr("example_widget.foo", "active", "true"), - resource.TestCheckResourceAttr("example_widget.foo", "name", rName), - ), + ConfigStateChecks: []statecheck.StateCheck{ + stateCheckExampleResourceExists("example_widget.foo", &widget), + stateCheckExampleWidgetValues(widget, rName), + statecheck.ExpectKnownValue("example_widget.foo", tfjsonpath.New("active"), knownvalue.Bool(true)), + statecheck.ExpectKnownValue("example_widget.foo", tfjsonpath.New("name"), knownvalue.StringExact(rName)), + }, }, { // use a dynamic configuration with the random name from above Config: testAccExampleResourceUpdated(rName), - Check: resource.ComposeTestCheckFunc( - testAccCheckExampleResourceExists("example_widget.foo", &widget), - testAccCheckExampleWidgetValuesUpdated(widget, rName), - resource.TestCheckResourceAttr("example_widget.foo", "active", "false"), - resource.TestCheckResourceAttr("example_widget.foo", "name", rName), - ), + ConfigStateChecks: []statecheck.StateCheck{ + stateCheckExampleResourceExists("example_widget.foo", &widget), + stateCheckExampleWidgetValuesUpdated(widget, rName), + statecheck.ExpectKnownValue("example_widget.foo", tfjsonpath.New("active"), knownvalue.Bool(false)), + statecheck.ExpectKnownValue("example_widget.foo", tfjsonpath.New("name"), knownvalue.StringExact(rName)), + }, }, }, }) } -func testAccCheckExampleWidgetValuesUpdated(widget *example.Widget, name string) resource.TestCheckFunc { - return func(s *terraform.State) error { - if *widget.Active != false { - return fmt.Errorf("bad active state, expected \"false\", got: %#v", *widget.Active) - } - if *widget.Name != name { - return fmt.Errorf("bad name, expected \"%s\", got: %#v", name, *widget.Name) - } - return nil - } -} - // testAccExampleResource returns an configuration for an Example Widget with the provided name func testAccExampleResourceUpdated(name string) string { return fmt.Sprintf(` @@ -248,7 +194,7 @@ resource "example_widget" "foo" { } ``` -It’s common for resources to just have the above update test, as it is a +It's common for resources to just have the above update test, as it is a superset of the basic test. So long as the basics are covered, combining the two tests is sufficient as opposed to having two separate tests. @@ -287,17 +233,17 @@ func TestAccExampleWidget_expectPlan(t *testing.T) { // use an incomplete configuration that we expect // to result in a non-empty plan after apply Config: testAccExampleResourceIncomplete(rName), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("example_widget.foo", "name", rName), - ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("example_widget.foo", tfjsonpath.New("name"), knownvalue.StringExact(rName)), + }, ExpectNonEmptyPlan: true, }, { // apply the complete configuration Config: testAccExampleResourceComplete(rName), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("example_widget.foo", "name", rName), - ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("example_widget.foo", tfjsonpath.New("name"), knownvalue.StringExact(rName)), + }, }, }, }) @@ -362,7 +308,7 @@ then advancing the revisions to evaluate the fix. # Conclusion -Terraform’s [Testing Framework][1] allows for powerful, iterative acceptance +Terraform's [Testing Framework][1] allows for powerful, iterative acceptance tests that enable developers to fully test the behavior of Terraform plugins. By following the above best practices, developers can ensure their plugin behaves correctly across the most common use cases and everyday operations users will @@ -383,6 +329,8 @@ for safely managing infrastructure. [7]: https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/helper/resource#TestStep.ExpectError -[8]: /terraform/plugin/testing/acceptance-tests/teststep#check-functions +[8]: /terraform/plugin/testing/acceptance-tests/state-checks + +[9]: /terraform/plugin/testing/acceptance-tests/state-checks/resource -[9]: /terraform/plugin/testing/acceptance-tests/teststep#builtin-check-functions \ No newline at end of file +[10]: /terraform/plugin/testing/acceptance-tests/state-checks/custom \ No newline at end of file From c1879251ea10fe83a52a6dd7be847f1764ef2285 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Aug 2024 12:48:20 -0400 Subject: [PATCH 5/8] build(deps): Bump github.com/hashicorp/hc-install from 0.7.0 to 0.8.0 (#362) Bumps [github.com/hashicorp/hc-install](https://github.com/hashicorp/hc-install) from 0.7.0 to 0.8.0. - [Release notes](https://github.com/hashicorp/hc-install/releases) - [Commits](https://github.com/hashicorp/hc-install/compare/v0.7.0...v0.8.0) --- updated-dependencies: - dependency-name: github.com/hashicorp/hc-install dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 5 +++-- go.sum | 10 ++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index fabe1f4e0..0813ada5c 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/hashicorp/go-hclog v1.6.3 github.com/hashicorp/go-uuid v1.0.3 github.com/hashicorp/go-version v1.7.0 - github.com/hashicorp/hc-install v0.7.0 + github.com/hashicorp/hc-install v0.8.0 github.com/hashicorp/hcl/v2 v2.21.0 github.com/hashicorp/logutils v1.0.0 github.com/hashicorp/terraform-exec v0.21.0 @@ -35,6 +35,7 @@ require ( github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-plugin v1.6.0 // indirect + github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/terraform-registry-address v0.2.3 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect github.com/hashicorp/yamux v0.1.1 // indirect @@ -48,7 +49,7 @@ require ( github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - golang.org/x/mod v0.17.0 // indirect + golang.org/x/mod v0.19.0 // indirect golang.org/x/net v0.25.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.22.0 // indirect diff --git a/go.sum b/go.sum index bda3ceb87..6db6736df 100644 --- a/go.sum +++ b/go.sum @@ -57,13 +57,15 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A= github.com/hashicorp/go-plugin v1.6.0/go.mod h1:lBS5MtSSBZk0SHc66KACcjjlU6WzEVP/8pwz68aMkCI= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/hc-install v0.7.0 h1:Uu9edVqjKQxxuD28mR5TikkKDd/p55S8vzPC1659aBk= -github.com/hashicorp/hc-install v0.7.0/go.mod h1:ELmmzZlGnEcqoUMKUuykHaPCIR1sYLYX+KSggWSKZuA= +github.com/hashicorp/hc-install v0.8.0 h1:LdpZeXkZYMQhoKPCecJHlKvUkQFixN/nvyR1CdfOLjI= +github.com/hashicorp/hc-install v0.8.0/go.mod h1:+MwJYjDfCruSD/udvBmRB22Nlkwwkwf5sAB6uTIhSaU= github.com/hashicorp/hcl/v2 v2.21.0 h1:lve4q/o/2rqwYOgUg3y3V2YPyD1/zkCLGjIV74Jit14= github.com/hashicorp/hcl/v2 v2.21.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= @@ -146,8 +148,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= +golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= From 9501a02b120c21132854c73c01d415dc53e6c81f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Aug 2024 09:21:06 -0400 Subject: [PATCH 6/8] build(deps): Bump golang.org/x/crypto from 0.25.0 to 0.26.0 (#364) Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.25.0 to 0.26.0. - [Commits](https://github.com/golang/crypto/compare/v0.25.0...v0.26.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 8 ++++---- go.sum | 20 ++++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index 0813ada5c..6e40ce49a 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 github.com/mitchellh/go-testing-interface v1.14.1 github.com/zclconf/go-cty v1.15.0 - golang.org/x/crypto v0.25.0 + golang.org/x/crypto v0.26.0 ) require ( @@ -51,9 +51,9 @@ require ( github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect golang.org/x/mod v0.19.0 // indirect golang.org/x/net v0.25.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.22.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect diff --git a/go.sum b/go.sum index 6db6736df..a88571538 100644 --- a/go.sum +++ b/go.sum @@ -145,8 +145,8 @@ 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= 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.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= @@ -159,8 +159,8 @@ golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -173,19 +173,19 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= -golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= From 5d78aa425672b061e9ff1195b4331db2070a1346 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Thu, 8 Aug 2024 23:29:18 +0200 Subject: [PATCH 7/8] Implement comparison state checks to replace `TestCheckResourceAttrPtr`, `TestCheckResourceAttrPair` and `TestCheckTypeSetElemAttrPair` (#330) * compare: Add compare package and implementations for ValuesSame, ValuesSameAny, ValuesDiffer, ValuesDifferAny, and ValuesDifferAll * statecheck: Add CompareValue, CompareValueContains, and CompareValuePairs state checks * statecheck: Fixing references in tests * Adding go docs and constructor to compare package types * Adding go docs for CompareValue state checks * Refactoring ValueComparer implementations to handle slice and map of interface values * Refactoring CompareValueCollection to extract nested values * Adding further test coverage for CompareValueCollection * Adding compare pkg doc * Removing ValuesDifferAll, ValuesDifferAny, ValuesSameAny * Adding website docs for value comparers and compare value state checks * Amending docs for known value checks * build(deps): Bump hashicorp/setup-terraform from 3.1.0 to 3.1.1 (#335) Bumps [hashicorp/setup-terraform](https://github.com/hashicorp/setup-terraform) from 3.1.0 to 3.1.1. - [Release notes](https://github.com/hashicorp/setup-terraform/releases) - [Changelog](https://github.com/hashicorp/setup-terraform/blob/main/CHANGELOG.md) - [Commits](https://github.com/hashicorp/setup-terraform/compare/97f030cf6dc0b4f5e0da352c7bca9cca34579800...651471c36a6092792c552e8b1bef71e592b462d8) --- updated-dependencies: - dependency-name: hashicorp/setup-terraform dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): Bump hashicorp/setup-copywrite from 1.1.2 to 1.1.3 (#336) Bumps [hashicorp/setup-copywrite](https://github.com/hashicorp/setup-copywrite) from 1.1.2 to 1.1.3. - [Release notes](https://github.com/hashicorp/setup-copywrite/releases) - [Commits](https://github.com/hashicorp/setup-copywrite/compare/867a1a2a064a0626db322392806428f7dc59cb3e...32638da2d4e81d56a0764aa1547882fc4d209636) --- updated-dependencies: - dependency-name: hashicorp/setup-copywrite dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Result of tsccr-helper -log-level=info gha update -latest . (#337) Co-authored-by: hashicorp-tsccr[bot] * build(deps): Bump github.com/hashicorp/terraform-json (#338) Bumps [github.com/hashicorp/terraform-json](https://github.com/hashicorp/terraform-json) from 0.21.0 to 0.22.0. - [Release notes](https://github.com/hashicorp/terraform-json/releases) - [Commits](https://github.com/hashicorp/terraform-json/compare/v0.21.0...v0.22.0) --- updated-dependencies: - dependency-name: github.com/hashicorp/terraform-json dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): Bump github.com/hashicorp/terraform-json (#339) Bumps [github.com/hashicorp/terraform-json](https://github.com/hashicorp/terraform-json) from 0.22.0 to 0.22.1. - [Release notes](https://github.com/hashicorp/terraform-json/releases) - [Commits](https://github.com/hashicorp/terraform-json/compare/v0.22.0...v0.22.1) --- updated-dependencies: - dependency-name: github.com/hashicorp/terraform-json dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): Bump github.com/hashicorp/terraform-exec (#341) Bumps [github.com/hashicorp/terraform-exec](https://github.com/hashicorp/terraform-exec) from 0.20.0 to 0.21.0. - [Release notes](https://github.com/hashicorp/terraform-exec/releases) - [Changelog](https://github.com/hashicorp/terraform-exec/blob/main/CHANGELOG.md) - [Commits](https://github.com/hashicorp/terraform-exec/compare/v0.20.0...v0.21.0) --- updated-dependencies: - dependency-name: github.com/hashicorp/terraform-exec dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * all: Add deferred action testing support (plan checks, version check, and CLI options) (#331) * sloppy first commit! * add version checks and tests * add changelogs * update terraform-plugin-go * spelling fix * update `terraform-json` * switch to pointer bool value * Update changelog * [CI] Update issue comment triage workflow file * [CI] Update issue comment triage workflow file * [CI] Update issue comment triage workflow file * [CI] Update issue comment triage workflow file * Result of tsccr-helper -log-level=info gha update -latest . (#342) Co-authored-by: hashicorp-tsccr[bot] * build(deps): Bump github.com/hashicorp/terraform-plugin-sdk/v2 (#343) Bumps [github.com/hashicorp/terraform-plugin-sdk/v2](https://github.com/hashicorp/terraform-plugin-sdk) from 2.33.0 to 2.34.0. - [Release notes](https://github.com/hashicorp/terraform-plugin-sdk/releases) - [Changelog](https://github.com/hashicorp/terraform-plugin-sdk/blob/main/CHANGELOG.md) - [Commits](https://github.com/hashicorp/terraform-plugin-sdk/compare/v2.33.0...v2.34.0) --- updated-dependencies: - dependency-name: github.com/hashicorp/terraform-plugin-sdk/v2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * workflows: Delete old issue label remove workflow (#344) Replaced by `.github/workflows/issue-comment-triage.yml` * build(deps): Bump github.com/hashicorp/hc-install from 0.6.4 to 0.7.0 (#345) updated-dependencies: - dependency-name: github.com/hashicorp/hc-install dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [CI] Update lock workflow file * build(deps): Bump github.com/hashicorp/go-version from 1.6.0 to 1.7.0 (#346) Bumps [github.com/hashicorp/go-version](https://github.com/hashicorp/go-version) from 1.6.0 to 1.7.0. - [Release notes](https://github.com/hashicorp/go-version/releases) - [Changelog](https://github.com/hashicorp/go-version/blob/main/CHANGELOG.md) - [Commits](https://github.com/hashicorp/go-version/compare/v1.6.0...v1.7.0) --- updated-dependencies: - dependency-name: github.com/hashicorp/go-version dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Result of tsccr-helper -log-level=info gha update -latest . (#348) Co-authored-by: hashicorp-tsccr[bot] * [CI] terraform-devex-repos automation * [CI] terraform-devex-repos automation * [CI] terraform-devex-repos automation * [CI] terraform-devex-repos automation * [CI] terraform-devex-repos automation * [CI] terraform-devex-repos automation * build(deps): Bump golang.org/x/crypto from 0.23.0 to 0.24.0 (#350) Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.23.0 to 0.24.0. - [Commits](https://github.com/golang/crypto/compare/v0.23.0...v0.24.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [CI] terraform-devex-repos automation * [CI] terraform-devex-repos automation * [CI] terraform-devex-repos automation * [CI] Update lock workflow file * build(deps): Bump github.com/hashicorp/hcl/v2 from 2.20.1 to 2.21.0 (#352) Bumps [github.com/hashicorp/hcl/v2](https://github.com/hashicorp/hcl) from 2.20.1 to 2.21.0. - [Release notes](https://github.com/hashicorp/hcl/releases) - [Changelog](https://github.com/hashicorp/hcl/blob/main/CHANGELOG.md) - [Commits](https://github.com/hashicorp/hcl/compare/v2.20.1...v2.21.0) --- updated-dependencies: - dependency-name: github.com/hashicorp/hcl/v2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * SEC-090: Automated trusted workflow pinning (2024-06-17) (#351) * Result of tsccr-helper -log-level=info gha update -latest . * Add version to .goreleaser file --------- Co-authored-by: hashicorp-tsccr[bot] Co-authored-by: Austin Valle * build(deps): Bump github.com/hashicorp/copywrite in /tools (#355) Bumps [github.com/hashicorp/copywrite](https://github.com/hashicorp/copywrite) from 0.18.0 to 0.19.0. - [Release notes](https://github.com/hashicorp/copywrite/releases) - [Changelog](https://github.com/hashicorp/copywrite/blob/main/.goreleaser.yaml) - [Commits](https://github.com/hashicorp/copywrite/compare/v0.18.0...v0.19.0) --- updated-dependencies: - dependency-name: github.com/hashicorp/copywrite dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Result of tsccr-helper -log-level=info gha update -latest . (#354) Co-authored-by: hashicorp-tsccr[bot] Co-authored-by: Austin Valle * knownvalue: Add `Int32Exact` and `Float32Exact` checks (#356) * Add `float32exact` and `int32exact` types * Update website documentation * Add changelog entries * Update changelog wording * build(deps): Bump golang.org/x/crypto from 0.24.0 to 0.25.0 (#358) Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.24.0 to 0.25.0. - [Commits](https://github.com/golang/crypto/compare/v0.24.0...v0.25.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Update changelog * all: Add deferred action testing support (plan checks, version check, and CLI options) (#331) * sloppy first commit! * add version checks and tests * add changelogs * update terraform-plugin-go * spelling fix * update `terraform-json` * switch to pointer bool value * Update changelog * [CI] terraform-devex-repos automation * [CI] terraform-devex-repos automation * knownvalue: Add `Int32Exact` and `Float32Exact` checks (#356) * Add `float32exact` and `int32exact` types * Update website documentation * Add changelog entries * Update changelog wording * Update changelog * quick doc fix * added invalid test case * adjust compare value collection to work with nested attributes * add changelogs * add tfversion skip for protov6 * update example for value comparers --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: hashicorp-tsccr[bot] <129506189+hashicorp-tsccr[bot]@users.noreply.github.com> Co-authored-by: hashicorp-tsccr[bot] Co-authored-by: Austin Valle Co-authored-by: hc-github-team-tf-provider-devex Co-authored-by: Service Account - Terraform Provider DevEx <100357958+hc-github-team-tf-provider-devex@users.noreply.github.com> Co-authored-by: Selena Goods --- .../unreleased/FEATURES-20240717-155731.yaml | 6 + .../unreleased/FEATURES-20240717-160116.yaml | 6 + .../unreleased/FEATURES-20240717-160331.yaml | 7 + .../unreleased/FEATURES-20240717-164418.yaml | 7 + .../unreleased/NOTES-20240717-155810.yaml | 6 + .../unreleased/NOTES-20240717-164911.yaml | 7 + compare/doc.go | 5 + compare/value_comparer.go | 13 + compare/values_differ.go | 31 + compare/values_differ_test.go | 62 + compare/values_same.go | 31 + compare/values_same_test.go | 73 + statecheck/compare_value.go | 114 + statecheck/compare_value_collection.go | 223 ++ statecheck/compare_value_collection_test.go | 1988 +++++++++++++++++ statecheck/compare_value_pairs.go | 111 + statecheck/compare_value_pairs_test.go | 142 ++ statecheck/compare_value_test.go | 241 ++ statecheck/expect_known_value_test.go | 20 + website/data/plugin-testing-nav-data.json | 9 + .../known-value-checks/index.mdx | 7 +- .../state-checks/resource.mdx | 220 +- .../testing/acceptance-tests/testcase.mdx | 2 +- .../value-comparers/index.mdx | 183 ++ website/docs/plugin/testing/index.mdx | 4 - 25 files changed, 3506 insertions(+), 12 deletions(-) create mode 100644 .changes/unreleased/FEATURES-20240717-155731.yaml create mode 100644 .changes/unreleased/FEATURES-20240717-160116.yaml create mode 100644 .changes/unreleased/FEATURES-20240717-160331.yaml create mode 100644 .changes/unreleased/FEATURES-20240717-164418.yaml create mode 100644 .changes/unreleased/NOTES-20240717-155810.yaml create mode 100644 .changes/unreleased/NOTES-20240717-164911.yaml create mode 100644 compare/doc.go create mode 100644 compare/value_comparer.go create mode 100644 compare/values_differ.go create mode 100644 compare/values_differ_test.go create mode 100644 compare/values_same.go create mode 100644 compare/values_same_test.go create mode 100644 statecheck/compare_value.go create mode 100644 statecheck/compare_value_collection.go create mode 100644 statecheck/compare_value_collection_test.go create mode 100644 statecheck/compare_value_pairs.go create mode 100644 statecheck/compare_value_pairs_test.go create mode 100644 statecheck/compare_value_test.go create mode 100644 website/docs/plugin/testing/acceptance-tests/value-comparers/index.mdx diff --git a/.changes/unreleased/FEATURES-20240717-155731.yaml b/.changes/unreleased/FEATURES-20240717-155731.yaml new file mode 100644 index 000000000..6f06b4eb9 --- /dev/null +++ b/.changes/unreleased/FEATURES-20240717-155731.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'compare: Introduced new `compare` package, which contains interfaces and implementations + for value comparisons in state checks.' +time: 2024-07-17T15:57:31.637692-04:00 +custom: + Issue: "330" diff --git a/.changes/unreleased/FEATURES-20240717-160116.yaml b/.changes/unreleased/FEATURES-20240717-160116.yaml new file mode 100644 index 000000000..6783377d1 --- /dev/null +++ b/.changes/unreleased/FEATURES-20240717-160116.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'statecheck: Added `CompareValue` state check, which compares sequential values of the + specified attribute at the given managed resource, or data source, using the supplied value comparer.' +time: 2024-07-17T16:01:16.194665-04:00 +custom: + Issue: "330" diff --git a/.changes/unreleased/FEATURES-20240717-160331.yaml b/.changes/unreleased/FEATURES-20240717-160331.yaml new file mode 100644 index 000000000..1187e32a3 --- /dev/null +++ b/.changes/unreleased/FEATURES-20240717-160331.yaml @@ -0,0 +1,7 @@ +kind: FEATURES +body: 'statecheck: Added `CompareValueCollection` state check, which compares each item in + the specified collection (e.g., list, set) attribute, with the second specified + attribute at the given managed resources, or data sources, using the supplied value comparer.' +time: 2024-07-17T16:03:31.77827-04:00 +custom: + Issue: "330" diff --git a/.changes/unreleased/FEATURES-20240717-164418.yaml b/.changes/unreleased/FEATURES-20240717-164418.yaml new file mode 100644 index 000000000..312311b1f --- /dev/null +++ b/.changes/unreleased/FEATURES-20240717-164418.yaml @@ -0,0 +1,7 @@ +kind: FEATURES +body: 'statecheck: Added `CompareValuePairs` state check, which compares the + specified attributes at the given managed resources, or data sources, using + the supplied value comparer.' +time: 2024-07-17T16:44:18.612874-04:00 +custom: + Issue: "330" diff --git a/.changes/unreleased/NOTES-20240717-155810.yaml b/.changes/unreleased/NOTES-20240717-155810.yaml new file mode 100644 index 000000000..a8edd41c4 --- /dev/null +++ b/.changes/unreleased/NOTES-20240717-155810.yaml @@ -0,0 +1,6 @@ +kind: NOTES +body: 'compare: The `compare` package is considered experimental and may be altered + or removed in a subsequent release' +time: 2024-07-17T15:58:10.435384-04:00 +custom: + Issue: "330" diff --git a/.changes/unreleased/NOTES-20240717-164911.yaml b/.changes/unreleased/NOTES-20240717-164911.yaml new file mode 100644 index 000000000..bc3af510b --- /dev/null +++ b/.changes/unreleased/NOTES-20240717-164911.yaml @@ -0,0 +1,7 @@ +kind: NOTES +body: 'statecheck: `CompareValue`, `CompareValueCollection`, and `CompareValuePairs` + state checks are considered experimental and may be altered or removed in a subsequent + release.' +time: 2024-07-17T16:49:11.296585-04:00 +custom: + Issue: "330" diff --git a/compare/doc.go b/compare/doc.go new file mode 100644 index 000000000..feb4a4c00 --- /dev/null +++ b/compare/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package compare contains the value comparer interface, and types implementing the value comparer interface. +package compare diff --git a/compare/value_comparer.go b/compare/value_comparer.go new file mode 100644 index 000000000..af635898b --- /dev/null +++ b/compare/value_comparer.go @@ -0,0 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package compare + +// ValueComparer defines an interface that is implemented to run comparison logic on multiple values. Individual +// implementations determine how the comparison is performed (e.g., values differ, values equal). +type ValueComparer interface { + // CompareValues should assert the given known values against any expectations. + // Values are always ordered in the order they were added. Use the error + // return to signal unexpected values or implementation errors. + CompareValues(values ...any) error +} diff --git a/compare/values_differ.go b/compare/values_differ.go new file mode 100644 index 000000000..24bd2ae22 --- /dev/null +++ b/compare/values_differ.go @@ -0,0 +1,31 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package compare + +import ( + "fmt" + "reflect" +) + +var _ ValueComparer = valuesDiffer{} + +type valuesDiffer struct{} + +// CompareValues determines whether each value in the sequence of the supplied values +// differs from the preceding value. +func (v valuesDiffer) CompareValues(values ...any) error { + for i := 1; i < len(values); i++ { + if reflect.DeepEqual(values[i-1], values[i]) { + return fmt.Errorf("expected values to differ, but they are the same: %v == %v", values[i-1], values[i]) + } + } + + return nil +} + +// ValuesDiffer returns a ValueComparer for asserting that each value in the sequence of +// the values supplied to the CompareValues method differs from the preceding value. +func ValuesDiffer() valuesDiffer { + return valuesDiffer{} +} diff --git a/compare/values_differ_test.go b/compare/values_differ_test.go new file mode 100644 index 000000000..35653f339 --- /dev/null +++ b/compare/values_differ_test.go @@ -0,0 +1,62 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package compare_test + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/compare" +) + +func TestValuesDiffer_CompareValues(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + in []any + expectedError error + }{ + "nil": {}, + "single-value": { + in: []any{"str"}, + }, + "non-matching-sequential-values": { + in: []any{"str", "other_str", "str"}, + }, + "matching-values-string": { + in: []any{"str", "other_str", "other_str"}, + expectedError: fmt.Errorf("expected values to differ, but they are the same: other_str == other_str"), + }, + "matching-values-slice": { + in: []any{ + []any{"other_str"}, + []any{"other_str"}, + }, + expectedError: fmt.Errorf("expected values to differ, but they are the same: [other_str] == [other_str]"), + }, + "matching-values-map": { + in: []any{ + map[string]any{"a": "other_str"}, + map[string]any{"a": "other_str"}, + }, + expectedError: fmt.Errorf("expected values to differ, but they are the same: map[a:other_str] == map[a:other_str]"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + err := compare.ValuesDiffer().CompareValues(testCase.in...) + + if diff := cmp.Diff(err, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/compare/values_same.go b/compare/values_same.go new file mode 100644 index 000000000..46ee13f31 --- /dev/null +++ b/compare/values_same.go @@ -0,0 +1,31 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package compare + +import ( + "fmt" + "reflect" +) + +var _ ValueComparer = valuesSame{} + +type valuesSame struct{} + +// CompareValues determines whether each value in the sequence of the supplied values +// is the same as the preceding value. +func (v valuesSame) CompareValues(values ...any) error { + for i := 1; i < len(values); i++ { + if !reflect.DeepEqual(values[i-1], values[i]) { + return fmt.Errorf("expected values to be the same, but they differ: %v != %v", values[i-1], values[i]) + } + } + + return nil +} + +// ValuesSame returns a ValueComparer for asserting that each value in the sequence of +// the values supplied to the CompareValues method is the same as the preceding value. +func ValuesSame() valuesSame { + return valuesSame{} +} diff --git a/compare/values_same_test.go b/compare/values_same_test.go new file mode 100644 index 000000000..dde2ee6ea --- /dev/null +++ b/compare/values_same_test.go @@ -0,0 +1,73 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package compare_test + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/compare" +) + +func TestValuesSame_CompareValues(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + in []any + expectedError error + }{ + "nil": {}, + "single-value": { + in: []any{"str"}, + }, + "matching-values": { + in: []any{"str", "str", "str"}, + }, + "non-matching-values-string": { + in: []any{"str", "str", "other_str"}, + expectedError: fmt.Errorf("expected values to be the same, but they differ: str != other_str"), + }, + "non-matching-values-slice": { + in: []any{ + []any{"str"}, + []any{"str"}, + []any{"other_str"}, + }, + expectedError: fmt.Errorf("expected values to be the same, but they differ: [str] != [other_str]"), + }, + "non-matching-values-map": { + in: []any{ + map[string]any{"a": "str"}, + map[string]any{"a": "str"}, + map[string]any{"a": "other_str"}, + }, + expectedError: fmt.Errorf("expected values to be the same, but they differ: map[a:str] != map[a:other_str]"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + err := compare.ValuesSame().CompareValues(testCase.in...) + + if diff := cmp.Diff(err, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +// equateErrorMessage reports errors to be equal if both are nil +// or both have the same message. +var equateErrorMessage = cmp.Comparer(func(x, y error) bool { + if x == nil || y == nil { + return x == nil && y == nil + } + return x.Error() == y.Error() +}) diff --git a/statecheck/compare_value.go b/statecheck/compare_value.go new file mode 100644 index 000000000..68a6ef9d5 --- /dev/null +++ b/statecheck/compare_value.go @@ -0,0 +1,114 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck + +import ( + "context" + "fmt" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/compare" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +// Resource State Check +var _ StateCheck = &compareValue{} + +type compareValue struct { + resourceAddresses []string + attributePaths []tfjsonpath.Path + stateValues []any + comparer compare.ValueComparer +} + +func (e *compareValue) AddStateValue(resourceAddress string, attributePath tfjsonpath.Path) StateCheck { + e.resourceAddresses = append(e.resourceAddresses, resourceAddress) + e.attributePaths = append(e.attributePaths, attributePath) + + return e +} + +// CheckState implements the state check logic. +func (e *compareValue) CheckState(ctx context.Context, req CheckStateRequest, resp *CheckStateResponse) { + var resource *tfjson.StateResource + + if req.State == nil { + resp.Error = fmt.Errorf("state is nil") + + return + } + + if req.State.Values == nil { + resp.Error = fmt.Errorf("state does not contain any state values") + + return + } + + if req.State.Values.RootModule == nil { + resp.Error = fmt.Errorf("state does not contain a root module") + + return + } + + // All calls to AddStateValue occur before any TestStep is run, populating the resourceAddresses + // and attributePaths slices. The stateValues slice is populated during execution of each TestStep. + // Each call to CheckState happens sequentially during each TestStep. + // The currentIndex is reflective of the current state value being checked. + currentIndex := len(e.stateValues) + + 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.State.Values.RootModule.Resources { + if resourceAddress == r.Address { + resource = r + + break + } + } + + if resource == nil { + resp.Error = fmt.Errorf("%s - Resource not found in state", 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.stateValues = append(e.stateValues, result) + + err = e.comparer.CompareValues(e.stateValues...) + + if err != nil { + resp.Error = err + } +} + +// CompareValue returns a state check that compares values retrieved from state using the +// supplied value comparer. +func CompareValue(comparer compare.ValueComparer) *compareValue { + return &compareValue{ + comparer: comparer, + } +} diff --git a/statecheck/compare_value_collection.go b/statecheck/compare_value_collection.go new file mode 100644 index 000000000..7a06c6010 --- /dev/null +++ b/statecheck/compare_value_collection.go @@ -0,0 +1,223 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck + +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 State Check +var _ StateCheck = &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 +} + +// CheckState implements the state check logic. +func (e *compareValueCollection) CheckState(ctx context.Context, req CheckStateRequest, resp *CheckStateResponse) { + var resourceOne *tfjson.StateResource + var resourceTwo *tfjson.StateResource + + if req.State == nil { + resp.Error = fmt.Errorf("state is nil") + + return + } + + if req.State.Values == nil { + resp.Error = fmt.Errorf("state does not contain any state values") + + return + } + + if req.State.Values.RootModule == nil { + resp.Error = fmt.Errorf("state does not contain a root module") + + return + } + + for _, r := range req.State.Values.RootModule.Resources { + if e.resourceAddressOne == r.Address { + resourceOne = r + + break + } + } + + if resourceOne == nil { + resp.Error = fmt.Errorf("%s - Resource not found in state", 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.State.Values.RootModule.Resources { + if e.resourceAddressTwo == r.Address { + resourceTwo = r + + break + } + } + + if resourceTwo == nil { + resp.Error = fmt.Errorf("%s - Resource not found in state", 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 state 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) StateCheck { + return &compareValueCollection{ + resourceAddressOne: resourceAddressOne, + collectionPath: collectionPath, + resourceAddressTwo: resourceAddressTwo, + attributePath: attributePath, + comparer: comparer, + } +} diff --git a/statecheck/compare_value_collection_test.go b/statecheck/compare_value_collection_test.go new file mode 100644 index 000000000..31cdc9ec0 --- /dev/null +++ b/statecheck/compare_value_collection_test.go @@ -0,0 +1,1988 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck_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/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestCompareValueCollection_CheckState_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 + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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 + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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 + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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", + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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", + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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", + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("list_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesSame(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_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", + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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", + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("list_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesDiffer(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_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" + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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" + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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" + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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" + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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", + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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", + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("map_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesSame(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_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", + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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", + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("map_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesDiffer(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_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" + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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" + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesSame(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_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", + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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" + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesDiffer(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_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" + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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" + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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" + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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" + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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" + } + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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" + } + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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" + } + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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" + } + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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" + } + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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" + } + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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" + } + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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" + } + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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" + } + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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" + } + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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" + } + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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" + } + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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" + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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" + } + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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" + } + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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" + } + } + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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" + } + } + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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/statecheck/compare_value_pairs.go b/statecheck/compare_value_pairs.go new file mode 100644 index 000000000..8db67c562 --- /dev/null +++ b/statecheck/compare_value_pairs.go @@ -0,0 +1,111 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck + +import ( + "context" + "fmt" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/compare" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +// Resource State Check +var _ StateCheck = &compareValuePairs{} + +type compareValuePairs struct { + resourceAddressOne string + attributePathOne tfjsonpath.Path + resourceAddressTwo string + attributePathTwo tfjsonpath.Path + comparer compare.ValueComparer +} + +// CheckState implements the state check logic. +func (e *compareValuePairs) CheckState(ctx context.Context, req CheckStateRequest, resp *CheckStateResponse) { + var resourceOne *tfjson.StateResource + var resourceTwo *tfjson.StateResource + + if req.State == nil { + resp.Error = fmt.Errorf("state is nil") + + return + } + + if req.State.Values == nil { + resp.Error = fmt.Errorf("state does not contain any state values") + + return + } + + if req.State.Values.RootModule == nil { + resp.Error = fmt.Errorf("state does not contain a root module") + + return + } + + for _, r := range req.State.Values.RootModule.Resources { + if e.resourceAddressOne == r.Address { + resourceOne = r + + break + } + } + + if resourceOne == nil { + resp.Error = fmt.Errorf("%s - Resource not found in state", e.resourceAddressOne) + + return + } + + resultOne, err := tfjsonpath.Traverse(resourceOne.AttributeValues, e.attributePathOne) + + if err != nil { + resp.Error = err + + return + } + + for _, r := range req.State.Values.RootModule.Resources { + if e.resourceAddressTwo == r.Address { + resourceTwo = r + + break + } + } + + if resourceTwo == nil { + resp.Error = fmt.Errorf("%s - Resource not found in state", 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 state check that compares the value in state for the first given resource address and +// path with the value in state 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) StateCheck { + return &compareValuePairs{ + resourceAddressOne: resourceAddressOne, + attributePathOne: attributePathOne, + resourceAddressTwo: resourceAddressTwo, + attributePathTwo: attributePathTwo, + comparer: comparer, + } +} diff --git a/statecheck/compare_value_pairs_test.go b/statecheck/compare_value_pairs_test.go new file mode 100644 index 000000000..4df478832 --- /dev/null +++ b/statecheck/compare_value_pairs_test.go @@ -0,0 +1,142 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck_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/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestCompareValuePairs_CheckState_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 + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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 + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValuePairs( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + "test_resource.two", + tfjsonpath.New("bool_attribute"), + compare.ValuesSame(), + ), + }, + }, + }, + }) +} + +func TestCompareValuePairs_CheckState_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 + }`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.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_CheckState_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 + }`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValuePairs( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + "test_resource.one", + tfjsonpath.New("float_attribute"), + compare.ValuesDiffer(), + ), + }, + }, + }, + }) +} diff --git a/statecheck/compare_value_test.go b/statecheck/compare_value_test.go new file mode 100644 index 000000000..271692425 --- /dev/null +++ b/statecheck/compare_value_test.go @@ -0,0 +1,241 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck_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/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestCompareValue_CheckState_NoStateValues(t *testing.T) { + t.Parallel() + + boolValuesDiffer := statecheck.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 + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + // No state values have been added + boolValuesDiffer, + }, + ExpectError: regexp.MustCompile(`resource addresses index out of bounds: 0`), + }, + }, + }) +} + +func TestCompareValue_CheckState_ValuesSame_ValueDiffersError(t *testing.T) { + t.Parallel() + + boolValuesDiffer := statecheck.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 + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + boolValuesDiffer.AddStateValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + ), + }, + }, + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + boolValuesDiffer.AddStateValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + ), + }, + }, + { + Config: `resource "test_resource" "one" { + bool_attribute = false + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + boolValuesDiffer.AddStateValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + ), + }, + ExpectError: regexp.MustCompile(`expected values to be the same, but they differ: true != false`), + }, + }, + }) +} + +func TestCompareValue_CheckState_ValuesSame(t *testing.T) { + t.Parallel() + + boolValuesDiffer := statecheck.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 + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + boolValuesDiffer.AddStateValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + ), + }, + }, + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + boolValuesDiffer.AddStateValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + ), + }, + }, + }, + }) +} + +func TestCompareValue_CheckState_ValuesDiffer_ValueSameError(t *testing.T) { + t.Parallel() + + boolValuesDiffer := statecheck.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 + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + boolValuesDiffer.AddStateValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + ), + }, + }, + { + Config: `resource "test_resource" "one" { + bool_attribute = false + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + boolValuesDiffer.AddStateValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + ), + }, + }, + { + Config: `resource "test_resource" "one" { + bool_attribute = false + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + boolValuesDiffer.AddStateValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + ), + }, + ExpectError: regexp.MustCompile(`expected values to differ, but they are the same: false == false`), + }, + }, + }) +} + +func TestCompareValue_CheckState_ValuesDiffer(t *testing.T) { + t.Parallel() + + boolValuesDiffer := statecheck.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 + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + boolValuesDiffer.AddStateValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + ), + }, + }, + { + Config: `resource "test_resource" "one" { + bool_attribute = false + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + boolValuesDiffer.AddStateValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + ), + }, + }, + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + boolValuesDiffer.AddStateValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + ), + }, + }, + }, + }) +} diff --git a/statecheck/expect_known_value_test.go b/statecheck/expect_known_value_test.go index f02b8cd09..2eb56d4e4 100644 --- a/statecheck/expect_known_value_test.go +++ b/statecheck/expect_known_value_test.go @@ -1622,6 +1622,26 @@ func testProvider() *schema.Provider { }, }, }, + "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, diff --git a/website/data/plugin-testing-nav-data.json b/website/data/plugin-testing-nav-data.json index 6fbce82b3..be9cff643 100644 --- a/website/data/plugin-testing-nav-data.json +++ b/website/data/plugin-testing-nav-data.json @@ -140,6 +140,15 @@ } ] }, + { + "title": "Value Comparers", + "routes": [ + { + "title": "Overview", + "path": "acceptance-tests/value-comparers" + } + ] + }, { "title": "Sweepers", "path": "acceptance-tests/sweepers" diff --git a/website/docs/plugin/testing/acceptance-tests/known-value-checks/index.mdx b/website/docs/plugin/testing/acceptance-tests/known-value-checks/index.mdx index a942ce660..569f6b5b6 100644 --- a/website/docs/plugin/testing/acceptance-tests/known-value-checks/index.mdx +++ b/website/docs/plugin/testing/acceptance-tests/known-value-checks/index.mdx @@ -2,18 +2,19 @@ page_title: 'Plugin Development - Acceptance Testing: Known Values' description: >- How to use known values in the testing module. - Known values define an expected type, and value for a resource attribute, or output value in a Terraform plan for use in Plan Checks. + Known values define an expected type, and value for a resource attribute, or output value in a Terraform plan or state for use in Plan Checks or State Checks. --- # Known Value Checks -Known Value Checks are for use in conjunction with [Plan Checks](/terraform/plugin/testing/acceptance-tests/plan-checks), which leverage the [terraform-json](https://pkg.go.dev/github.com/hashicorp/terraform-json) representation of a Terraform plan. +Known Value Checks are for use in conjunction with [Plan Checks](/terraform/plugin/testing/acceptance-tests/plan-checks), and [State Checks](/terraform/plugin/testing/acceptance-tests/state-checks) which leverage the [terraform-json](https://pkg.go.dev/github.com/hashicorp/terraform-json) representation of a Terraform plan. ## Usage Example uses in the testing module include: -- The [`ExpectknownValue()`](/terraform/plugin/testing/acceptance-tests/plan-checks/resource#example-using-plancheck-expectknownvalue), [`ExpectKnownOutputValue()`](/terraform/plugin/testing/acceptance-tests/plan-checks/output#example-using-plancheck-expectknownoutputvalue) and [`ExpectKnownOutputValueAtPath()`](/terraform/plugin/testing/acceptance-tests/plan-checks/output#example-using-plancheck-expectknownoutputvalueatpath) [built-in plan checks](/terraform/plugin/testing/acceptance-tests/plan-checks) use known value checks for asserting whether a specific resource attribute, or output value has a particular type, and value. +- **Plan Checks**: The [`ExpectKnownValue()`](/terraform/plugin/testing/acceptance-tests/plan-checks/resource#expectknownvalue-plan-check), [`ExpectKnownOutputValue()`](/terraform/plugin/testing/acceptance-tests/plan-checks/output#expectknownoutputvalue-plan-check) and [`ExpectKnownOutputValueAtPath()`](/terraform/plugin/testing/acceptance-tests/plan-checks/output#expectknownoutputvalueatpath-plan-check) [built-in plan checks](/terraform/plugin/testing/acceptance-tests/plan-checks) use known value checks for asserting whether a specific resource attribute, or output value has a particular type, and value. +- **State Checks**: The [`ExpectKnownValue()`](/terraform/plugin/testing/acceptance-tests/state-checks/resource#expectknownvalue-state-check), [`ExpectKnownOutputValue()`](/terraform/plugin/testing/acceptance-tests/state-checks/output#expectknownoutputvalue-state-check) and [`ExpectKnownOutputValueAtPath()`](/terraform/plugin/testing/acceptance-tests/state-checks/output#expectknownoutputvalueatpath-state-check) [built-in state checks](/terraform/plugin/testing/acceptance-tests/state-checks) use known value checks for asserting whether a specific resource attribute, or output value has a particular type, and value. ## Using a Known Value Check diff --git a/website/docs/plugin/testing/acceptance-tests/state-checks/resource.mdx b/website/docs/plugin/testing/acceptance-tests/state-checks/resource.mdx index a3e94cbbe..d11db8f63 100644 --- a/website/docs/plugin/testing/acceptance-tests/state-checks/resource.mdx +++ b/website/docs/plugin/testing/acceptance-tests/state-checks/resource.mdx @@ -9,10 +9,222 @@ description: >- The `terraform-plugin-testing` module provides a package [`statecheck`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/statecheck) with built-in managed resource, and data source state checks for common use-cases: -| Check | Description | -|-----------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [`ExpectKnownValue`](/terraform/plugin/testing/acceptance-tests/state-checks/resource#expectknownvalue-state-check) | Asserts the specified attribute at the given managed resource, or data source, has the specified type, and value. | -| [`ExpectSensitiveValue`](/terraform/plugin/testing/acceptance-tests/state-checks/resource#expectsensitivevalue-state-check) | Asserts the specified attribute at the given managed resource, or data source, has a sensitive value. | +| Check | Description | +|---------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [`CompareValue`](/terraform/plugin/testing/acceptance-tests/state-checks/resource#comparevalue-state-check) | Compares sequential values of the specified attribute at the given managed resource, or data source, using the supplied [value comparer](/terraform/plugin/testing/acceptance-tests/value-comparers). | +| [`CompareValueCollection`](/terraform/plugin/testing/acceptance-tests/state-checks/resource#comparevaluecollection-state-check) | Compares each item in the specified collection (e.g., list, set) attribute, with the second specified attribute at the given managed resources, or data sources, using the supplied [value comparer](/terraform/plugin/testing/acceptance-tests/value-comparers). | +| [`CompareValuePairs`](/terraform/plugin/testing/acceptance-tests/state-checks/resource#comparevaluecollection-state-check) | Compares the specified attributes at the given managed resources, or data sources, using the supplied [value comparer](/terraform/plugin/testing/acceptance-tests/value-comparers). | +| [`ExpectKnownValue`](/terraform/plugin/testing/acceptance-tests/state-checks/resource#expectknownvalue-state-check) | Asserts the specified attribute at the given managed resource, or data source, has the specified type, and value. | +| [`ExpectSensitiveValue`](/terraform/plugin/testing/acceptance-tests/state-checks/resource#expectsensitivevalue-state-check) | Asserts the specified attribute at the given managed resource, or data source, has a sensitive value. | + +## `CompareValue` State Check + +The intended usage of [`statecheck.CompareValue(comparer)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/statecheck#CompareValue) state check is to retrieve a specific resource attribute value from state during sequential test steps, and to compare these values using the supplied value comparer. + +Refer to [Value Comparers](/terraform/plugin/testing/acceptance-tests/value-comparers) for details, and examples of the available [compare.ValueComparer](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/compare#ValueComparer) types that can be used with the `CompareValue` state check. + +```go +package example_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/compare" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestCompareValue_CheckState_ValuesSame(t *testing.T) { + t.Parallel() + + compareValuesSame := statecheck.CompareValue(compare.ValuesSame()) + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + // Example resource containing a computed attribute named "computed_attribute" + Config: `resource "test_resource" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + compareValuesSame.AddStateValue( + "test_resource.one", + tfjsonpath.New("computed_attribute"), + ), + }, + }, + { + // Example resource containing a computed attribute named "computed_attribute" + Config: `resource "test_resource" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + compareValuesSame.AddStateValue( + "test_resource.one", + tfjsonpath.New("computed_attribute"), + ), + }, + }, + }, + }) +} +``` + +## `CompareValueCollection` State Check + +The [`statecheck.CompareValueCollection(resourceAddressOne, collectionPath, resourceAddressTwo, attributePath, comparer)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/statecheck#CompareValueCollection) state check retrieves a specific collection (e.g., list, set) resource attribute, and a second resource attribute from state, and compares each of the items in the collection with the second attribute using the supplied value comparer. + +Refer to [Value Comparers](/terraform/plugin/testing/acceptance-tests/value-comparers) for details, and examples of the available [compare.ValueComparer](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/compare#ValueComparer) types that can be used with the `CompareValueCollection` state check. + +The following example illustrates how a `CompareValue` state check can be used to determine whether an attribute value appears in a collection attribute. Note that this is for illustrative purposes only, `CompareValue` should only be used for checking computed values. + +```go +package example_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/compare" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestCompareValueCollection_CheckState_ValuesSame(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + // The following is for illustrative purposes. State checking + // should only be used for computed attributes + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + list_attribute = [ + "str2", + "str", + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("list_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesSame(), + ), + }, + }, + }, + }) +} +``` + +The following example illustrates how a `CompareValue` state check can be used to determine whether an object attribute value appears in a collection (e.g., list) attribute containing objects. Note that this is for illustrative purposes only, `CompareValue` should only be used for checking computed values. + + +```go +package example_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/compare" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestCompareValueCollection_CheckState_ValuesSame(t *testing.T) { + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + // The following is for illustrative purposes. State checking + // should only be used for computed attributes + Config: `resource "test_resource" "one" { + list_nested_attribute = [ + { + a = false + b = "two" + }, + { + a = true + b = "four" + } + ] + single_nested_attribute = { + a = true + b = "four" + } + }`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.one", + []tfjsonpath.Path{ + tfjsonpath.New("list_nested_attribute"), + tfjsonpath.New("b"), + }, + "test_resource.one", + tfjsonpath.New("single_nested_attribute").AtMapKey("b"), + compare.ValuesSame(), + ), + }, + }, + }, + }) +} +``` + +## `CompareValuePairs` State Check + +The [`statecheck.CompareValuePairs(resourceAddressOne, attributePathOne, resourceAddressTwo, attributePathTwo, comparer)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/statecheck#CompareValuePairs) state check provides a basis for retrieving a pair of attribute values, and comparing them using the supplied value comparer. + +Refer to [Value Comparers](/terraform/plugin/testing/acceptance-tests/value-comparers) for details, and examples of the available [compare.ValueComparer](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/compare#ValueComparer) types that can be used with the `CompareValuePairs` state check. + +```go +package statecheck_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/compare" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestCompareValuePairs_CheckState_ValuesSame(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + // Example resource containing a computed attribute named "computed_attribute" + Config: `resource "test_resource" "one" {} + + resource "test_resource" "two" {} + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValuePairs( + "test_resource.one", + tfjsonpath.New("computed_attribute"), + "test_resource.two", + tfjsonpath.New("computed_attribute"), + compare.ValuesSame(), + ), + }, + }, + }, + }) +} +``` ## `ExpectKnownValue` State Check diff --git a/website/docs/plugin/testing/acceptance-tests/testcase.mdx b/website/docs/plugin/testing/acceptance-tests/testcase.mdx index 57ddd888f..77ba53078 100644 --- a/website/docs/plugin/testing/acceptance-tests/testcase.mdx +++ b/website/docs/plugin/testing/acceptance-tests/testcase.mdx @@ -157,7 +157,7 @@ but before any test steps are executed. The [Terraform Version Checks](/terrafor are generic checks that check logic against the Terraform CLI version and can immediately pass or fail a test before any test steps are executed. -The tfversion package provides built-in checks for common scenarios. +The [`tfversion`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion) package provides built-in checks for common scenarios. **Example usage:** diff --git a/website/docs/plugin/testing/acceptance-tests/value-comparers/index.mdx b/website/docs/plugin/testing/acceptance-tests/value-comparers/index.mdx new file mode 100644 index 000000000..3ef5e19d6 --- /dev/null +++ b/website/docs/plugin/testing/acceptance-tests/value-comparers/index.mdx @@ -0,0 +1,183 @@ +--- +page_title: 'Plugin Development - Acceptance Testing: Value Comparers' +description: >- + How to use value comparers in the testing module. + Value comparers define a comparison for a resource attribute, or output value for use in State Checks. +--- + +# Value Comparers + + + +Value Comparers are for use in conjunction with [State Checks](/terraform/plugin/testing/acceptance-tests/state-checks), which leverage the [terraform-json](https://pkg.go.dev/github.com/hashicorp/terraform-json) representation of Terraform state. + + + +Value comparers can be used to assert a resource or data source attribute value across multiple [Test Steps](/terraform/plugin/testing/acceptance-tests/teststep), like asserting that a randomly generated resource attribute doesn't change after multiple apply steps. This is done by creating the value comparer, typically before the test case is defined, using the relevant constructor function: +```go +func TestExample(t *testing.T) { + // Create the value comparer so we can add state values to it during the test steps + compareValuesDiffer := statecheck.CompareValue(compare.ValuesDiffer()) + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + // .. test steps omitted + }, + }) +} +``` + +Once the value comparer is created, state values can be added in `TestStep.ConfigStateChecks`: +```go +func TestExample(t *testing.T) { + // Create the value comparer so we can add state values to it during the test steps + compareValuesDiffer := statecheck.CompareValue(compare.ValuesDiffer()) + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + // Add the current state value of "computed_attribute" to the value comparer. + // Since there are no other state values at this point, no assertion is made. + compareValuesDiffer.AddStateValue( + "test_resource.one", + tfjsonpath.New("computed_attribute"), + ), + }, + }, + { + Config: `resource "test_resource" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + // Add the current state value of "computed_attribute" to the value comparer. + // Since there is an existing state value in the value comparer at this point, + // if the two values are equal, the test will produce an error. + compareValuesDiffer.AddStateValue( + "test_resource.one", + tfjsonpath.New("computed_attribute"), + ), + }, + }, + }, + }) +} +``` + +The value comparer implementation (defined by the [`ValueComparer` interface](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/compare#ValueComparer)) determines what assertion occurs when a state value is added. The built-in value comparers are: +- [`CompareValue()`](/terraform/plugin/testing/acceptance-tests/state-checks/resource#comparevalue-state-check) +- [`CompareValueCollection()`](/terraform/plugin/testing/acceptance-tests/state-checks/resource#comparevaluecollection-state-check) +- [`CompareValuePairs()`](/terraform/plugin/testing/acceptance-tests/state-checks/resource#comparevaluepairs-state-check) + +## Values Differ + +The [ValuesDiffer](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/compare#ValuesDiffer) value comparer verifies that each value in the sequence of values supplied to the `CompareValues()` method differs from the preceding value. + +Example usage of [ValuesDiffer](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/compare#ValuesDiffer) in a [CompareValue](/terraform/plugin/testing/acceptance-tests/state-checks/resource#comparevalue-state-check) state check. + +```go +package example_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-plugin-testing/compare" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestCompareValue_CheckState_ValuesDiffer(t *testing.T) { + t.Parallel() + + compareValuesDiffer := statecheck.CompareValue(compare.ValuesDiffer()) + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + // Add the current state value of "computed_attribute" to the value comparer. + // Since there are no other state values at this point, no assertion is made. + compareValuesDiffer.AddStateValue( + "test_resource.one", + tfjsonpath.New("computed_attribute"), + ), + }, + }, + { + // Add the current state value of "computed_attribute" to the value comparer. + // Since there is an existing state value in the value comparer at this point, + // if the two values are equal, the test will produce an error. + Config: `resource "test_resource" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + compareValuesDiffer.AddStateValue( + "test_resource.one", + tfjsonpath.New("computed_attribute"), + ), + }, + }, + }, + }) +} +``` + +## Values Same + +The [ValuesSame](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/compare#ValuesSame) value comparer verifies that each value in the sequence of values supplied to the `CompareValues()` method is the same as the preceding value. + +Example usage of [ValuesSame](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/compare#ValuesSame) in a [CompareValue](/terraform/plugin/testing/acceptance-tests/state-checks/resource#comparevalue-state-check) state check. + +```go +package example_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-plugin-testing/compare" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestCompareValue_CheckState_ValuesSame(t *testing.T) { + t.Parallel() + + compareValuesSame := statecheck.CompareValue(compare.ValuesSame()) + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + // Add the current state value of "computed_attribute" to the value comparer. + // Since there are no other state values at this point, no assertion is made. + compareValuesSame.AddStateValue( + "test_resource.one", + tfjsonpath.New("computed_attribute"), + ), + }, + }, + { + Config: `resource "test_resource" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + // Add the current state value of "computed_attribute" to the value comparer. + // Since there is an existing state value in the value comparer at this point, + // if the two values are not equal, the test will produce an error. + compareValuesSame.AddStateValue( + "test_resource.one", + tfjsonpath.New("computed_attribute"), + ), + }, + }, + }, + }) +} +``` diff --git a/website/docs/plugin/testing/index.mdx b/website/docs/plugin/testing/index.mdx index 2ed9cb366..d994c3ad7 100644 --- a/website/docs/plugin/testing/index.mdx +++ b/website/docs/plugin/testing/index.mdx @@ -37,10 +37,6 @@ plugins, **it is highly recommended to use an account dedicated to testing, to ensure no infrastructure is created in error in any environment that cannot be completely and safely destroyed.** -HashiCorp runs nightly acceptance tests of providers found in the [Terraform -Providers GitHub Organization](https://github.com/terraform-providers) to ensure -each Provider is working correctly. - For a given plugin, Acceptance Tests can be run from the root of the project by using a common make task: From dfafdd7aa12923aad56131dc541bc3278bd8a712 Mon Sep 17 00:00:00 2001 From: hc-github-team-tf-provider-devex Date: Thu, 8 Aug 2024 21:30:59 +0000 Subject: [PATCH 8/8] Update changelog --- .changes/1.10.0.md | 14 ++++++++++++++ .changes/unreleased/FEATURES-20240717-155731.yaml | 6 ------ .changes/unreleased/FEATURES-20240717-160116.yaml | 6 ------ .changes/unreleased/FEATURES-20240717-160331.yaml | 7 ------- .changes/unreleased/FEATURES-20240717-164418.yaml | 7 ------- .changes/unreleased/NOTES-20240717-155810.yaml | 6 ------ .changes/unreleased/NOTES-20240717-164911.yaml | 7 ------- CHANGELOG.md | 14 ++++++++++++++ 8 files changed, 28 insertions(+), 39 deletions(-) create mode 100644 .changes/1.10.0.md delete mode 100644 .changes/unreleased/FEATURES-20240717-155731.yaml delete mode 100644 .changes/unreleased/FEATURES-20240717-160116.yaml delete mode 100644 .changes/unreleased/FEATURES-20240717-160331.yaml delete mode 100644 .changes/unreleased/FEATURES-20240717-164418.yaml delete mode 100644 .changes/unreleased/NOTES-20240717-155810.yaml delete mode 100644 .changes/unreleased/NOTES-20240717-164911.yaml diff --git a/.changes/1.10.0.md b/.changes/1.10.0.md new file mode 100644 index 000000000..ca9a927d6 --- /dev/null +++ b/.changes/1.10.0.md @@ -0,0 +1,14 @@ +## 1.10.0 (August 08, 2024) + +NOTES: + +* compare: The `compare` package is considered experimental and may be altered or removed in a subsequent release ([#330](https://github.com/hashicorp/terraform-plugin-testing/issues/330)) +* statecheck: `CompareValue`, `CompareValueCollection`, and `CompareValuePairs` state checks are considered experimental and may be altered or removed in a subsequent release. ([#330](https://github.com/hashicorp/terraform-plugin-testing/issues/330)) + +FEATURES: + +* compare: Introduced new `compare` package, which contains interfaces and implementations for value comparisons in state checks. ([#330](https://github.com/hashicorp/terraform-plugin-testing/issues/330)) +* statecheck: Added `CompareValue` state check, which compares sequential values of the specified attribute at the given managed resource, or data source, using the supplied value comparer. ([#330](https://github.com/hashicorp/terraform-plugin-testing/issues/330)) +* statecheck: Added `CompareValueCollection` state check, which compares each item in the specified collection (e.g., list, set) attribute, with the second specified attribute at the given managed resources, or data sources, using the supplied value comparer. ([#330](https://github.com/hashicorp/terraform-plugin-testing/issues/330)) +* statecheck: Added `CompareValuePairs` state check, which compares the specified attributes at the given managed resources, or data sources, using the supplied value comparer. ([#330](https://github.com/hashicorp/terraform-plugin-testing/issues/330)) + diff --git a/.changes/unreleased/FEATURES-20240717-155731.yaml b/.changes/unreleased/FEATURES-20240717-155731.yaml deleted file mode 100644 index 6f06b4eb9..000000000 --- a/.changes/unreleased/FEATURES-20240717-155731.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: FEATURES -body: 'compare: Introduced new `compare` package, which contains interfaces and implementations - for value comparisons in state checks.' -time: 2024-07-17T15:57:31.637692-04:00 -custom: - Issue: "330" diff --git a/.changes/unreleased/FEATURES-20240717-160116.yaml b/.changes/unreleased/FEATURES-20240717-160116.yaml deleted file mode 100644 index 6783377d1..000000000 --- a/.changes/unreleased/FEATURES-20240717-160116.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: FEATURES -body: 'statecheck: Added `CompareValue` state check, which compares sequential values of the - specified attribute at the given managed resource, or data source, using the supplied value comparer.' -time: 2024-07-17T16:01:16.194665-04:00 -custom: - Issue: "330" diff --git a/.changes/unreleased/FEATURES-20240717-160331.yaml b/.changes/unreleased/FEATURES-20240717-160331.yaml deleted file mode 100644 index 1187e32a3..000000000 --- a/.changes/unreleased/FEATURES-20240717-160331.yaml +++ /dev/null @@ -1,7 +0,0 @@ -kind: FEATURES -body: 'statecheck: Added `CompareValueCollection` state check, which compares each item in - the specified collection (e.g., list, set) attribute, with the second specified - attribute at the given managed resources, or data sources, using the supplied value comparer.' -time: 2024-07-17T16:03:31.77827-04:00 -custom: - Issue: "330" diff --git a/.changes/unreleased/FEATURES-20240717-164418.yaml b/.changes/unreleased/FEATURES-20240717-164418.yaml deleted file mode 100644 index 312311b1f..000000000 --- a/.changes/unreleased/FEATURES-20240717-164418.yaml +++ /dev/null @@ -1,7 +0,0 @@ -kind: FEATURES -body: 'statecheck: Added `CompareValuePairs` state check, which compares the - specified attributes at the given managed resources, or data sources, using - the supplied value comparer.' -time: 2024-07-17T16:44:18.612874-04:00 -custom: - Issue: "330" diff --git a/.changes/unreleased/NOTES-20240717-155810.yaml b/.changes/unreleased/NOTES-20240717-155810.yaml deleted file mode 100644 index a8edd41c4..000000000 --- a/.changes/unreleased/NOTES-20240717-155810.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: NOTES -body: 'compare: The `compare` package is considered experimental and may be altered - or removed in a subsequent release' -time: 2024-07-17T15:58:10.435384-04:00 -custom: - Issue: "330" diff --git a/.changes/unreleased/NOTES-20240717-164911.yaml b/.changes/unreleased/NOTES-20240717-164911.yaml deleted file mode 100644 index bc3af510b..000000000 --- a/.changes/unreleased/NOTES-20240717-164911.yaml +++ /dev/null @@ -1,7 +0,0 @@ -kind: NOTES -body: 'statecheck: `CompareValue`, `CompareValueCollection`, and `CompareValuePairs` - state checks are considered experimental and may be altered or removed in a subsequent - release.' -time: 2024-07-17T16:49:11.296585-04:00 -custom: - Issue: "330" diff --git a/CHANGELOG.md b/CHANGELOG.md index 97eeed1ec..a64850d7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +## 1.10.0 (August 08, 2024) + +NOTES: + +* compare: The `compare` package is considered experimental and may be altered or removed in a subsequent release ([#330](https://github.com/hashicorp/terraform-plugin-testing/issues/330)) +* statecheck: `CompareValue`, `CompareValueCollection`, and `CompareValuePairs` state checks are considered experimental and may be altered or removed in a subsequent release. ([#330](https://github.com/hashicorp/terraform-plugin-testing/issues/330)) + +FEATURES: + +* compare: Introduced new `compare` package, which contains interfaces and implementations for value comparisons in state checks. ([#330](https://github.com/hashicorp/terraform-plugin-testing/issues/330)) +* statecheck: Added `CompareValue` state check, which compares sequential values of the specified attribute at the given managed resource, or data source, using the supplied value comparer. ([#330](https://github.com/hashicorp/terraform-plugin-testing/issues/330)) +* statecheck: Added `CompareValueCollection` state check, which compares each item in the specified collection (e.g., list, set) attribute, with the second specified attribute at the given managed resources, or data sources, using the supplied value comparer. ([#330](https://github.com/hashicorp/terraform-plugin-testing/issues/330)) +* statecheck: Added `CompareValuePairs` state check, which compares the specified attributes at the given managed resources, or data sources, using the supplied value comparer. ([#330](https://github.com/hashicorp/terraform-plugin-testing/issues/330)) + ## 1.9.0 (July 09, 2024) ENHANCEMENTS: