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/1.11.0.md b/.changes/1.11.0.md new file mode 100644 index 000000000..67bed2b58 --- /dev/null +++ b/.changes/1.11.0.md @@ -0,0 +1,12 @@ +## 1.11.0 (November 19, 2024) + +NOTES: + +* all: This Go module has been updated to Go 1.22 per the [Go support policy](https://go.dev/doc/devel/release#policy). It is recommended to review the [Go 1.22 release notes](https://go.dev/doc/go1.22) before upgrading. Any consumers building on earlier Go versions may experience errors. ([#371](https://github.com/hashicorp/terraform-plugin-testing/issues/371)) +* echoprovider: The `echoprovider` package is considered experimental and may be altered or removed in a subsequent release ([#389](https://github.com/hashicorp/terraform-plugin-testing/issues/389)) + +FEATURES: + +* tfversion: Added `SkipIfNotAlpha` version check for testing experimental features of alpha Terraform builds. ([#388](https://github.com/hashicorp/terraform-plugin-testing/issues/388)) +* echoprovider: Introduced new `echoprovider` package, which contains a v6 Terraform provider that can be used to test ephemeral resource data. ([#389](https://github.com/hashicorp/terraform-plugin-testing/issues/389)) + diff --git a/.changes/1.12.0.md b/.changes/1.12.0.md new file mode 100644 index 000000000..a7032b292 --- /dev/null +++ b/.changes/1.12.0.md @@ -0,0 +1,19 @@ +## 1.12.0 (March 18, 2025) + +NOTES: + +* all: This Go module has been updated to Go 1.23 per the [Go support policy](https://go.dev/doc/devel/release#policy). It is recommended to review the [Go 1.23 release notes](https://go.dev/doc/go1.23) before upgrading. Any consumers building on earlier Go versions may experience errors. ([#454](https://github.com/hashicorp/terraform-plugin-testing/issues/454)) + +FEATURES: + +* knownvalue: added function checks for custom validation of resource attribute or output values. ([#412](https://github.com/hashicorp/terraform-plugin-testing/issues/412)) + +ENHANCEMENTS: + +* knownvalue: Updated the `ObjectExact` error message to report extra/missing attributes from the actual object. ([#451](https://github.com/hashicorp/terraform-plugin-testing/issues/451)) +* plancheck: Improved the unknown value plan check error messages to include a known value if one exists. ([#450](https://github.com/hashicorp/terraform-plugin-testing/issues/450)) + +BUG FIXES: + +* plancheck: Fixed bug with all unknown value plan checks where a valid path would return a "path not found" error. ([#450](https://github.com/hashicorp/terraform-plugin-testing/issues/450)) + diff --git a/.changes/1.13.0-alpha.1.md b/.changes/1.13.0-alpha.1.md new file mode 100644 index 000000000..52cbf0b06 --- /dev/null +++ b/.changes/1.13.0-alpha.1.md @@ -0,0 +1,6 @@ +## 1.13.0-alpha.1 (March 27, 2025) + +NOTES: + +* This alpha pre-release contains testing utilities for managed resource identity, which can be used with `Terraform v1.12.0-alpha20250319`, to assert identity data stored during apply workflows. A managed resource in a provider can read/store identity data using the `terraform-plugin-framework@v1.15.0-alpha.1` or `terraform-plugin-sdk/v2@v2.37.0-alpha.1` Go modules. To assert identity data stored by a provider in state, use the `statecheck.ExpectIdentity` state check. ([#470](https://github.com/hashicorp/terraform-plugin-testing/issues/470)) + diff --git a/.changes/1.13.0-beta.1.md b/.changes/1.13.0-beta.1.md new file mode 100644 index 000000000..29e617ea3 --- /dev/null +++ b/.changes/1.13.0-beta.1.md @@ -0,0 +1,15 @@ +## 1.13.0-beta.1 (April 18, 2025) + +BREAKING CHANGES: + +* importstate: `ImportStatePersist` and `ImportStateVerify` are not supported for plannable import (`ImportBlockWith*`) and will return an error ([#476](https://github.com/hashicorp/terraform-plugin-testing/issues/476)) +* importstate: renamed `ImportStateWithId` to `ImportStateWithID` and renamed `ImportCommandWithId` to `ImportCommandWithID`. ([#465](https://github.com/hashicorp/terraform-plugin-testing/issues/465)) + +NOTES: + +* This beta pre-release adds support for managed resource identity, which can be used with Terraform v1.12.0-beta2. Acceptance tests can use the `ImportBlockWithResourceIdentity` kind to exercise the import of a managed resource using its resource identity object values instead of using a string identifier. ([#480](https://github.com/hashicorp/terraform-plugin-testing/issues/480)) + +BUG FIXES: + +* importstate: plannable import (`ImportBlockWith*`) fixed for a resource with a dependency ([#476](https://github.com/hashicorp/terraform-plugin-testing/issues/476)) + diff --git a/.changes/1.13.0.md b/.changes/1.13.0.md new file mode 100644 index 000000000..b056b9e2b --- /dev/null +++ b/.changes/1.13.0.md @@ -0,0 +1,20 @@ +## 1.13.0 (May 16, 2025) + +NOTES: + +* reduced the volume of DEBUG-level logging to make it easier to visually scan debug output ([#463](https://github.com/hashicorp/terraform-plugin-testing/issues/463)) + +FEATURES: + +* ImportState: Added support for testing plannable import via Terraform configuration. Configuration is used from the previous test step if available. `Config`, `ConfigFile`, and `ConfigDirectory` can also be used directly with `ImportState` if needed. ([#442](https://github.com/hashicorp/terraform-plugin-testing/issues/442)) +* ImportState: Added `ImportStateKind` to control which method of import the `ImportState` test step uses. `ImportCommandWithID` (default, same behavior as today) , `ImportBlockWithID`, and `ImportBlockWithResourceIdentity`. ([#442](https://github.com/hashicorp/terraform-plugin-testing/issues/442)) +* ImportState: Added `ImportStateConfigExact` to opt-out of new import config generation for plannable import. ([#494](https://github.com/hashicorp/terraform-plugin-testing/issues/494)) +* statecheck: Added `ExpectIdentityValueMatchesState` state check to assert that an identity value matches a state value at the same path. ([#503](https://github.com/hashicorp/terraform-plugin-testing/issues/503)) +* statecheck: Added `ExpectIdentityValueMatchesStateAtPath` state check to assert that an identity value matches a state value at different paths. ([#503](https://github.com/hashicorp/terraform-plugin-testing/issues/503)) + +ENHANCEMENTS: + +* statecheck: Added `ExpectIdentityValue` state check, which asserts a specified attribute value of a managed resource identity in state. ([#468](https://github.com/hashicorp/terraform-plugin-testing/issues/468)) +* statecheck: Added `ExpectIdentity` state check, which asserts all data of a managed resource identity in state. ([#470](https://github.com/hashicorp/terraform-plugin-testing/issues/470)) +* Adds `AdditionalCLIOptions.PlanOptions.NoRefresh` to test `terraform plan -refresh=false` ([#490](https://github.com/hashicorp/terraform-plugin-testing/issues/490)) + diff --git a/.changes/1.13.1.md b/.changes/1.13.1.md new file mode 100644 index 000000000..78a0e6d9c --- /dev/null +++ b/.changes/1.13.1.md @@ -0,0 +1,6 @@ +## 1.13.1 (May 21, 2025) + +BUG FIXES: + +* echoprovider: Fixed bug where Terraform v1.12+ would return an error message indicating the provider doesn't support `GetResourceIdentitySchemas`. ([#512](https://github.com/hashicorp/terraform-plugin-testing/issues/512)) + diff --git a/.changes/1.13.2.md b/.changes/1.13.2.md new file mode 100644 index 000000000..1f6429e3b --- /dev/null +++ b/.changes/1.13.2.md @@ -0,0 +1,7 @@ +## 1.13.2 (June 11, 2025) + +BUG FIXES: + +* helper/resource: Updated `ImportBlockWith*` import state modes to use the `ExpectNonEmpty` field to allow non-empty import plans to pass successfully. ([#518](https://github.com/hashicorp/terraform-plugin-testing/issues/518)) +* helper/resource: Fixed bug with import state mode where prior test config is not used for `ConfigFile` or `ConfigDirectory` ([#516](https://github.com/hashicorp/terraform-plugin-testing/issues/516)) + diff --git a/.changes/1.14.0-beta.1.md b/.changes/1.14.0-beta.1.md new file mode 100644 index 000000000..df7c04c0b --- /dev/null +++ b/.changes/1.14.0-beta.1.md @@ -0,0 +1,8 @@ +## 1.14.0-beta.1 (September 18, 2025) + +NOTES: + +* Adds an opt-in compatibility flag for config mode tests to unlock upgrade from v1.5.1 to latest for specific providers. ([#496](https://github.com/hashicorp/terraform-plugin-testing/issues/496)) +* This beta pre-release adds a new query mode to support testing for list blocks which can be used with Terraform v1.14+ ([#531](https://github.com/hashicorp/terraform-plugin-testing/issues/531)) +* all: This Go module has been updated to Go 1.24 per the Go support policy. It is recommended to review the Go 1.24 release notes before upgrading. ([#551](https://github.com/hashicorp/terraform-plugin-testing/issues/551)) + diff --git a/.changes/1.4.0.md b/.changes/1.4.0.md new file mode 100644 index 000000000..2e8cb5815 --- /dev/null +++ b/.changes/1.4.0.md @@ -0,0 +1,8 @@ +## 1.4.0 (July 24, 2023) + +FEATURES: + +* tfjsonpath: Introduced new `tfjsonpath` package which contains methods that allow traversal of Terraform JSON data ([#154](https://github.com/hashicorp/terraform-plugin-testing/issues/154)) +* plancheck: Added `ExpectUnknownValue` built-in plan check, which asserts that a given attribute has an unknown value ([#154](https://github.com/hashicorp/terraform-plugin-testing/issues/154)) +* plancheck: Added `ExpectSensitiveValue` built-in plan check, which asserts that a given attribute has a sensitive value ([#154](https://github.com/hashicorp/terraform-plugin-testing/issues/154)) + diff --git a/.changes/1.5.0.md b/.changes/1.5.0.md new file mode 100644 index 000000000..421cb2867 --- /dev/null +++ b/.changes/1.5.0.md @@ -0,0 +1,14 @@ +## 1.5.0 (August 31, 2023) + +FEATURES: + +* config: Introduced new `config` package which contains interfaces and helper functions for working with native Terraform configuration and variables ([#153](https://github.com/hashicorp/terraform-plugin-testing/issues/153)) +* helper/resource: Added `TestStep.ConfigDirectory` to allow specifying a directory containing Terraform configuration for use during acceptance tests ([#153](https://github.com/hashicorp/terraform-plugin-testing/issues/153)) +* helper/resource: Added `TestStep.ConfigFile` to allow specifying a file containing Terraform configuration for use during acceptance tests ([#153](https://github.com/hashicorp/terraform-plugin-testing/issues/153)) +* helper/resource: Added `TestStep.ConfigVariables` to allow specifying Terraform variables for use with Terraform configuration during acceptance tests ([#153](https://github.com/hashicorp/terraform-plugin-testing/issues/153)) +* helper/resource: Removed data resource and managed resource `id` attribute requirement ([#84](https://github.com/hashicorp/terraform-plugin-testing/issues/84)) + +ENHANCEMENTS: + +* helper/resource: Added `TestStep` type `ImportStateVerifyIdentifierAttribute` field, which can override the default `id` attribute used for matching prior resource state with imported resource state ([#84](https://github.com/hashicorp/terraform-plugin-testing/issues/84)) + diff --git a/.changes/1.5.1.md b/.changes/1.5.1.md new file mode 100644 index 000000000..0407854d4 --- /dev/null +++ b/.changes/1.5.1.md @@ -0,0 +1,6 @@ +## 1.5.1 (August 31, 2023) + +BUG FIXES: + +* helper/resource: Fix regression by allowing providers to be defined both at the `TestCase` level, and within `TestStep.Config` ([#177](https://github.com/hashicorp/terraform-plugin-testing/issues/177)) + diff --git a/.changes/1.6.0.md b/.changes/1.6.0.md new file mode 100644 index 000000000..55c1838b4 --- /dev/null +++ b/.changes/1.6.0.md @@ -0,0 +1,25 @@ +## 1.6.0 (December 04, 2023) + +NOTES: + +* all: This Go module has been updated to Go 1.20 per the [Go support policy](https://go.dev/doc/devel/release#policy). It is recommended to review the [Go 1.20 release notes](https://go.dev/doc/go1.20) before upgrading. Any consumers building on earlier Go versions may experience errors. ([#180](https://github.com/hashicorp/terraform-plugin-testing/issues/180)) +* helper/resource: Configuration based `TestStep` now include post-apply plan checks for output changes in addition to resource changes. If this causes unexpected new test failures, most `output` configuration blocks can be likely be removed. Test steps involving resources and data sources should never need to use `output` configuration blocks as plan and state checks support working on resource and data source attributes values directly. ([#234](https://github.com/hashicorp/terraform-plugin-testing/issues/234)) +* helper/resource: Implicit `terraform refresh` commands during each `TestStep` have been removed to fix plan check and performance issues, which can cause new test failures when testing schema changes (e.g. state upgrades) that have a final `TestStep` with `PlanOnly: true`. Remove `PlanOnly: true` from the final `TestStep` to fix affected tests which will ensure that updated schema changes are applied to the state before attempting to automatically destroy resources. ([#223](https://github.com/hashicorp/terraform-plugin-testing/issues/223)) + +FEATURES: + +* plancheck: Added `ExpectUnknownOutputValue` built-in plan check, which asserts that a given output value at a specified address is unknown ([#220](https://github.com/hashicorp/terraform-plugin-testing/issues/220)) +* plancheck: Added `ExpectUnknownOutputValueAtPath` built-in plan check, which asserts that a given output value at a specified address, and path is unknown ([#220](https://github.com/hashicorp/terraform-plugin-testing/issues/220)) +* plancheck: Added `ExpectNullOutputValue` built-in plan check, which asserts that a given output value at a specified address is null ([#220](https://github.com/hashicorp/terraform-plugin-testing/issues/220)) +* plancheck: Added `ExpectNullOutputValueAtPath` built-in plan check, which asserts that a given output value at a specified address, and path is null ([#220](https://github.com/hashicorp/terraform-plugin-testing/issues/220)) + +ENHANCEMENTS: + +* helper/resource: Removed separate refresh commands, which increases testing performance ([#223](https://github.com/hashicorp/terraform-plugin-testing/issues/223)) +* helper/resource: Automatically add `required_providers` configuration to `TestStep.Config` Terraform language configuration when using Terraform >= 1.0.* ([#216](https://github.com/hashicorp/terraform-plugin-testing/issues/216)) + +BUG FIXES: + +* plancheck: Ensured `ExpectEmptyPlan` and `ExpectNonEmptyPlan` account for output changes ([#222](https://github.com/hashicorp/terraform-plugin-testing/issues/222)) +* helper/resource: Ensured `TestStep.ExpectNonEmptyPlan` accounts for output changes with Terraform 0.14 and later ([#234](https://github.com/hashicorp/terraform-plugin-testing/issues/234)) + diff --git a/.changes/1.7.0.md b/.changes/1.7.0.md new file mode 100644 index 000000000..7695ad966 --- /dev/null +++ b/.changes/1.7.0.md @@ -0,0 +1,31 @@ +## 1.7.0 (March 05, 2024) + +NOTES: + +* helper/resource: Error messages generated by the testing logic, which were updated for clarity in this release, are not protected by compatibility promises. While testing logic errors are usable in certain scenarios with `ErrorCheck` and `ExpectError` functionality, error messaging checks should be based on provider-controlled messaging or when appropriate to use other testing features such as `ExpectNonEmptyPlan` instead. ([#238](https://github.com/hashicorp/terraform-plugin-testing/issues/238)) +* Numerical values in the plan are now represented as json.Number, not float64. Custom plan checks relying upon float64 representation may need altering ([#248](https://github.com/hashicorp/terraform-plugin-testing/issues/248)) +* plancheck: Deprecated `ExpectNullOutputValue` and `ExpectNullOutputValueAtPath`. Use `ExpectKnownOutputValue` and `ExpectKnownOutputValueAtPath` with `knownvalue.Null` instead ([#275](https://github.com/hashicorp/terraform-plugin-testing/issues/275)) +* plancheck: `ExpectKnownValue`, `ExpectKnownOutputValue` and `ExpectKnownOutputValueAtPath` plan checks are considered experimental and may be altered or removed in a subsequent release ([#276](https://github.com/hashicorp/terraform-plugin-testing/issues/276)) +* statecheck: `ExpectKnownValue`, `ExpectKnownOutputValue` and `ExpectKnownOutputValueAtPath` state checks are considered experimental and may be altered or removed in a subsequent release ([#276](https://github.com/hashicorp/terraform-plugin-testing/issues/276)) +* knownvalue: The `knownvalue` package is considered experimental and may be altered or removed in a subsequent release ([#276](https://github.com/hashicorp/terraform-plugin-testing/issues/276)) +* all: This Go module has been updated to Go 1.21 per the [Go support policy](https://go.dev/doc/devel/release#policy). It is recommended to review the [Go 1.21 release notes](https://go.dev/doc/go1.21) before upgrading. Any consumers building on earlier Go versions may experience errors ([#300](https://github.com/hashicorp/terraform-plugin-testing/issues/300)) + +FEATURES: + +* plancheck: Added `ExpectKnownValue` plan check, which asserts that a given resource attribute has a defined type, and value ([#248](https://github.com/hashicorp/terraform-plugin-testing/issues/248)) +* plancheck: Added `ExpectKnownOutputValue` plan check, which asserts that a given output value has a defined type, and value ([#248](https://github.com/hashicorp/terraform-plugin-testing/issues/248)) +* plancheck: Added `ExpectKnownOutputValueAtPath` plan check, which asserts that a given output value at a specified path has a defined type, and value ([#248](https://github.com/hashicorp/terraform-plugin-testing/issues/248)) +* knownvalue: Introduced new `knownvalue` package which contains types for working with plan checks and state checks ([#248](https://github.com/hashicorp/terraform-plugin-testing/issues/248)) +* statecheck: Introduced new `statecheck` package with interface and built-in state check functionality ([#275](https://github.com/hashicorp/terraform-plugin-testing/issues/275)) +* statecheck: Added `ExpectKnownValue` state check, which asserts that a given resource attribute has a defined type, and value ([#275](https://github.com/hashicorp/terraform-plugin-testing/issues/275)) +* statecheck: Added `ExpectKnownOutputValue` state check, which asserts that a given output value has a defined type, and value ([#275](https://github.com/hashicorp/terraform-plugin-testing/issues/275)) +* statecheck: Added `ExpectKnownOutputValueAtPath` plan check, which asserts that a given output value at a specified path has a defined type, and value ([#275](https://github.com/hashicorp/terraform-plugin-testing/issues/275)) +* statecheck: Added `ExpectSensitiveValue` built-in state check, which asserts that a given attribute has a sensitive value ([#275](https://github.com/hashicorp/terraform-plugin-testing/issues/275)) + +BUG FIXES: + +* helper/resource: Clarified error messaging from testing failures, especially when using `TestStep.PlanOnly: true` ([#238](https://github.com/hashicorp/terraform-plugin-testing/issues/238)) +* helper/resource: Fix detection of provider block declaration in `Config`, `ConfigDirectory`, and `ConfigFile` ([#265](https://github.com/hashicorp/terraform-plugin-testing/issues/265)) +* helper/resource: Fix detection of terraform block declaration in `Config`, `ConfigDirectory`, and `ConfigFile` ([#265](https://github.com/hashicorp/terraform-plugin-testing/issues/265)) +* helper/resource: Fixed internal deferred test helpers to properly report file and line information in test failures. ([#292](https://github.com/hashicorp/terraform-plugin-testing/issues/292)) + diff --git a/.changes/1.8.0.md b/.changes/1.8.0.md new file mode 100644 index 000000000..10e813247 --- /dev/null +++ b/.changes/1.8.0.md @@ -0,0 +1,19 @@ +## 1.8.0 (May 17, 2024) + +FEATURES: + +* plancheck: Added `ExpectDeferredChange` and `ExpectNoDeferredChanges` checks for experimental deferred action support. ([#331](https://github.com/hashicorp/terraform-plugin-testing/issues/331)) +* tfversion: Added `SkipIfNotPrerelease` version check for testing experimental features of prerelease Terraform builds. ([#331](https://github.com/hashicorp/terraform-plugin-testing/issues/331)) + +ENHANCEMENTS: + +* helper/acctest: Improve scope of IPv4/IPv6 random address generation in RandIpAddress() ([#305](https://github.com/hashicorp/terraform-plugin-testing/issues/305)) +* knownvalue: Add `TupleExact`, `TuplePartial` and `TupleSizeExact` checks for dynamic value testing. ([#312](https://github.com/hashicorp/terraform-plugin-testing/issues/312)) +* tfversion: Ensured Terraform CLI prerelease versions are considered semantically equal to patch versions in built-in checks to match the Terraform CLI versioning policy ([#303](https://github.com/hashicorp/terraform-plugin-testing/issues/303)) +* helper/resource: Added `(TestCase).AdditionalCLIOptions` with `AllowDeferral` option for plan and apply commands. ([#331](https://github.com/hashicorp/terraform-plugin-testing/issues/331)) + +BUG FIXES: + +* helper/resource: Fix panic in output state shimming when a tuple is present. ([#310](https://github.com/hashicorp/terraform-plugin-testing/issues/310)) +* tfversion: Fixed `RequireBelow` ignoring equal versioning to fail a test ([#303](https://github.com/hashicorp/terraform-plugin-testing/issues/303)) + diff --git a/.changes/1.9.0.md b/.changes/1.9.0.md new file mode 100644 index 000000000..dcbca5418 --- /dev/null +++ b/.changes/1.9.0.md @@ -0,0 +1,7 @@ +## 1.9.0 (July 09, 2024) + +ENHANCEMENTS: + +* knownvalue: Add `Int32Exact` check for int32 value testing. ([#356](https://github.com/hashicorp/terraform-plugin-testing/issues/356)) +* knownvalue: Add `Float32Exact` check for float32 value testing. ([#356](https://github.com/hashicorp/terraform-plugin-testing/issues/356)) + diff --git a/.changie.yaml b/.changie.yaml index 00b125957..1beba3233 100644 --- a/.changie.yaml +++ b/.changie.yaml @@ -1,6 +1,5 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: MPL-2.0 - +# DO NOT EDIT - This GitHub Workflow is managed by automation +# https://github.com/hashicorp/terraform-devex-repos changesDir: .changes unreleasedDir: unreleased changelogPath: CHANGELOG.md diff --git a/.copywrite.hcl b/.copywrite.hcl index 301109050..b9f35eddf 100644 --- a/.copywrite.hcl +++ b/.copywrite.hcl @@ -5,8 +5,11 @@ project { copyright_year = 2014 header_ignore = [ + # internal catalog metadata (prose) + "META.d/**/*.yaml", + # changie tooling configuration and CHANGELOG entries (prose) - ".changes/unreleased/*.yaml", + ".changes/unreleased/**", ".changie.yaml", # GitHub issue template configuration diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 922ee27f4..ed6d5d312 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1,17 @@ -* @hashicorp/terraform-devex +* @hashicorp/terraform-core-plugins + +# engineering and web presence get notified of, and can approve changes to web tooling, but not content. + +/website/ @hashicorp/web-presence @hashicorp/terraform-core-plugins +/website/data/ +/website/public/ +/website/content/ + +# education and engineering get notified of, and can approve changes to web content. + +/website/data/ @hashicorp/team-docs-packer-and-terraform @hashicorp/terraform-core-plugins +/website/public/ @hashicorp/team-docs-packer-and-terraform @hashicorp/terraform-core-plugins +/website/content/ @hashicorp/team-docs-packer-and-terraform @hashicorp/terraform-core-plugins +/website/docs/ @hashicorp/team-docs-packer-and-terraform @hashicorp/terraform-core-plugins +/website/img/ @hashicorp/team-docs-packer-and-terraform @hashicorp/terraform-core-plugins +/website/README.md @hashicorp/team-docs-packer-and-terraform @hashicorp/terraform-core-plugins \ No newline at end of file diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 700329227..66573fb25 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -18,11 +18,24 @@ while we're on v0, breaking changes will be accepted during minor releases. - [I just have a question](#i-just-have-a-question) - [I want to report a vulnerability](#i-want-to-report-a-vulnerability) - [New Issue](#new-issue) + * [Bug Reports](#bug-reports) + * [Feature Requests](#feature-requests) + * [Documentation Contributions](#documentation-contributions) - [New Pull Request](#new-pull-request) + * [Cosmetic changes, code formatting, and typos](#cosmetic-changes-code-formatting-and-typos) + + [Exceptions](#exceptions) + * [License Headers](#license-headers) + - [Linting](#linting) + - [Testing](#testing) + * [GitHub Actions Tests](#github-actions-tests) + * [Go Unit Tests](#go-unit-tests) + - [Maintainers Guide](#maintainers-guide) + * [Releases](#releases) ## I just have a question -> **Note:** We use GitHub for tracking bugs and feature requests related to +> [!Note] +> We use GitHub for tracking bugs and feature requests related to > terraform-plugin-testing. For questions, please see relevant channels at @@ -31,7 +44,7 @@ https://www.terraform.io/community.html ## I want to report a vulnerability Please disclose security vulnerabilities responsibly by following the procedure -described at https://www.hashicorp.com/security#vulnerability-reporting +described at https://www.hashicorp.com/en/trust/security/vulnerability-management ## New Issue diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 036285bae..564a6bbf2 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,15 +3,27 @@ version: 2 updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "daily" - package-ecosystem: "gomod" directory: "/" schedule: interval: "daily" + groups: + # Group all terraform-plugin-(go|sdk|framework|testing) dependencies together + "terraform-plugin": + patterns: + - "github.com/hashicorp/terraform-plugin-*" - package-ecosystem: "gomod" directory: "/tools" schedule: interval: "daily" + - package-ecosystem: "github-actions" + directory: "/" + groups: + "github-actions": + patterns: + - "*" # Group all GitHub Actions dependencies together + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + timezone: "Etc/UTC" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..38e8ce712 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,16 @@ +## Related Issue + +Fixes # + +## Description + +In plain English, describe your approach to addressing the issue linked above. For example, if you made a particular design decision, let us know why you chose this path instead of another solution. + + +## Rollback Plan + +- [ ] If a change needs to be reverted, we will roll out an update to the code within 7 days. + +## Changes to Security Controls + +Are there any changes to security controls (access controls, encryption, logging) in this pull request? If so, explain. diff --git a/.github/workflows/add-content-to-project.yml b/.github/workflows/add-content-to-project.yml deleted file mode 100644 index d16fe5e99..000000000 --- a/.github/workflows/add-content-to-project.yml +++ /dev/null @@ -1,40 +0,0 @@ -# Based on https://github.com/leonsteinhaeuser/project-beta-automations - -name: "Add Issues/PRs to TF Provider DevEx team board" - -on: - issues: - types: [opened, reopened] - pull_request_target: - # NOTE: The way content is added to project board is equivalent to an "upsert". - # Calling it multiple times will be idempotent. - # - # See https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ - # to see the reasoning behind using `pull_request_target` instead of `pull_request` - types: [opened, reopened, ready_for_review] - -jobs: - add-content-to-project: - name: "Add Content to project" - runs-on: ubuntu-latest - steps: - - name: "Set Issue to 'Priority = Triage Next'" - uses: leonsteinhaeuser/project-beta-automations@d1c1261558118c0876fdb2b57a649303925e5a70 # v2.1.0 - if: github.event_name == 'issues' - with: - gh_token: ${{ secrets.TF_DEVEX_PROJECT_GITHUB_TOKEN }} - organization: "hashicorp" - project_id: 99 #< https://github.com/orgs/hashicorp/projects/99 - resource_node_id: ${{ github.event.issue.node_id }} - operation_mode: custom_field - custom_field_values: '[{\"name\":\"Priority\",\"type\":\"single_select\",\"value\":\"Triage Next\"}]' - - name: "Set Pull Request to 'Priority = Triage Next'" - uses: leonsteinhaeuser/project-beta-automations@d1c1261558118c0876fdb2b57a649303925e5a70 # v2.1.0 - if: github.event_name == 'pull_request_target' - with: - gh_token: ${{ secrets.TF_DEVEX_PROJECT_GITHUB_TOKEN }} - organization: "hashicorp" - project_id: 99 #< https://github.com/orgs/hashicorp/projects/99 - resource_node_id: ${{ github.event.pull_request.node_id }} - operation_mode: custom_field - custom_field_values: '[{\"name\":\"Priority\",\"type\":\"single_select\",\"value\":\"Triage Next\"}]' diff --git a/.github/workflows/ci-changie.yml b/.github/workflows/ci-changie.yml new file mode 100644 index 000000000..f4f04dc10 --- /dev/null +++ b/.github/workflows/ci-changie.yml @@ -0,0 +1,27 @@ +# DO NOT EDIT - This GitHub Workflow is managed by automation +# https://github.com/hashicorp/terraform-devex-repos + +# Continuous integration handling for changie +name: ci-changie + +on: + pull_request: + paths: + - .changes/unreleased/*.yaml + - .changie.yaml + - .github/workflows/ci-changie.yml + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + steps: + # Ensure terraform-devex-repos is updated on version changes. + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + # Ensure terraform-devex-repos is updated on version changes. + - uses: miniscruff/changie-action@5036dffa79ffc007110dc7f75eca7ef72780e147 # v2.1.0 + with: + version: latest + args: batch patch --dry-run diff --git a/.github/workflows/ci-github-actions.yml b/.github/workflows/ci-github-actions.yml index 1187a9370..cfa5ca7bf 100644 --- a/.github/workflows/ci-github-actions.yml +++ b/.github/workflows/ci-github-actions.yml @@ -13,8 +13,8 @@ jobs: actionlint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 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 3edd55115..b645d72d1 100644 --- a/.github/workflows/ci-go.yml +++ b/.github/workflows/ci-go.yml @@ -16,27 +16,40 @@ jobs: golangci-lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version-file: 'go.mod' - run: go mod download - - uses: golangci/golangci-lint-action@639cd343e1d3b897ff35927a75193d57cfcba299 # v3.6.0 + - uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 test: - name: test (Go v${{ matrix.go-version }}) + name: test (Go ${{ matrix.go-version }} / TF ${{ matrix.terraform }}) runs-on: ubuntu-latest strategy: matrix: - go-version: [ '1.20', '1.19' ] + go-version: [ '1.25', '1.24' ] + terraform: ${{ fromJSON(vars.TF_VERSIONS_PROTOCOL_V5) }} steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version: ${{ matrix.go-version }} + - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 + with: + terraform_version: ${{ matrix.terraform }} + terraform_wrapper: false - run: go mod download - - run: go test -coverprofile=coverage.out ./... + - run: go test -v -coverprofile=coverage.out ./... + env: + TF_ACC: "1" + - name: Remove wildcard suffix from TF version + id: tf_version + run: | + orginal_version="${{ matrix.terraform }}" + wildcard=".*" + echo "version=${orginal_version%"$wildcard"}" >> "$GITHUB_OUTPUT" - run: go tool cover -html=coverage.out -o coverage.html - - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: - name: go-${{ matrix.go-version }}-coverage + name: go-${{ matrix.go-version }}-terraform-${{ steps.tf_version.outputs.version }}-coverage path: coverage.html diff --git a/.github/workflows/ci-goreleaser.yml b/.github/workflows/ci-goreleaser.yml index fc75cd5bd..1a15b7125 100644 --- a/.github/workflows/ci-goreleaser.yml +++ b/.github/workflows/ci-goreleaser.yml @@ -14,10 +14,10 @@ jobs: check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version-file: 'go.mod' - - uses: goreleaser/goreleaser-action@336e29918d653399e599bfca99fadc1d7ffbc9f7 # v4.3.0 + - uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 with: args: check diff --git a/.github/workflows/compliance.yml b/.github/workflows/compliance.yml index 5350408de..c53644eb1 100644 --- a/.github/workflows/compliance.yml +++ b/.github/workflows/compliance.yml @@ -11,7 +11,7 @@ jobs: copywrite: runs-on: ubuntu-latest steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - uses: hashicorp/setup-copywrite@867a1a2a064a0626db322392806428f7dc59cb3e # v1.1.2 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: hashicorp/setup-copywrite@32638da2d4e81d56a0764aa1547882fc4d209636 # v1.1.3 - run: copywrite headers --plan - run: copywrite license --plan diff --git a/.github/workflows/issue-comment-created.yml b/.github/workflows/issue-comment-created.yml deleted file mode 100644 index 0c4f898c4..000000000 --- a/.github/workflows/issue-comment-created.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: Issue Comment Created Triage - -on: - issue_comment: - types: [created] - -jobs: - issue_comment_triage: - runs-on: ubuntu-latest - steps: - - uses: actions-ecosystem/action-remove-labels@2ce5d41b4b6aa8503e285553f75ed56e0a40bae0 # v1.3.0 - with: - labels: | - stale - waiting-response diff --git a/.github/workflows/issue-comment-triage.yml b/.github/workflows/issue-comment-triage.yml new file mode 100644 index 000000000..00017cdfb --- /dev/null +++ b/.github/workflows/issue-comment-triage.yml @@ -0,0 +1,21 @@ +# DO NOT EDIT - This GitHub Workflow is managed by automation +# https://github.com/hashicorp/terraform-devex-repos +name: Issue Comment Triage + +on: + issue_comment: + types: [created] + +jobs: + issue_comment_triage: + runs-on: ubuntu-latest + env: + # issue_comment events are triggered by comments on issues and pull requests. Checking the + # value of github.event.issue.pull_request tells us whether the issue is an issue or is + # actually a pull request, allowing us to dynamically set the gh subcommand: + # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issue_comment-on-issues-only-or-pull-requests-only + COMMAND: ${{ github.event.issue.pull_request && 'pr' || 'issue' }} + GH_TOKEN: ${{ github.token }} + steps: + - name: 'Remove waiting-response on comment' + run: gh ${{ env.COMMAND }} edit ${{ github.event.issue.html_url }} --remove-label waiting-response diff --git a/.github/workflows/jira-sync.yml b/.github/workflows/jira-sync.yml deleted file mode 100644 index 737d56a5d..000000000 --- a/.github/workflows/jira-sync.yml +++ /dev/null @@ -1,39 +0,0 @@ -on: - issues: - types: [closed, deleted, reopened] - pull_request_target: - types: [closed, reopened] - -name: Jira Sync - -jobs: - sync: - runs-on: ubuntu-latest - name: Jira sync - steps: - - - name: Login - uses: atlassian/gajira-login@45fd029b9f1d6d8926c6f04175aa80c0e42c9026 # v3.0.1 - if: contains(github.event.pull_request.labels.*.name, 'tf-devex-triage') || contains(github.event.issue.labels.*.name, 'tf-devex-triage') - env: - JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} - JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} - JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} - - name: Search for existing issue - id: search - if: contains(github.event.pull_request.labels.*.name, 'tf-devex-triage') || contains(github.event.issue.labels.*.name, 'tf-devex-triage') - uses: tomhjp/gh-action-jira-search@04700b457f317c3e341ce90da5a3ff4ce058f2fa # v0.2.2 - with: - jql: 'project="TFECO" and "Team (R&D)[Labels]"="TF-DevEx" and description ~ "${{ github.event.issue.html_url || github.event.pull_request.html_url }}" and labels in (Github)' - - name: Close task - if: ( github.event.action == 'closed' || github.event.action == 'deleted' ) && steps.search.outputs.issue - uses: atlassian/gajira-transition@38fc9cd61b03d6a53dd35fcccda172fe04b36de3 # v3.0.1 - with: - issue: ${{ steps.search.outputs.issue }} - transition: "Closed" - - name: Reopen task - if: github.event.action == 'reopened' && steps.search.outputs.issue - uses: atlassian/gajira-transition@38fc9cd61b03d6a53dd35fcccda172fe04b36de3 # v3.0.1 - with: - issue: ${{ steps.search.outputs.issue }} - transition: "To Do" diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 840b0af79..8917c75d6 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -1,23 +1,22 @@ +# DO NOT EDIT - This GitHub Workflow is managed by automation +# https://github.com/hashicorp/terraform-devex-repos name: 'Lock Threads' on: schedule: - - cron: '50 1 * * *' + - cron: '29 22 * * *' jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@be8aa5be94131386884a6da4189effda9b14aa21 # v4.0.1 + # NOTE: When TSCCR updates the GitHub action version, update the template workflow file to avoid drift: + # https://github.com/hashicorp/terraform-devex-repos/blob/main/modules/repo/workflows/lock.tftpl + - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1 with: + process-only: 'issues, prs' github-token: ${{ github.token }} - issue-comment: > - I'm going to lock this issue because it has been closed for _30 days_ ⏳. This helps our maintainers find and focus on the active issues. - - If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further. issue-inactive-days: '30' - pr-comment: > - I'm going to lock this pull request because it has been closed for _30 days_ ⏳. This helps our maintainers find and focus on the active contributions. - - If you have found a problem that seems related to this change, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further. + issue-lock-reason: resolved pr-inactive-days: '30' + pr-lock-reason: resolved diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d6890d7b6..59d3009a6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,10 +11,6 @@ on: permissions: contents: read # Changelog commit operations use service account PAT -env: - CI_COMMIT_AUTHOR: hc-github-team-tf-provider-devex - CI_COMMIT_EMAIL: github-team-tf-provider-devex@hashicorp.com - jobs: changelog-version: runs-on: ubuntu-latest @@ -29,36 +25,36 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 # Avoid persisting GITHUB_TOKEN credentials as they take priority over our service account PAT for `git push` operations # More details: https://github.com/actions/checkout/blob/b4626ce19ce1106186ddf9bb20e706842f11a7c3/adrs/0153-checkout-v2.md#persist-credentials persist-credentials: false - name: Batch changes - uses: miniscruff/changie-action@b6d52c80deb236a5b548f8774cd5a18b87da9e9a # v1.0.1 + uses: miniscruff/changie-action@5036dffa79ffc007110dc7f75eca7ef72780e147 # v2.1.0 with: version: latest args: batch ${{ needs.changelog-version.outputs.version }} - name: Merge changes - uses: miniscruff/changie-action@b6d52c80deb236a5b548f8774cd5a18b87da9e9a # v1.0.1 + uses: miniscruff/changie-action@5036dffa79ffc007110dc7f75eca7ef72780e147 # v2.1.0 with: version: latest args: merge - name: Git push changelog run: | - git config --global user.name "${{ env.CI_COMMIT_AUTHOR }}" - git config --global user.email "${{ env.CI_COMMIT_EMAIL }}" + git config --global user.name "${{ vars.TF_DEVEX_CI_COMMIT_AUTHOR }}" + git config --global user.email "${{ vars.TF_DEVEX_CI_COMMIT_EMAIL }}" git add . git commit -a -m "Update changelog" - git push "https://${{ env.CI_COMMIT_AUTHOR }}:${{ secrets.TF_DEVEX_COMMIT_GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" + git push "https://${{ vars.TF_DEVEX_CI_COMMIT_AUTHOR }}:${{ secrets.TF_DEVEX_COMMIT_GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" release-tag: needs: changelog runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 # Default input is the SHA that initially triggered the workflow. As we created a new commit in the previous job, @@ -70,11 +66,11 @@ jobs: - name: Git push release tag run: | - git config --global user.name "${{ env.CI_COMMIT_AUTHOR }}" - git config --global user.email "${{ env.CI_COMMIT_EMAIL }}" + git config --global user.name "${{ vars.TF_DEVEX_CI_COMMIT_AUTHOR }}" + git config --global user.email "${{ vars.TF_DEVEX_CI_COMMIT_EMAIL }}" git tag "${{ inputs.versionNumber }}" - git push "https://${{ env.CI_COMMIT_AUTHOR }}:${{ secrets.TF_DEVEX_COMMIT_GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" "${{ inputs.versionNumber }}" + git push "https://${{ vars.TF_DEVEX_CI_COMMIT_AUTHOR }}:${{ secrets.TF_DEVEX_COMMIT_GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" "${{ inputs.versionNumber }}" goreleaser: needs: [ changelog-version, changelog, release-tag ] @@ -83,12 +79,12 @@ jobs: contents: write # Needed for goreleaser to create GitHub release issues: write # Needed for goreleaser to close associated milestone steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: ref: ${{ inputs.versionNumber }} fetch-depth: 0 - - uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1 + - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version-file: 'go.mod' @@ -97,7 +93,7 @@ jobs: cd .changes sed -e "1{/# /d;}" -e "2{/^$/d;}" ${{ needs.changelog-version.outputs.version }}.md > /tmp/release-notes.txt - - uses: goreleaser/goreleaser-action@336e29918d653399e599bfca99fadc1d7ffbc9f7 # v4.3.0 + - uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/send-to-jira.yml b/.github/workflows/send-to-jira.yml deleted file mode 100644 index 909935e12..000000000 --- a/.github/workflows/send-to-jira.yml +++ /dev/null @@ -1,37 +0,0 @@ -on: - issues: - types: [labeled] - -name: Jira Sync - -jobs: - sync: - runs-on: ubuntu-latest - name: Jira sync - steps: - - - name: Login - uses: atlassian/gajira-login@45fd029b9f1d6d8926c6f04175aa80c0e42c9026 # v3.0.1 - if: github.event.label.name == 'tf-devex-triage' - env: - JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} - JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} - JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} - - name: Search for existing issue - id: search - if: github.event.label.name == 'tf-devex-triage' - uses: tomhjp/gh-action-jira-search@04700b457f317c3e341ce90da5a3ff4ce058f2fa # v0.2.2 - with: - jql: 'project="TFECO" and "Team (R&D)[Labels]"="TF-DevEx" and description ~ "${{ github.event.issue.html_url || github.event.pull_request.html_url }}" and labels in (Github)' - - - name: Create task in DevEx board - if: github.event.label.name == 'tf-devex-triage' && !steps.search.outputs.issue - uses: atlassian/gajira-create@59e177c4f6451399df5b4911c2211104f171e669 #v3.0.1 - with: - project: TFECO - issuetype: "Task" - summary: "[GH] ${{ github.event.issue.title || github.event.pull_request.title }}" - description: "${{ github.event.issue.html_url || github.event.pull_request.html_url }} \n Synced by Github Actions, tagged by ${{ github.actor }}" - # customfield_10091 is Team (R&D) - fields: '{"customfield_10091": ["TF-DevEx"], "labels": ["Github"]}' - diff --git a/.golangci.yml b/.golangci.yml index bd09eed57..7cde8b21e 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,16 +1,12 @@ -issues: - max-per-linter: 0 - max-same-issues: 0 - +version: "2" linters: - disable-all: true + default: none enable: + - copyloopvar - durationcheck - errcheck - - exportloopref - forcetypeassert - - gofmt - - gosimple + - govet - ineffassign - makezero - misspell @@ -18,12 +14,41 @@ linters: - paralleltest - predeclared - staticcheck - - tenv - unconvert - unparam - unused - - vet - -run: - # Prevent false positive timeouts in CI - timeout: 5m \ No newline at end of file + - usetesting + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - third_party$ + - builtin$ + - examples$ + settings: + staticcheck: + checks: + - all + - '-QF1001' # "could apply De Morgan's law" -- https://staticcheck.dev/docs/checks/#QF1001 + - '-QF1002' # "could use tagged switch" -- https://staticcheck.dev/docs/checks/#QF1002 + - '-QF1004' # "could use strings.ReplaceAll instead" -- https://staticcheck.dev/docs/checks/#QF1004 + - '-QF1008' # "could remove embedded field "Block" from selector" -- https://staticcheck.dev/docs/checks/#QF1008 + - '-ST1003' # example: "const autoTFVarsJson should be autoTFVarsJSON" -- https://staticcheck.dev/docs/checks/#ST1003 + - '-ST1005' # "error strings should not end with punctuation or newlines" -- https://staticcheck.dev/docs/checks/#ST1005 + - '-ST1016' # example: "methods on the same type should have the same receiver name (seen 2x "r", 2x "s")" -- https://staticcheck.dev/docs/checks/#ST1016 +issues: + max-issues-per-linter: 0 + max-same-issues: 0 +formatters: + enable: + - gofmt + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/.goreleaser.yml b/.goreleaser.yml index 33424ec46..74f85eb59 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,11 +1,10 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: MPL-2.0 - +version: 2 project_name: terraform-plugin-testing builds: - skip: true milestones: - close: true release: + prerelease: auto ids: - 'none' diff --git a/CHANGELOG.md b/CHANGELOG.md index 1784e1671..c5cca6273 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,220 @@ +## 1.14.0-beta.1 (September 18, 2025) + +NOTES: + +* Adds an opt-in compatibility flag for config mode tests to unlock upgrade from v1.5.1 to latest for specific providers. ([#496](https://github.com/hashicorp/terraform-plugin-testing/issues/496)) +* This beta pre-release adds a new query mode to support testing for list blocks which can be used with Terraform v1.14+ ([#531](https://github.com/hashicorp/terraform-plugin-testing/issues/531)) +* all: This Go module has been updated to Go 1.24 per the Go support policy. It is recommended to review the Go 1.24 release notes before upgrading. ([#551](https://github.com/hashicorp/terraform-plugin-testing/issues/551)) + +## 1.13.2 (June 11, 2025) + +BUG FIXES: + +* helper/resource: Updated `ImportBlockWith*` import state modes to use the `ExpectNonEmpty` field to allow non-empty import plans to pass successfully. ([#518](https://github.com/hashicorp/terraform-plugin-testing/issues/518)) +* helper/resource: Fixed bug with import state mode where prior test config is not used for `ConfigFile` or `ConfigDirectory` ([#516](https://github.com/hashicorp/terraform-plugin-testing/issues/516)) + +## 1.13.1 (May 21, 2025) + +BUG FIXES: + +* echoprovider: Fixed bug where Terraform v1.12+ would return an error message indicating the provider doesn't support `GetResourceIdentitySchemas`. ([#512](https://github.com/hashicorp/terraform-plugin-testing/issues/512)) + +## 1.13.0 (May 16, 2025) + +NOTES: + +* reduced the volume of DEBUG-level logging to make it easier to visually scan debug output ([#463](https://github.com/hashicorp/terraform-plugin-testing/issues/463)) + +FEATURES: + +* ImportState: Added support for testing plannable import via Terraform configuration. Configuration is used from the previous test step if available. `Config`, `ConfigFile`, and `ConfigDirectory` can also be used directly with `ImportState` if needed. ([#442](https://github.com/hashicorp/terraform-plugin-testing/issues/442)) +* ImportState: Added `ImportStateKind` to control which method of import the `ImportState` test step uses. `ImportCommandWithID` (default, same behavior as today) , `ImportBlockWithID`, and `ImportBlockWithResourceIdentity`. ([#442](https://github.com/hashicorp/terraform-plugin-testing/issues/442)) +* ImportState: Added `ImportStateConfigExact` to opt-out of new import config generation for plannable import. ([#494](https://github.com/hashicorp/terraform-plugin-testing/issues/494)) +* statecheck: Added `ExpectIdentityValueMatchesState` state check to assert that an identity value matches a state value at the same path. ([#503](https://github.com/hashicorp/terraform-plugin-testing/issues/503)) +* statecheck: Added `ExpectIdentityValueMatchesStateAtPath` state check to assert that an identity value matches a state value at different paths. ([#503](https://github.com/hashicorp/terraform-plugin-testing/issues/503)) + +ENHANCEMENTS: + +* statecheck: Added `ExpectIdentityValue` state check, which asserts a specified attribute value of a managed resource identity in state. ([#468](https://github.com/hashicorp/terraform-plugin-testing/issues/468)) +* statecheck: Added `ExpectIdentity` state check, which asserts all data of a managed resource identity in state. ([#470](https://github.com/hashicorp/terraform-plugin-testing/issues/470)) +* Adds `AdditionalCLIOptions.PlanOptions.NoRefresh` to test `terraform plan -refresh=false` ([#490](https://github.com/hashicorp/terraform-plugin-testing/issues/490)) + +## 1.13.0-beta.1 (April 18, 2025) + +BREAKING CHANGES: + +* importstate: `ImportStatePersist` and `ImportStateVerify` are not supported for plannable import (`ImportBlockWith*`) and will return an error ([#476](https://github.com/hashicorp/terraform-plugin-testing/issues/476)) +* importstate: renamed `ImportStateWithId` to `ImportStateWithID` and renamed `ImportCommandWithId` to `ImportCommandWithID`. ([#465](https://github.com/hashicorp/terraform-plugin-testing/issues/465)) + +NOTES: + +* This beta pre-release adds support for managed resource identity, which can be used with Terraform v1.12.0-beta2. Acceptance tests can use the `ImportBlockWithResourceIdentity` kind to exercise the import of a managed resource using its resource identity object values instead of using a string identifier. ([#480](https://github.com/hashicorp/terraform-plugin-testing/issues/480)) + +BUG FIXES: + +* importstate: plannable import (`ImportBlockWith*`) fixed for a resource with a dependency ([#476](https://github.com/hashicorp/terraform-plugin-testing/issues/476)) + +## 1.13.0-alpha.1 (March 27, 2025) + +NOTES: + +* This alpha pre-release contains testing utilities for managed resource identity, which can be used with `Terraform v1.12.0-alpha20250319`, to assert identity data stored during apply workflows. A managed resource in a provider can read/store identity data using the `terraform-plugin-framework@v1.15.0-alpha.1` or `terraform-plugin-sdk/v2@v2.37.0-alpha.1` Go modules. To assert identity data stored by a provider in state, use the `statecheck.ExpectIdentity` state check. ([#470](https://github.com/hashicorp/terraform-plugin-testing/issues/470)) + +## 1.12.0 (March 18, 2025) + +NOTES: + +* all: This Go module has been updated to Go 1.23 per the [Go support policy](https://go.dev/doc/devel/release#policy). It is recommended to review the [Go 1.23 release notes](https://go.dev/doc/go1.23) before upgrading. Any consumers building on earlier Go versions may experience errors. ([#454](https://github.com/hashicorp/terraform-plugin-testing/issues/454)) + +FEATURES: + +* knownvalue: added function checks for custom validation of resource attribute or output values. ([#412](https://github.com/hashicorp/terraform-plugin-testing/issues/412)) + +ENHANCEMENTS: + +* knownvalue: Updated the `ObjectExact` error message to report extra/missing attributes from the actual object. ([#451](https://github.com/hashicorp/terraform-plugin-testing/issues/451)) +* plancheck: Improved the unknown value plan check error messages to include a known value if one exists. ([#450](https://github.com/hashicorp/terraform-plugin-testing/issues/450)) + +BUG FIXES: + +* plancheck: Fixed bug with all unknown value plan checks where a valid path would return a "path not found" error. ([#450](https://github.com/hashicorp/terraform-plugin-testing/issues/450)) + +## 1.11.0 (November 19, 2024) + +NOTES: + +* all: This Go module has been updated to Go 1.22 per the [Go support policy](https://go.dev/doc/devel/release#policy). It is recommended to review the [Go 1.22 release notes](https://go.dev/doc/go1.22) before upgrading. Any consumers building on earlier Go versions may experience errors. ([#371](https://github.com/hashicorp/terraform-plugin-testing/issues/371)) +* echoprovider: The `echoprovider` package is considered experimental and may be altered or removed in a subsequent release ([#389](https://github.com/hashicorp/terraform-plugin-testing/issues/389)) + +FEATURES: + +* tfversion: Added `SkipIfNotAlpha` version check for testing experimental features of alpha Terraform builds. ([#388](https://github.com/hashicorp/terraform-plugin-testing/issues/388)) +* echoprovider: Introduced new `echoprovider` package, which contains a v6 Terraform provider that can be used to test ephemeral resource data. ([#389](https://github.com/hashicorp/terraform-plugin-testing/issues/389)) + +## 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: + +* knownvalue: Add `Int32Exact` check for int32 value testing. ([#356](https://github.com/hashicorp/terraform-plugin-testing/issues/356)) +* knownvalue: Add `Float32Exact` check for float32 value testing. ([#356](https://github.com/hashicorp/terraform-plugin-testing/issues/356)) + +## 1.8.0 (May 17, 2024) + +FEATURES: + +* plancheck: Added `ExpectDeferredChange` and `ExpectNoDeferredChanges` checks for experimental deferred action support. ([#331](https://github.com/hashicorp/terraform-plugin-testing/issues/331)) +* tfversion: Added `SkipIfNotPrerelease` version check for testing experimental features of prerelease Terraform builds. ([#331](https://github.com/hashicorp/terraform-plugin-testing/issues/331)) + +ENHANCEMENTS: + +* helper/acctest: Improve scope of IPv4/IPv6 random address generation in RandIpAddress() ([#305](https://github.com/hashicorp/terraform-plugin-testing/issues/305)) +* knownvalue: Add `TupleExact`, `TuplePartial` and `TupleSizeExact` checks for dynamic value testing. ([#312](https://github.com/hashicorp/terraform-plugin-testing/issues/312)) +* tfversion: Ensured Terraform CLI prerelease versions are considered semantically equal to patch versions in built-in checks to match the Terraform CLI versioning policy ([#303](https://github.com/hashicorp/terraform-plugin-testing/issues/303)) +* helper/resource: Added `(TestCase).AdditionalCLIOptions` with `AllowDeferral` option for plan and apply commands. ([#331](https://github.com/hashicorp/terraform-plugin-testing/issues/331)) + +BUG FIXES: + +* helper/resource: Fix panic in output state shimming when a tuple is present. ([#310](https://github.com/hashicorp/terraform-plugin-testing/issues/310)) +* tfversion: Fixed `RequireBelow` ignoring equal versioning to fail a test ([#303](https://github.com/hashicorp/terraform-plugin-testing/issues/303)) + +## 1.7.0 (March 05, 2024) + +NOTES: + +* helper/resource: Error messages generated by the testing logic, which were updated for clarity in this release, are not protected by compatibility promises. While testing logic errors are usable in certain scenarios with `ErrorCheck` and `ExpectError` functionality, error messaging checks should be based on provider-controlled messaging or when appropriate to use other testing features such as `ExpectNonEmptyPlan` instead. ([#238](https://github.com/hashicorp/terraform-plugin-testing/issues/238)) +* Numerical values in the plan are now represented as json.Number, not float64. Custom plan checks relying upon float64 representation may need altering ([#248](https://github.com/hashicorp/terraform-plugin-testing/issues/248)) +* plancheck: Deprecated `ExpectNullOutputValue` and `ExpectNullOutputValueAtPath`. Use `ExpectKnownOutputValue` and `ExpectKnownOutputValueAtPath` with `knownvalue.Null` instead ([#275](https://github.com/hashicorp/terraform-plugin-testing/issues/275)) +* plancheck: `ExpectKnownValue`, `ExpectKnownOutputValue` and `ExpectKnownOutputValueAtPath` plan checks are considered experimental and may be altered or removed in a subsequent release ([#276](https://github.com/hashicorp/terraform-plugin-testing/issues/276)) +* statecheck: `ExpectKnownValue`, `ExpectKnownOutputValue` and `ExpectKnownOutputValueAtPath` state checks are considered experimental and may be altered or removed in a subsequent release ([#276](https://github.com/hashicorp/terraform-plugin-testing/issues/276)) +* knownvalue: The `knownvalue` package is considered experimental and may be altered or removed in a subsequent release ([#276](https://github.com/hashicorp/terraform-plugin-testing/issues/276)) +* all: This Go module has been updated to Go 1.21 per the [Go support policy](https://go.dev/doc/devel/release#policy). It is recommended to review the [Go 1.21 release notes](https://go.dev/doc/go1.21) before upgrading. Any consumers building on earlier Go versions may experience errors ([#300](https://github.com/hashicorp/terraform-plugin-testing/issues/300)) + +FEATURES: + +* plancheck: Added `ExpectKnownValue` plan check, which asserts that a given resource attribute has a defined type, and value ([#248](https://github.com/hashicorp/terraform-plugin-testing/issues/248)) +* plancheck: Added `ExpectKnownOutputValue` plan check, which asserts that a given output value has a defined type, and value ([#248](https://github.com/hashicorp/terraform-plugin-testing/issues/248)) +* plancheck: Added `ExpectKnownOutputValueAtPath` plan check, which asserts that a given output value at a specified path has a defined type, and value ([#248](https://github.com/hashicorp/terraform-plugin-testing/issues/248)) +* knownvalue: Introduced new `knownvalue` package which contains types for working with plan checks and state checks ([#248](https://github.com/hashicorp/terraform-plugin-testing/issues/248)) +* statecheck: Introduced new `statecheck` package with interface and built-in state check functionality ([#275](https://github.com/hashicorp/terraform-plugin-testing/issues/275)) +* statecheck: Added `ExpectKnownValue` state check, which asserts that a given resource attribute has a defined type, and value ([#275](https://github.com/hashicorp/terraform-plugin-testing/issues/275)) +* statecheck: Added `ExpectKnownOutputValue` state check, which asserts that a given output value has a defined type, and value ([#275](https://github.com/hashicorp/terraform-plugin-testing/issues/275)) +* statecheck: Added `ExpectKnownOutputValueAtPath` plan check, which asserts that a given output value at a specified path has a defined type, and value ([#275](https://github.com/hashicorp/terraform-plugin-testing/issues/275)) +* statecheck: Added `ExpectSensitiveValue` built-in state check, which asserts that a given attribute has a sensitive value ([#275](https://github.com/hashicorp/terraform-plugin-testing/issues/275)) + +BUG FIXES: + +* helper/resource: Clarified error messaging from testing failures, especially when using `TestStep.PlanOnly: true` ([#238](https://github.com/hashicorp/terraform-plugin-testing/issues/238)) +* helper/resource: Fix detection of provider block declaration in `Config`, `ConfigDirectory`, and `ConfigFile` ([#265](https://github.com/hashicorp/terraform-plugin-testing/issues/265)) +* helper/resource: Fix detection of terraform block declaration in `Config`, `ConfigDirectory`, and `ConfigFile` ([#265](https://github.com/hashicorp/terraform-plugin-testing/issues/265)) +* helper/resource: Fixed internal deferred test helpers to properly report file and line information in test failures. ([#292](https://github.com/hashicorp/terraform-plugin-testing/issues/292)) + +## 1.6.0 (December 04, 2023) + +NOTES: + +* all: This Go module has been updated to Go 1.20 per the [Go support policy](https://go.dev/doc/devel/release#policy). It is recommended to review the [Go 1.20 release notes](https://go.dev/doc/go1.20) before upgrading. Any consumers building on earlier Go versions may experience errors. ([#180](https://github.com/hashicorp/terraform-plugin-testing/issues/180)) +* helper/resource: Configuration based `TestStep` now include post-apply plan checks for output changes in addition to resource changes. If this causes unexpected new test failures, most `output` configuration blocks can be likely be removed. Test steps involving resources and data sources should never need to use `output` configuration blocks as plan and state checks support working on resource and data source attributes values directly. ([#234](https://github.com/hashicorp/terraform-plugin-testing/issues/234)) +* helper/resource: Implicit `terraform refresh` commands during each `TestStep` have been removed to fix plan check and performance issues, which can cause new test failures when testing schema changes (e.g. state upgrades) that have a final `TestStep` with `PlanOnly: true`. Remove `PlanOnly: true` from the final `TestStep` to fix affected tests which will ensure that updated schema changes are applied to the state before attempting to automatically destroy resources. ([#223](https://github.com/hashicorp/terraform-plugin-testing/issues/223)) + +FEATURES: + +* plancheck: Added `ExpectUnknownOutputValue` built-in plan check, which asserts that a given output value at a specified address is unknown ([#220](https://github.com/hashicorp/terraform-plugin-testing/issues/220)) +* plancheck: Added `ExpectUnknownOutputValueAtPath` built-in plan check, which asserts that a given output value at a specified address, and path is unknown ([#220](https://github.com/hashicorp/terraform-plugin-testing/issues/220)) +* plancheck: Added `ExpectNullOutputValue` built-in plan check, which asserts that a given output value at a specified address is null ([#220](https://github.com/hashicorp/terraform-plugin-testing/issues/220)) +* plancheck: Added `ExpectNullOutputValueAtPath` built-in plan check, which asserts that a given output value at a specified address, and path is null ([#220](https://github.com/hashicorp/terraform-plugin-testing/issues/220)) + +ENHANCEMENTS: + +* helper/resource: Removed separate refresh commands, which increases testing performance ([#223](https://github.com/hashicorp/terraform-plugin-testing/issues/223)) +* helper/resource: Automatically add `required_providers` configuration to `TestStep.Config` Terraform language configuration when using Terraform >= 1.0.* ([#216](https://github.com/hashicorp/terraform-plugin-testing/issues/216)) + +BUG FIXES: + +* plancheck: Ensured `ExpectEmptyPlan` and `ExpectNonEmptyPlan` account for output changes ([#222](https://github.com/hashicorp/terraform-plugin-testing/issues/222)) +* helper/resource: Ensured `TestStep.ExpectNonEmptyPlan` accounts for output changes with Terraform 0.14 and later ([#234](https://github.com/hashicorp/terraform-plugin-testing/issues/234)) + +## 1.5.1 (August 31, 2023) + +BUG FIXES: + +* helper/resource: Fix regression by allowing providers to be defined both at the `TestCase` level, and within `TestStep.Config` ([#177](https://github.com/hashicorp/terraform-plugin-testing/issues/177)) + +## 1.5.0 (August 31, 2023) + +FEATURES: + +* config: Introduced new `config` package which contains interfaces and helper functions for working with native Terraform configuration and variables ([#153](https://github.com/hashicorp/terraform-plugin-testing/issues/153)) +* helper/resource: Added `TestStep.ConfigDirectory` to allow specifying a directory containing Terraform configuration for use during acceptance tests ([#153](https://github.com/hashicorp/terraform-plugin-testing/issues/153)) +* helper/resource: Added `TestStep.ConfigFile` to allow specifying a file containing Terraform configuration for use during acceptance tests ([#153](https://github.com/hashicorp/terraform-plugin-testing/issues/153)) +* helper/resource: Added `TestStep.ConfigVariables` to allow specifying Terraform variables for use with Terraform configuration during acceptance tests ([#153](https://github.com/hashicorp/terraform-plugin-testing/issues/153)) +* helper/resource: Removed data resource and managed resource `id` attribute requirement ([#84](https://github.com/hashicorp/terraform-plugin-testing/issues/84)) + +ENHANCEMENTS: + +* helper/resource: Added `TestStep` type `ImportStateVerifyIdentifierAttribute` field, which can override the default `id` attribute used for matching prior resource state with imported resource state ([#84](https://github.com/hashicorp/terraform-plugin-testing/issues/84)) + +## 1.4.0 (July 24, 2023) + +FEATURES: + +* tfjsonpath: Introduced new `tfjsonpath` package which contains methods that allow traversal of Terraform JSON data ([#154](https://github.com/hashicorp/terraform-plugin-testing/issues/154)) +* plancheck: Added `ExpectUnknownValue` built-in plan check, which asserts that a given attribute has an unknown value ([#154](https://github.com/hashicorp/terraform-plugin-testing/issues/154)) +* plancheck: Added `ExpectSensitiveValue` built-in plan check, which asserts that a given attribute has a sensitive value ([#154](https://github.com/hashicorp/terraform-plugin-testing/issues/154)) + ## 1.3.0 (June 13, 2023) FEATURES: diff --git a/META.d/_summary.yaml b/META.d/_summary.yaml new file mode 100644 index 000000000..56ab752a7 --- /dev/null +++ b/META.d/_summary.yaml @@ -0,0 +1,10 @@ +--- +schema: 1.1 + +partition: tf-ecosystem + +summary: + owner: team-tf-core-plugins + description: | + Module for testing Terraform providers + visibility: public diff --git a/README.md b/README.md index 8342917c3..f97327ad6 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,11 @@ When run from the root of a Terraform Provider codebase, Terraform’s testing f This project follows the [support policy](https://golang.org/doc/devel/release.html#policy) of Go as its support policy. The two latest major releases of Go are supported by the project. -Currently, that means Go **1.19** or later must be used when including this project as a dependency. +Currently, that means Go **1.24** or later must be used when including this project as a dependency. + +## Documentation + +Visit the [Testing Terraform Plugins docs](https://developer.hashicorp.com/terraform/plugin/testing) to learn about how to best use this helper module. ## Contributing 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..6a6988721 --- /dev/null +++ b/compare/values_differ_test.go @@ -0,0 +1,60 @@ +// 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 { + 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..e868ab0ea --- /dev/null +++ b/compare/values_same_test.go @@ -0,0 +1,71 @@ +// 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 { + 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/config/config.go b/config/config.go new file mode 100644 index 000000000..4a663610f --- /dev/null +++ b/config/config.go @@ -0,0 +1,38 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package config + +// TestStepConfigFunc is the callback type used with acceptance tests to +// specify a string which either identifies a directory containing +// Terraform configuration files, or a file that contains Terraform +// configuration. +type TestStepConfigFunc func(TestStepConfigRequest) string + +// TestStepConfigRequest defines the request supplied to types +// implementing TestStepConfigFunc. StepNumber is one-based +// and is used in the predefined helper functions: +// +// - [config.TestStepDirectory] +// - [config.TestStepFile]. +// +// TestName is used in the predefined helper functions: +// +// - [config.TestNameDirectory] +// - [config.TestStepDirectory] +// - [config.TestNameFile] +// - [config.TestStepFile] +type TestStepConfigRequest struct { + StepNumber int + TestName string +} + +// Exec executes TestStepConfigFunc if it is not nil, otherwise an +// empty string is returned. +func (f TestStepConfigFunc) Exec(req TestStepConfigRequest) string { + if f != nil { + return f(req) + } + + return "" +} diff --git a/config/constraints.go b/config/constraints.go new file mode 100644 index 000000000..3842cc0cc --- /dev/null +++ b/config/constraints.go @@ -0,0 +1,27 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package config + +// anyFloat is a constraint that permits any floating-point type. This type +// definition is copied rather than depending on x/exp/constraints since the +// dependency is otherwise unneeded, the definition is relatively trivial and +// static, and the Go language maintainers are not sure if/where these will live +// in the standard library. +// +// Reference: https://github.com/golang/go/issues/61914 +type anyFloat interface { + ~float32 | ~float64 +} + +// anyInteger is a constraint that permits any integer type. This type +// definition is copied rather than depending on x/exp/constraints since the +// dependency is otherwise unneeded, the definition is relatively trivial and +// static, and the Go language maintainers are not sure if/where these will live +// in the standard library. +// +// Reference: https://github.com/golang/go/issues/61914 +type anyInteger interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 | + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr +} diff --git a/config/directory.go b/config/directory.go new file mode 100644 index 000000000..c3c9ab0c0 --- /dev/null +++ b/config/directory.go @@ -0,0 +1,63 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package config + +import ( + "path/filepath" + "strconv" +) + +// StaticDirectory returns the supplied directory. +func StaticDirectory(directory string) func(TestStepConfigRequest) string { + return func(_ TestStepConfigRequest) string { + return directory + } +} + +// TestNameDirectory returns the name of the test prefixed with +// "testdata". +// +// For example, given test code: +// +// func TestExampleCloudThing_basic(t *testing.T) { +// resource.Test(t, resource.TestCase{ +// Steps: []resource.TestStep{ +// { +// ConfigDirectory: config.TestNameDirectory(), +// }, +// }, +// }) +// } +// +// The testing configurations will be expected in the +// testdata/TestExampleCloudThing_basic/ directory. +func TestNameDirectory() func(TestStepConfigRequest) string { + return func(req TestStepConfigRequest) string { + return filepath.Join("testdata", req.TestName) + } +} + +// TestStepDirectory returns the name of the test suffixed with the +// test step number and prefixed with "testdata". +// +// For example, given test code: +// +// func TestExampleCloudThing_basic(t *testing.T) { +// resource.Test(t, resource.TestCase{ +// Steps: []resource.TestStep{ +// { +// ConfigDirectory: config.TestStepDirectory(), +// }, +// }, +// }) +// } +// +// The testing configurations will be expected in the +// testdata/TestExampleCloudThing_basic/1 directory as +// TestStepConfigRequest.StepNumber is one-based. +func TestStepDirectory() func(TestStepConfigRequest) string { + return func(req TestStepConfigRequest) string { + return filepath.Join("testdata", req.TestName, strconv.Itoa(req.StepNumber)) + } +} diff --git a/config/directory_test.go b/config/directory_test.go new file mode 100644 index 000000000..2418c40f4 --- /dev/null +++ b/config/directory_test.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package config_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/config" +) + +func TestTestStepConfigFunc_Exec_Directory(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + testStepConfigFunc config.TestStepConfigFunc + testStepConfigRequest config.TestStepConfigRequest + expected string + }{ + "static_directory": { + testStepConfigFunc: config.StaticDirectory("name_of_directory"), + expected: "name_of_directory", + }, + "test_name_directory": { + testStepConfigFunc: config.TestNameDirectory(), + testStepConfigRequest: config.TestStepConfigRequest{ + TestName: "TestTestStepConfigFunc_Exec", + }, + expected: "testdata/TestTestStepConfigFunc_Exec", + }, + "test_step_directory": { + testStepConfigFunc: config.TestStepDirectory(), + testStepConfigRequest: config.TestStepConfigRequest{ + StepNumber: 1, + TestName: "TestTestStepConfigFunc_Exec", + }, + expected: "testdata/TestTestStepConfigFunc_Exec/1", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.testStepConfigFunc.Exec(testCase.testStepConfigRequest) + + if testCase.expected != got { + t.Errorf("expected %s, got %s", testCase.expected, got) + } + }) + } +} diff --git a/config/doc.go b/config/doc.go new file mode 100644 index 000000000..e85d5f81c --- /dev/null +++ b/config/doc.go @@ -0,0 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package config implements functionality for supporting native +// Terraform configuration and variables for testing purposes. +package config diff --git a/config/file.go b/config/file.go new file mode 100644 index 000000000..1974c4065 --- /dev/null +++ b/config/file.go @@ -0,0 +1,63 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package config + +import ( + "path/filepath" + "strconv" +) + +// StaticFile returns the supplied file. +func StaticFile(file string) func(TestStepConfigRequest) string { + return func(_ TestStepConfigRequest) string { + return file + } +} + +// TestNameFile returns the name of the test suffixed with the supplied +// file and prefixed with "testdata". +// +// For example, given test code: +// +// func TestExampleCloudThing_basic(t *testing.T) { +// resource.Test(t, resource.TestCase{ +// Steps: []resource.TestStep{ +// { +// ConfigFile: config.TestNameFile("test.tf"), +// }, +// }, +// }) +// } +// +// The testing configuration will be expected in the +// testdata/TestExampleCloudThing_basic/test.tf file. +func TestNameFile(file string) func(TestStepConfigRequest) string { + return func(req TestStepConfigRequest) string { + return filepath.Join("testdata", req.TestName, file) + } +} + +// TestStepFile returns the name of the test suffixed with the test +// step number and the supplied file, and prefixed with "testdata". +// +// For example, given test code: +// +// func TestExampleCloudThing_basic(t *testing.T) { +// resource.Test(t, resource.TestCase{ +// Steps: []resource.TestStep{ +// { +// ConfigFile: config.TestStepFile("test.tf"), +// }, +// }, +// }) +// } +// +// The testing configuration will be expected in the +// testdata/TestExampleCloudThing_basic/1/test.tf file +// as TestStepConfigRequest.StepNumber is one-based. +func TestStepFile(file string) func(TestStepConfigRequest) string { + return func(req TestStepConfigRequest) string { + return filepath.Join("testdata", req.TestName, strconv.Itoa(req.StepNumber), file) + } +} diff --git a/config/file_test.go b/config/file_test.go new file mode 100644 index 000000000..db60cba6a --- /dev/null +++ b/config/file_test.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package config_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/config" +) + +func TestTestStepConfigFunc_Exec_File(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + testStepConfigFunc config.TestStepConfigFunc + testStepConfigRequest config.TestStepConfigRequest + expected string + }{ + "static_file": { + testStepConfigFunc: config.StaticFile("name_of_file"), + expected: "name_of_file", + }, + "test_name_file": { + testStepConfigFunc: config.TestNameFile("test.tf"), + testStepConfigRequest: config.TestStepConfigRequest{ + TestName: "TestTestStepConfigFunc_Exec", + }, + expected: "testdata/TestTestStepConfigFunc_Exec/test.tf", + }, + "test_step_file": { + testStepConfigFunc: config.TestStepFile("test.tf"), + testStepConfigRequest: config.TestStepConfigRequest{ + StepNumber: 1, + TestName: "TestTestStepConfigFunc_Exec", + }, + expected: "testdata/TestTestStepConfigFunc_Exec/1/test.tf", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.testStepConfigFunc.Exec(testCase.testStepConfigRequest) + + if testCase.expected != got { + t.Errorf("expected %s, got %s", testCase.expected, got) + } + }) + } +} diff --git a/config/variable.go b/config/variable.go new file mode 100644 index 000000000..fa109a2a5 --- /dev/null +++ b/config/variable.go @@ -0,0 +1,324 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package config + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "reflect" +) + +const autoTFVarsJson = "terraform-plugin-testing.auto.tfvars.json" + +// Variable interface is an alias to json.Marshaler. +type Variable interface { + json.Marshaler +} + +// Variables is a type holding a key-value map of variable names +// to types implementing the Variable interface. +type Variables map[string]Variable + +// Write creates a file in the destination supplied +// containing JSON encoded Variables. +func (v Variables) Write(dest string) error { + if len(v) == 0 { + return nil + } + + b, err := json.Marshal(v) + + if err != nil { + return fmt.Errorf("cannot marshal variables: %s", err) + } + + outFilename := filepath.Join(dest, autoTFVarsJson) + + err = os.WriteFile(outFilename, b, 0600) + + if err != nil { + return fmt.Errorf("cannot write variables file: %s", err) + } + + return nil +} + +var _ Variable = boolVariable{} + +// boolVariable supports JSON encoding of a bool. +type boolVariable struct { + value bool +} + +// MarshalJSON returns the JSON encoding of boolVariable. +func (v boolVariable) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +// BoolVariable returns boolVariable which implements Variable. +func BoolVariable(value bool) boolVariable { + return boolVariable{ + value: value, + } +} + +var _ Variable = floatVariable{} + +// floatVariable supports JSON encoding of any floating-point type. +type floatVariable struct { + value any +} + +// MarshalJSON returns the JSON encoding of floatVariable. +func (v floatVariable) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +// FloatVariable returns floatVariable which implements Variable. +func FloatVariable[T anyFloat](value T) floatVariable { + return floatVariable{ + value: value, + } +} + +var _ Variable = integerVariable{} + +// integerVariable supports JSON encoding of any integer type. +type integerVariable struct { + value any +} + +// MarshalJSON returns the JSON encoding of integerVariable. +func (v integerVariable) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +// IntegerVariable returns integerVariable which implements Variable. +func IntegerVariable[T anyInteger](value T) integerVariable { + return integerVariable{ + value: value, + } +} + +var _ Variable = listVariable{} + +// listVariable supports JSON encoding of slice of Variable. +type listVariable struct { + value []Variable +} + +// MarshalJSON returns the JSON encoding of listVariable. +// Every Variable within a listVariable must be the same +// underlying type. +func (v listVariable) MarshalJSON() ([]byte, error) { + if !typesEq(v.value) { + return nil, errors.New("lists must contain the same type") + } + + return json.Marshal(v.value) +} + +// ListVariable returns listVariable which implements Variable. +func ListVariable(value ...Variable) listVariable { + return listVariable{ + value: value, + } +} + +var _ Variable = mapVariable{} + +// mapVariable supports JSON encoding of a key-value map of +// string to Variable. +type mapVariable struct { + value map[string]Variable +} + +// MarshalJSON returns the JSON encoding of mapVariable. +// Every Variable in a mapVariable must be the same +// underlying type. +func (v mapVariable) MarshalJSON() ([]byte, error) { + var variables []Variable + + for _, variable := range v.value { + variables = append(variables, variable) + } + + if !typesEq(variables) { + return nil, errors.New("maps must contain the same type") + } + + return json.Marshal(v.value) +} + +// MapVariable returns mapVariable which implements Variable. +func MapVariable(value map[string]Variable) mapVariable { + return mapVariable{ + value: value, + } +} + +var _ Variable = objectVariable{} + +// objectVariable supports JSON encoding of a key-value +// map of string to Variable in which each Variable +// can be a different underlying type. +type objectVariable struct { + value map[string]Variable +} + +// MarshalJSON returns the JSON encoding of objectVariable. +func (v objectVariable) MarshalJSON() ([]byte, error) { + b, err := json.Marshal(v.value) + + if err != nil { + innerErr := err + + // Unwrap is used here to expose the initial error, for example + // "maps must contain the same type" whilst removing any errors + // related to the implementation (i.e., the usage of + // encoding/json in this instance. + for errors.Unwrap(innerErr) != nil { + innerErr = errors.Unwrap(err) + } + + return nil, innerErr + } + + return b, nil +} + +// ObjectVariable returns objectVariable which implements Variable. +func ObjectVariable(value map[string]Variable) objectVariable { + return objectVariable{ + value: value, + } +} + +var _ Variable = setVariable{} + +// setVariable supports JSON encoding of a slice of Variable. +type setVariable struct { + value []Variable +} + +// MarshalJSON returns the JSON encoding of setVariable. +// Every Variable in a setVariable must be the same +// underlying type. +func (v setVariable) MarshalJSON() ([]byte, error) { + for kx, x := range v.value { + for ky := kx + 1; ky < len(v.value); ky++ { + y := v.value[ky] + + if _, ok := x.(setVariable); !ok { + continue + } + + if _, ok := y.(setVariable); !ok { + continue + } + + if reflect.DeepEqual(x, y) { + return nil, errors.New("sets must contain unique elements") + } + } + } + + if !typesEq(v.value) { + return nil, errors.New("sets must contain the same type") + } + + return json.Marshal(v.value) +} + +// SetVariable returns setVariable which implements Variable. +func SetVariable(value ...Variable) setVariable { + return setVariable{ + value: value, + } +} + +var _ Variable = stringVariable{} + +// stringVariable supports JSON encoding of a string. +type stringVariable struct { + value string +} + +// MarshalJSON returns the JSON encoding of stringVariable. +func (v stringVariable) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +// StringVariable returns stringVariable which implements Variable. +func StringVariable(value string) stringVariable { + return stringVariable{ + value: value, + } +} + +var _ Variable = tupleVariable{} + +// tupleVariable supports JSON encoding of a slice of Variable +// in which each element in the slice can be a different +// underlying type. +type tupleVariable struct { + value []Variable +} + +// MarshalJSON returns the JSON encoding of tupleVariable. +func (v tupleVariable) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +// TupleVariable returns tupleVariable which implements Variable. +func TupleVariable(value ...Variable) tupleVariable { + return tupleVariable{ + value: value, + } +} + +// typesEq verifies that every element in the supplied slice of Variable +// is the same underlying type. +func typesEq(variables []Variable) bool { + var t reflect.Type + + for _, variable := range variables { + switch x := variable.(type) { + case listVariable: + if !typesEq(x.value) { + return false + } + case mapVariable: + var vars []Variable + + for _, v := range x.value { + vars = append(vars, v) + } + + if !typesEq(vars) { + return false + } + case setVariable: + if !typesEq(x.value) { + return false + } + } + + typeOfVariable := reflect.TypeOf(variable) + + if t == nil { + t = typeOfVariable + continue + } + + if t != typeOfVariable { + return false + } + } + + return true +} diff --git a/config/variable_test.go b/config/variable_test.go new file mode 100644 index 000000000..8cea766cc --- /dev/null +++ b/config/variable_test.go @@ -0,0 +1,368 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package config_test + +import ( + "bytes" + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/config" +) + +func TestMarshalJSON(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + variable config.Variable + expected []byte + expectedError string + }{ + "bool": { + variable: config.BoolVariable(true), + expected: []byte(`true`), + }, + "float": { + variable: config.FloatVariable(1.2), + expected: []byte(`1.2`), + }, + "integer": { + variable: config.IntegerVariable(12), + expected: []byte(`12`), + }, + "list_bool": { + variable: config.ListVariable( + config.BoolVariable(false), + config.BoolVariable(false), + config.BoolVariable(true), + ), + expected: []byte(`[false,false,true]`), + }, + "list_list": { + variable: config.ListVariable( + config.ListVariable( + config.BoolVariable(false), + config.BoolVariable(false), + config.BoolVariable(true), + ), + config.ListVariable( + config.BoolVariable(true), + config.BoolVariable(true), + config.BoolVariable(false), + ), + ), + expected: []byte(`[[false,false,true],[true,true,false]]`), + }, + "list_mixed_types": { + variable: config.ListVariable( + config.BoolVariable(false), + config.StringVariable("str"), + ), + expectedError: "lists must contain the same type", + }, + "list_list_mixed_types": { + variable: config.ListVariable( + config.ListVariable( + config.BoolVariable(false), + config.StringVariable("str"), + ), + ), + expectedError: "lists must contain the same type", + }, + "list_list_mixed_types_multiple_lists": { + variable: config.ListVariable( + config.ListVariable( + config.BoolVariable(false), + config.BoolVariable(false), + ), + config.ListVariable( + config.StringVariable("str"), + config.BoolVariable(false), + ), + ), + expectedError: "lists must contain the same type", + }, + "map_bool": { + variable: config.MapVariable( + map[string]config.Variable{ + "one": config.BoolVariable(false), + "two": config.BoolVariable(false), + "three": config.BoolVariable(true), + }, + ), + expected: []byte(`{"one":false,"three":true,"two":false}`), + }, + "map_map": { + variable: config.ListVariable( + config.MapVariable( + map[string]config.Variable{ + "one": config.BoolVariable(false), + "two": config.BoolVariable(false), + "three": config.BoolVariable(true), + }, + ), + config.MapVariable( + map[string]config.Variable{ + "one": config.BoolVariable(true), + "two": config.BoolVariable(true), + "three": config.BoolVariable(false), + }, + ), + ), + expected: []byte(`[{"one":false,"three":true,"two":false},{"one":true,"three":false,"two":true}]`), + }, + "map_mixed_types": { + variable: config.MapVariable( + map[string]config.Variable{ + "one": config.BoolVariable(false), + "two": config.StringVariable("str"), + }, + ), + expectedError: "maps must contain the same type", + }, + "map_map_mixed_types": { + variable: config.MapVariable( + map[string]config.Variable{ + "mapA": config.MapVariable( + map[string]config.Variable{ + "one": config.BoolVariable(false), + "two": config.StringVariable("str"), + }, + ), + }, + ), + expectedError: "maps must contain the same type", + }, + "map_map_mixed_types_multiple_maps": { + variable: config.MapVariable( + map[string]config.Variable{ + "mapA": config.MapVariable( + map[string]config.Variable{ + "one": config.BoolVariable(false), + "two": config.BoolVariable(true), + }, + ), + "mapB": config.MapVariable( + map[string]config.Variable{ + "one": config.BoolVariable(false), + "two": config.StringVariable("str"), + }, + ), + }, + ), + expectedError: "maps must contain the same type", + }, + "object": { + variable: config.ObjectVariable( + map[string]config.Variable{ + "bool": config.BoolVariable(true), + "list": config.ListVariable( + config.BoolVariable(false), + config.BoolVariable(true), + ), + "map": config.MapVariable( + map[string]config.Variable{ + "one": config.StringVariable("str_one"), + "two": config.StringVariable("str_two"), + }, + ), + }, + ), + expected: []byte(`{"bool":true,"list":[false,true],"map":{"one":"str_one","two":"str_two"}}`), + }, + "object_map_mixed_types": { + variable: config.ObjectVariable( + map[string]config.Variable{ + "bool": config.BoolVariable(true), + "list": config.ListVariable( + config.BoolVariable(false), + config.BoolVariable(true), + ), + "map": config.MapVariable( + map[string]config.Variable{ + "one": config.BoolVariable(false), + "two": config.StringVariable("str_two"), + }, + ), + }, + ), + expectedError: "maps must contain the same type", + }, + "set_bool": { + variable: config.SetVariable( + config.BoolVariable(false), + config.BoolVariable(false), + config.BoolVariable(true), + ), + expected: []byte(`[false,false,true]`), + }, + "set_set": { + variable: config.SetVariable( + config.SetVariable( + config.BoolVariable(false), + config.BoolVariable(false), + config.BoolVariable(true), + ), + config.SetVariable( + config.BoolVariable(true), + config.BoolVariable(true), + config.BoolVariable(false), + ), + ), + expected: []byte(`[[false,false,true],[true,true,false]]`), + }, + "set_mixed_types": { + variable: config.SetVariable( + config.BoolVariable(false), + config.StringVariable("str"), + ), + expectedError: "sets must contain the same type", + }, + "set_set_mixed_types": { + variable: config.SetVariable( + config.SetVariable( + config.BoolVariable(false), + config.StringVariable("str"), + ), + ), + expectedError: "sets must contain the same type", + }, + "set_set_mixed_types_multiple_sets": { + variable: config.SetVariable( + config.SetVariable( + config.BoolVariable(false), + config.BoolVariable(false), + ), + config.SetVariable( + config.StringVariable("str"), + config.BoolVariable(false), + ), + ), + expectedError: "sets must contain the same type", + }, + "set_non_unique": { + variable: config.SetVariable( + config.SetVariable( + config.BoolVariable(false), + config.BoolVariable(false), + ), + config.SetVariable( + config.BoolVariable(false), + config.BoolVariable(false), + ), + ), + expectedError: "sets must contain unique elements", + }, + "string": { + variable: config.StringVariable("str"), + expected: []byte(`"str"`), + }, + "tuple": { + variable: config.TupleVariable( + config.BoolVariable(true), + config.FloatVariable(1.2), + config.StringVariable("str"), + ), + expected: []byte(`[true,1.2,"str"]`), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.variable.MarshalJSON() + + if testCase.expectedError == "" && err != nil { + t.Errorf("unexpected error %s", err) + } + + if testCase.expectedError != "" && err == nil { + t.Errorf("expected error but got none") + } + + if testCase.expectedError != "" && err != nil { + if diff := cmp.Diff(err.Error(), testCase.expectedError); diff != "" { + t.Errorf("expected error %s, got error %s", testCase.expectedError, err) + } + } + + if !bytes.Equal(testCase.expected, got) { + t.Errorf("expected %s, got %s", testCase.expected, got) + } + }) + } +} + +func TestVariablesWrite(t *testing.T) { + t.Parallel() + + tempDir := t.TempDir() + + testCases := map[string]struct { + variables config.Variables + expected []byte + expectedError string + }{ + "write": { + variables: map[string]config.Variable{ + "bool": config.BoolVariable(true), + "string": config.StringVariable("str"), + }, + expected: []byte(`{"bool": true,"string": "str"}`), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + err := testCase.variables.Write(tempDir) + + if testCase.expectedError == "" && err != nil { + t.Errorf("unexpected error %s", err) + } + + if testCase.expectedError != "" && err == nil { + t.Errorf("expected error but got none") + } + + if testCase.expectedError != "" && err != nil { + if diff := cmp.Diff(err.Error(), testCase.expectedError); diff != "" { + t.Errorf("expected error %s, got error %s", testCase.expectedError, err) + } + } + + b, err := os.ReadFile(filepath.Join(tempDir, "terraform-plugin-testing.auto.tfvars.json")) + + if err != nil { + t.Errorf("error reading tfvars file: %s", err) + } + + var expectedUnmarshalled map[string]any + + err = json.Unmarshal(testCase.expected, &expectedUnmarshalled) + + if err != nil { + t.Errorf("error unmarshalling expected: %s", err) + } + + var gotUnmarshalled map[string]any + + err = json.Unmarshal(b, &gotUnmarshalled) + + if err != nil { + t.Errorf("error unmarshalling got: %s", err) + } + + if diff := cmp.Diff(expectedUnmarshalled, gotUnmarshalled); diff != "" { + t.Errorf("expected %s, got %s", expectedUnmarshalled, gotUnmarshalled) + } + }) + } +} diff --git a/echoprovider/doc.go b/echoprovider/doc.go new file mode 100644 index 000000000..753097f82 --- /dev/null +++ b/echoprovider/doc.go @@ -0,0 +1,20 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package echoprovider contains a protocol v6 Terraform provider that can be used to transfer data from +// provider configuration to state via a managed resource. This is only meant for provider acceptance testing +// of data that cannot be stored in Terraform artifacts (plan/state), such as an ephemeral resource. +// +// Example Usage: +// +// // Ephemeral resource that is under test +// ephemeral "examplecloud_thing" "this" { +// name = "thing-one" +// } +// +// provider "echo" { +// data = ephemeral.examplecloud_thing.this +// } +// +// resource "echo" "test" {} // The `echo.test.data` attribute will contain the ephemeral data from `ephemeral.examplecloud_thing.this` +package echoprovider diff --git a/echoprovider/server.go b/echoprovider/server.go new file mode 100644 index 000000000..efcdb15f1 --- /dev/null +++ b/echoprovider/server.go @@ -0,0 +1,424 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package echoprovider + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// NewProviderServer returns the "echo" provider, which is a protocol v6 Terraform provider meant only to be used for testing +// data which cannot be stored in Terraform artifacts (plan/state), such as an ephemeral resource. The "echo" provider can be included in +// an acceptance test with the `(resource.TestCase).ProtoV6ProviderFactories` field, for example: +// +// resource.UnitTest(t, resource.TestCase{ +// // .. other TestCase fields +// ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ +// "echo": echoprovider.NewProviderServer(), +// }, +// +// // .. TestSteps +// }) +// +// The "echo" provider configuration accepts in a dynamic "data" attribute, which will be stored in the "echo" managed resource "data" attribute, for example: +// +// // Ephemeral resource that is under test +// ephemeral "examplecloud_thing" "this" { +// name = "thing-one" +// } +// +// provider "echo" { +// data = ephemeral.examplecloud_thing.this +// } +// +// resource "echo" "test" {} // The `echo.test.data` attribute will contain the ephemeral data from `ephemeral.examplecloud_thing.this` +func NewProviderServer() func() (tfprotov6.ProviderServer, error) { + return func() (tfprotov6.ProviderServer, error) { + return &echoProviderServer{}, nil + } +} + +// echoProviderServer is a lightweight protocol version 6 provider server that saves data from the provider configuration (which is considered ephemeral) +// and then stores that data into state during ApplyResourceChange. +// +// As provider configuration is ephemeral, it's possible for the data to change between plan and apply. As a result of this, the echo provider +// will never propose new changes after it has been created, making it immutable (during plan, echo will always use prior state for it's plan, +// regardless of what the provider configuration is set to). This prevents the managed resource from continuously proposing new planned changes +// if the ephemeral data changes. +type echoProviderServer struct { + // The value of the "data" attribute during provider configuration. Will be directly echoed to the echo.data attribute. + providerConfigData tftypes.Value +} + +const echoResourceType = "echo" + +func (e *echoProviderServer) providerSchema() *tfprotov6.Schema { + return &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Description: "This provider is used to output the data attribute provided to the provider configuration into all resources instances of echo. " + + "This is only useful for testing ephemeral resources where the data isn't stored to state.", + DescriptionKind: tfprotov6.StringKindPlain, + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "data", + Type: tftypes.DynamicPseudoType, + Description: "Dynamic data to provide to the echo resource.", + DescriptionKind: tfprotov6.StringKindPlain, + Optional: true, + }, + }, + }, + } +} + +func (e *echoProviderServer) testResourceSchema() *tfprotov6.Schema { + return &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "data", + Type: tftypes.DynamicPseudoType, + Description: "Dynamic data that was provided to the provider configuration.", + DescriptionKind: tfprotov6.StringKindPlain, + Computed: true, + }, + }, + }, + } +} + +func (e *echoProviderServer) ApplyResourceChange(ctx context.Context, req *tfprotov6.ApplyResourceChangeRequest) (*tfprotov6.ApplyResourceChangeResponse, error) { + resp := &tfprotov6.ApplyResourceChangeResponse{} + + if req.TypeName != echoResourceType { + resp.Diagnostics = []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unsupported Resource", + Detail: fmt.Sprintf("ApplyResourceChange was called for a resource type that is not supported by this provider: %q", req.TypeName), + }, + } + + return resp, nil + } + + echoTestSchema := e.testResourceSchema() + + plannedState, diag := dynamicValueToValue(echoTestSchema, req.PlannedState) + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + // Destroy Op, just return planned state, which is null + if plannedState.IsNull() { + resp.NewState = req.PlannedState + return resp, nil + } + + // Take the provider config "data" attribute verbatim and put back into state. It shares the same type (DynamicPseudoType) + // as the echo "data" attribute. + newVal := tftypes.NewValue(echoTestSchema.ValueType(), map[string]tftypes.Value{ + "data": e.providerConfigData, + }) + + newState, diag := valuetoDynamicValue(echoTestSchema, newVal) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + resp.NewState = newState + + return resp, nil +} + +func (e *echoProviderServer) CallFunction(ctx context.Context, req *tfprotov6.CallFunctionRequest) (*tfprotov6.CallFunctionResponse, error) { + return &tfprotov6.CallFunctionResponse{}, nil +} + +func (e *echoProviderServer) ConfigureProvider(ctx context.Context, req *tfprotov6.ConfigureProviderRequest) (*tfprotov6.ConfigureProviderResponse, error) { + resp := &tfprotov6.ConfigureProviderResponse{} + + configVal, diags := dynamicValueToValue(e.providerSchema(), req.Config) + if diags != nil { + resp.Diagnostics = append(resp.Diagnostics, diags) + return resp, nil + } + + objVal := map[string]tftypes.Value{} + err := configVal.As(&objVal) + if err != nil { + diag := &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Error reading Config", + Detail: err.Error(), + } + resp.Diagnostics = append(resp.Diagnostics, diag) + return resp, nil //nolint:nilerr // error via diagnostic, not gRPC + } + + dynamicDataVal, ok := objVal["data"] + if !ok { + diag := &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: `Attribute "data" not found in config`, + } + resp.Diagnostics = append(resp.Diagnostics, diag) + return resp, nil //nolint:nilerr // error via diagnostic, not gRPC + } + + e.providerConfigData = dynamicDataVal.Copy() + + return resp, nil +} + +func (e *echoProviderServer) GetFunctions(ctx context.Context, req *tfprotov6.GetFunctionsRequest) (*tfprotov6.GetFunctionsResponse, error) { + return &tfprotov6.GetFunctionsResponse{}, nil +} + +func (e *echoProviderServer) GetMetadata(ctx context.Context, req *tfprotov6.GetMetadataRequest) (*tfprotov6.GetMetadataResponse, error) { + return &tfprotov6.GetMetadataResponse{ + Resources: []tfprotov6.ResourceMetadata{ + { + TypeName: echoResourceType, + }, + }, + }, nil +} + +func (e *echoProviderServer) GetProviderSchema(ctx context.Context, req *tfprotov6.GetProviderSchemaRequest) (*tfprotov6.GetProviderSchemaResponse, error) { + return &tfprotov6.GetProviderSchemaResponse{ + Provider: e.providerSchema(), + // MAINTAINER NOTE: This provider is only really built to support a single special resource type ("echo"). In the future, if we want + // to add more resource types to this provider, we'll likely need to refactor other RPCs in the provider server to handle that. + ResourceSchemas: map[string]*tfprotov6.Schema{ + echoResourceType: e.testResourceSchema(), + }, + }, nil +} + +func (e *echoProviderServer) ImportResourceState(ctx context.Context, req *tfprotov6.ImportResourceStateRequest) (*tfprotov6.ImportResourceStateResponse, error) { + return &tfprotov6.ImportResourceStateResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unsupported Resource Operation", + Detail: "ImportResourceState is not supported by this provider.", + }, + }, + }, nil +} + +func (e *echoProviderServer) MoveResourceState(ctx context.Context, req *tfprotov6.MoveResourceStateRequest) (*tfprotov6.MoveResourceStateResponse, error) { + return &tfprotov6.MoveResourceStateResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unsupported Resource Operation", + Detail: "MoveResourceState is not supported by this provider.", + }, + }, + }, nil +} + +func (e *echoProviderServer) PlanResourceChange(ctx context.Context, req *tfprotov6.PlanResourceChangeRequest) (*tfprotov6.PlanResourceChangeResponse, error) { + resp := &tfprotov6.PlanResourceChangeResponse{} + + if req.TypeName != echoResourceType { + resp.Diagnostics = []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unsupported Resource", + Detail: fmt.Sprintf("PlanResourceChange was called for a resource type that is not supported by this provider: %q", req.TypeName), + }, + } + + return resp, nil + } + + echoTestSchema := e.testResourceSchema() + priorState, diag := dynamicValueToValue(echoTestSchema, req.PriorState) + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + proposedNewState, diag := dynamicValueToValue(echoTestSchema, req.ProposedNewState) + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + // Destroying the resource, just return proposed new state (which is null) + if proposedNewState.IsNull() { + return &tfprotov6.PlanResourceChangeResponse{ + PlannedState: req.ProposedNewState, + }, nil + } + + // If the echo resource has prior state, don't plan anything new as it's valid for the ephemeral data to change + // between operations and we don't want to produce constant diffs. This resource is only for testing data, which a + // single plan/apply should suffice. + if !priorState.IsNull() { + return &tfprotov6.PlanResourceChangeResponse{ + PlannedState: req.PriorState, + }, nil + } + + // If we are creating, mark data as unknown in the plan. + // + // We can't set the proposed new state to the provider config data because it could change between plan/apply (provider config is ephemeral). + unknownVal := tftypes.NewValue(echoTestSchema.ValueType(), map[string]tftypes.Value{ + "data": tftypes.NewValue(tftypes.DynamicPseudoType, tftypes.UnknownValue), + }) + + plannedState, diag := valuetoDynamicValue(echoTestSchema, unknownVal) + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + resp.PlannedState = plannedState + + return resp, nil +} + +func (e *echoProviderServer) ReadDataSource(ctx context.Context, req *tfprotov6.ReadDataSourceRequest) (*tfprotov6.ReadDataSourceResponse, error) { + return &tfprotov6.ReadDataSourceResponse{}, nil +} + +func (e *echoProviderServer) ReadResource(ctx context.Context, req *tfprotov6.ReadResourceRequest) (*tfprotov6.ReadResourceResponse, error) { + // Just return current state, since the data doesn't need to be refreshed. + return &tfprotov6.ReadResourceResponse{ + NewState: req.CurrentState, + }, nil +} + +func (e *echoProviderServer) StopProvider(ctx context.Context, req *tfprotov6.StopProviderRequest) (*tfprotov6.StopProviderResponse, error) { + return &tfprotov6.StopProviderResponse{}, nil +} + +func (e *echoProviderServer) UpgradeResourceState(ctx context.Context, req *tfprotov6.UpgradeResourceStateRequest) (*tfprotov6.UpgradeResourceStateResponse, error) { + resp := &tfprotov6.UpgradeResourceStateResponse{} + + if req.TypeName != echoResourceType { + resp.Diagnostics = []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unsupported Resource", + Detail: fmt.Sprintf("UpgradeResourceState was called for a resource type that is not supported by this provider: %q", req.TypeName), + }, + } + + return resp, nil + } + + // Define options to be used when unmarshalling raw state. + // IgnoreUndefinedAttributes will silently skip over fields in the JSON + // that do not have a matching entry in the schema. + unmarshalOpts := tfprotov6.UnmarshalOpts{ + ValueFromJSONOpts: tftypes.ValueFromJSONOpts{ + IgnoreUndefinedAttributes: true, + }, + } + + providerSchema := e.providerSchema() + + if req.Version != providerSchema.Version { + resp.Diagnostics = []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unsupported Resource", + Detail: "UpgradeResourceState was called for echo, which does not support multiple schema versions", + }, + } + + return resp, nil + } + + // Terraform CLI can call UpgradeResourceState even if the stored state + // version matches the current schema. Presumably this is to account for + // the previous terraform-plugin-sdk implementation, which handled some + // state fixups on behalf of Terraform CLI. This will attempt to roundtrip + // the prior RawState to a state matching the current schema. + rawStateValue, err := req.RawState.UnmarshalWithOpts(providerSchema.ValueType(), unmarshalOpts) + + if err != nil { + diag := &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unable to Read Previously Saved State for UpgradeResourceState", + Detail: "There was an error reading the saved resource state using the current resource schema: " + err.Error(), + } + + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil //nolint:nilerr // error via diagnostic, not gRPC + } + + upgradedState, diag := valuetoDynamicValue(providerSchema, rawStateValue) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + resp.UpgradedState = upgradedState + + return resp, nil +} + +func (e *echoProviderServer) ValidateDataResourceConfig(ctx context.Context, req *tfprotov6.ValidateDataResourceConfigRequest) (*tfprotov6.ValidateDataResourceConfigResponse, error) { + return &tfprotov6.ValidateDataResourceConfigResponse{}, nil +} + +func (e *echoProviderServer) ValidateProviderConfig(ctx context.Context, req *tfprotov6.ValidateProviderConfigRequest) (*tfprotov6.ValidateProviderConfigResponse, error) { + return &tfprotov6.ValidateProviderConfigResponse{}, nil +} + +func (e *echoProviderServer) ValidateResourceConfig(ctx context.Context, req *tfprotov6.ValidateResourceConfigRequest) (*tfprotov6.ValidateResourceConfigResponse, error) { + return &tfprotov6.ValidateResourceConfigResponse{}, nil +} + +func (e *echoProviderServer) OpenEphemeralResource(ctx context.Context, req *tfprotov6.OpenEphemeralResourceRequest) (*tfprotov6.OpenEphemeralResourceResponse, error) { + return &tfprotov6.OpenEphemeralResourceResponse{}, nil +} + +func (e *echoProviderServer) RenewEphemeralResource(ctx context.Context, req *tfprotov6.RenewEphemeralResourceRequest) (*tfprotov6.RenewEphemeralResourceResponse, error) { + return &tfprotov6.RenewEphemeralResourceResponse{}, nil +} + +func (e *echoProviderServer) CloseEphemeralResource(ctx context.Context, req *tfprotov6.CloseEphemeralResourceRequest) (*tfprotov6.CloseEphemeralResourceResponse, error) { + return &tfprotov6.CloseEphemeralResourceResponse{}, nil +} + +func (e *echoProviderServer) ValidateEphemeralResourceConfig(ctx context.Context, req *tfprotov6.ValidateEphemeralResourceConfigRequest) (*tfprotov6.ValidateEphemeralResourceConfigResponse, error) { + return &tfprotov6.ValidateEphemeralResourceConfigResponse{}, nil +} + +func (e *echoProviderServer) GetResourceIdentitySchemas(context.Context, *tfprotov6.GetResourceIdentitySchemasRequest) (*tfprotov6.GetResourceIdentitySchemasResponse, error) { + return &tfprotov6.GetResourceIdentitySchemasResponse{}, nil +} + +func (e *echoProviderServer) UpgradeResourceIdentity(context.Context, *tfprotov6.UpgradeResourceIdentityRequest) (*tfprotov6.UpgradeResourceIdentityResponse, error) { + return &tfprotov6.UpgradeResourceIdentityResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unsupported UpgradeResourceIdentity Operation", + Detail: "Resource Identity is not supported by this provider.", + }, + }, + }, nil +} diff --git a/echoprovider/server_test.go b/echoprovider/server_test.go new file mode 100644 index 000000000..bbc6dd656 --- /dev/null +++ b/echoprovider/server_test.go @@ -0,0 +1,283 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package echoprovider_test + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-testing/echoprovider" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestEchoProviderServer_primitive(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // echo provider is protocol version 6 + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "echo": echoprovider.NewProviderServer(), + }, + Steps: []resource.TestStep{ + { + Config: ` + provider "echo" { + data = "hello world" + } + resource "echo" "test_one" {} + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownValue("echo.test_one", tfjsonpath.New("data")), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("echo.test_one", tfjsonpath.New("data"), knownvalue.StringExact("hello world")), + }, + }, + { + Config: ` + provider "echo" { + data = 200 + } + resource "echo" "test_two" {} + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownValue("echo.test_two", tfjsonpath.New("data")), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("echo.test_two", tfjsonpath.New("data"), knownvalue.Int64Exact(200)), + }, + }, + { + Config: ` + provider "echo" { + data = true + } + resource "echo" "test_three" {} + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownValue("echo.test_three", tfjsonpath.New("data")), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("echo.test_three", tfjsonpath.New("data"), knownvalue.Bool(true)), + }, + }, + }, + }) +} + +func TestEchoProviderServer_complex(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // echo provider is protocol version 6 + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "echo": echoprovider.NewProviderServer(), + }, + Steps: []resource.TestStep{ + { + Config: ` + provider "echo" { + data = tolist(["hello", "world"]) + } + resource "echo" "test_one" {} + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownValue("echo.test_one", tfjsonpath.New("data")), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("echo.test_one", tfjsonpath.New("data"), + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("hello"), + knownvalue.StringExact("world"), + }), + ), + }, + }, + { + Config: ` + provider "echo" { + data = tomap({"key1": "hello", "key2": "world"}) + } + resource "echo" "test_two" {} + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownValue("echo.test_two", tfjsonpath.New("data")), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("echo.test_two", tfjsonpath.New("data"), + knownvalue.MapExact(map[string]knownvalue.Check{ + "key1": knownvalue.StringExact("hello"), + "key2": knownvalue.StringExact("world"), + }), + ), + }, + }, + { + Config: ` + provider "echo" { + data = tomap({"key1": "hello", "key2": "world"}) + } + resource "echo" "test_two" {} + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + }, + }, + }, + }, + }) +} + +func TestEchoProviderServer_null(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // echo provider is protocol version 6 + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "echo": echoprovider.NewProviderServer(), + }, + Steps: []resource.TestStep{ + { + Config: ` + provider "echo" {} + resource "echo" "test_one" {} + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownValue("echo.test_one", tfjsonpath.New("data")), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("echo.test_one", tfjsonpath.New("data"), knownvalue.Null()), + }, + }, + }, + }) +} + +func TestEchoProviderServer_unknown(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // echo provider is protocol version 6 + }, + ExternalProviders: map[string]resource.ExternalProvider{ + "random": { + Source: "hashicorp/random", + }, + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "echo": echoprovider.NewProviderServer(), + }, + Steps: []resource.TestStep{ + { + Config: ` + resource "random_string" "str" { + length = 12 + } + provider "echo" { + data = random_string.str.result + } + resource "echo" "test_one" {} + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownValue("echo.test_one", tfjsonpath.New("data")), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("echo.test_one", tfjsonpath.New("data"), knownvalue.StringRegexp(regexp.MustCompile(`\S{12}`))), + }, + }, + { + Config: ` + resource "random_string" "str" { + length = 12 + } + provider "echo" { + data = random_string.str.result + } + resource "echo" "test_one" {} + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + }, + }, + }, + }, + }) +} + +func TestEchoProviderServer_immutable(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // echo provider is protocol version 6 + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "echo": echoprovider.NewProviderServer(), + }, + Steps: []resource.TestStep{ + { + Config: ` + provider "echo" { + data = "original value" + } + resource "echo" "test_one" {} + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownValue("echo.test_one", tfjsonpath.New("data")), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("echo.test_one", tfjsonpath.New("data"), knownvalue.StringExact("original value")), + }, + }, + { + // Despite the provider config data changing, the "echo.test_one" resource will never change as it's immutable. + Config: ` + provider "echo" { + data = ["tuple", "of", "values"] + } + resource "echo" "test_one" {} + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("echo.test_one", tfjsonpath.New("data"), knownvalue.StringExact("original value")), + }, + }, + }, + }) +} diff --git a/echoprovider/tftypes.go b/echoprovider/tftypes.go new file mode 100644 index 000000000..54e160a3c --- /dev/null +++ b/echoprovider/tftypes.go @@ -0,0 +1,64 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package echoprovider + +import ( + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func valuetoDynamicValue(schema *tfprotov6.Schema, value tftypes.Value) (*tfprotov6.DynamicValue, *tfprotov6.Diagnostic) { + if schema == nil { + diag := &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unable to Convert Value", + Detail: "Converting the Value to DynamicValue returned an unexpected error: missing schema", + } + + return nil, diag + } + + dynamicValue, err := tfprotov6.NewDynamicValue(schema.ValueType(), value) + if err != nil { + diag := &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unable to Convert Value", + Detail: "Converting the Value to DynamicValue returned an unexpected error: " + err.Error(), + } + + return &dynamicValue, diag + } + + return &dynamicValue, nil +} + +func dynamicValueToValue(schema *tfprotov6.Schema, dynamicValue *tfprotov6.DynamicValue) (tftypes.Value, *tfprotov6.Diagnostic) { + if schema == nil { + diag := &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unable to Convert DynamicValue", + Detail: "Converting the DynamicValue to Value returned an unexpected error: missing schema", + } + + return tftypes.NewValue(tftypes.Object{}, nil), diag + } + + if dynamicValue == nil { + return tftypes.NewValue(schema.ValueType(), nil), nil + } + + value, err := dynamicValue.Unmarshal(schema.ValueType()) + + if err != nil { + diag := &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unable to Convert DynamicValue", + Detail: "Converting the DynamicValue to Value returned an unexpected error: " + err.Error(), + } + + return value, diag + } + + return value, nil +} diff --git a/go.mod b/go.mod index a1748f870..d0f93f92d 100644 --- a/go.mod +++ b/go.mod @@ -1,57 +1,60 @@ module github.com/hashicorp/terraform-plugin-testing -go 1.19 +go 1.24.0 require ( - github.com/google/go-cmp v0.5.9 - github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 - github.com/hashicorp/go-hclog v1.5.0 - github.com/hashicorp/go-multierror v1.1.1 + github.com/google/go-cmp v0.7.0 + github.com/hashicorp/go-cty v1.5.0 + github.com/hashicorp/go-hclog v1.6.3 github.com/hashicorp/go-uuid v1.0.3 - github.com/hashicorp/go-version v1.6.0 - github.com/hashicorp/hc-install v0.5.2 - github.com/hashicorp/hcl/v2 v2.17.0 + github.com/hashicorp/go-version v1.7.0 + github.com/hashicorp/hc-install v0.9.2 + github.com/hashicorp/hcl/v2 v2.24.0 github.com/hashicorp/logutils v1.0.0 - github.com/hashicorp/terraform-exec v0.18.1 - github.com/hashicorp/terraform-json v0.17.0 - github.com/hashicorp/terraform-plugin-go v0.16.0 + github.com/hashicorp/terraform-exec v0.24.0 + github.com/hashicorp/terraform-json v0.27.2 + github.com/hashicorp/terraform-plugin-go v0.29.0 github.com/hashicorp/terraform-plugin-log v0.9.0 - github.com/hashicorp/terraform-plugin-sdk/v2 v2.26.1 - github.com/mitchellh/copystructure v1.2.0 + github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1 github.com/mitchellh/go-testing-interface v1.14.1 - github.com/mitchellh/reflectwalk v1.0.2 - github.com/zclconf/go-cty v1.13.2 - golang.org/x/crypto v0.10.0 + github.com/zclconf/go-cty v1.17.0 + golang.org/x/crypto v0.42.0 ) require ( - github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect + github.com/ProtonMail/go-crypto v1.1.6 // indirect github.com/agext/levenshtein v1.2.2 // indirect - github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect - github.com/cloudflare/circl v1.3.3 // indirect - github.com/fatih/color v1.13.0 // indirect - github.com/golang/protobuf v1.5.3 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/cloudflare/circl v1.6.1 // indirect + github.com/fatih/color v1.16.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-checkpoint v0.5.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-plugin v1.4.10 // indirect - github.com/hashicorp/terraform-registry-address v0.2.1 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-plugin v1.7.0 // indirect + github.com/hashicorp/go-retryablehttp v0.7.7 // indirect + github.com/hashicorp/terraform-registry-address v0.4.0 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect - github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect - github.com/mattn/go-colorable v0.1.12 // indirect - github.com/mattn/go-isatty v0.0.14 // indirect - github.com/mitchellh/go-wordwrap v1.0.0 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/oklog/run v1.0.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/oklog/run v1.1.0 // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect - github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - golang.org/x/mod v0.10.0 // indirect - golang.org/x/net v0.11.0 // indirect - golang.org/x/sys v0.9.0 // indirect - golang.org/x/text v0.10.0 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect - google.golang.org/grpc v1.56.0 // indirect - google.golang.org/protobuf v1.30.0 // indirect + golang.org/x/mod v0.27.0 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/text v0.29.0 // indirect + golang.org/x/tools v0.36.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect + google.golang.org/grpc v1.75.1 // indirect + google.golang.org/protobuf v1.36.9 // indirect ) diff --git a/go.sum b/go.sum index cf232418c..fc1f35b5e 100644 --- a/go.sum +++ b/go.sum @@ -1,35 +1,54 @@ -github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= -github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA= -github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= -github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= +github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= -github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= -github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= -github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= -github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= -github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= +github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= -github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= -github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4= -github.com/go-git/go-git/v5 v5.6.1 h1:q4ZRqQl4pR/ZJHc1L5CFjGA1a10u76aV1iC+nh+bHsk= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= +github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= +github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60= +github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= +github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-checkpoint v0.5.0 h1:MFYpPZCnQqQTE18jFwSII6eUQrD/oxMFp3mlgcqk5mU= @@ -37,100 +56,135 @@ github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuD github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 h1:1/D3zfFHttUKaCaGKZ/dR2roBXv0vKbSCnssIldfQdI= -github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320/go.mod h1:EiZBMaudVLy8fmjf9Npq1dq9RalhveqZG5w/yz3mHWs= -github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= -github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-cty v1.5.0 h1:EkQ/v+dDNUqnuVpmS5fPqyY71NXVgT5gf32+57xY8g0= +github.com/hashicorp/go-cty v1.5.0/go.mod h1:lFUCG5kd8exDobgSfyj4ONE/dc822kiYMguVKdHGMLM= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-plugin v1.4.10 h1:xUbmA4jC6Dq163/fWcp8P3JuHilrHHMLNRxzGQJ9hNk= -github.com/hashicorp/go-plugin v1.4.10/go.mod h1:6/1TEzT0eQznvI/gV2CM29DLSkAK/e58mUWKVsPaph0= +github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA= +github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= +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.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= -github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/hc-install v0.5.2 h1:SfwMFnEXVVirpwkDuSF5kymUOhrUxrTq3udEseZdOD0= -github.com/hashicorp/hc-install v0.5.2/go.mod h1:9QISwe6newMWIfEiXpzuu1k9HAGtQYgnSH8H9T8wmoI= -github.com/hashicorp/hcl/v2 v2.17.0 h1:z1XvSUyXd1HP10U4lrLg5e0JMVz6CPaJvAgxM0KNZVY= -github.com/hashicorp/hcl/v2 v2.17.0/go.mod h1:gJyW2PTShkJqQBKpAmPO3yxMxIuoXkOF2TpqXzrQyx4= +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.9.2 h1:v80EtNX4fCVHqzL9Lg/2xkp62bbvQMnvPQ0G+OmtO24= +github.com/hashicorp/hc-install v0.9.2/go.mod h1:XUqBQNnuT4RsxoxiM9ZaUk0NX8hi2h+Lb6/c0OZnC/I= +github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE= +github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/terraform-exec v0.18.1 h1:LAbfDvNQU1l0NOQlTuudjczVhHj061fNX5H8XZxHlH4= -github.com/hashicorp/terraform-exec v0.18.1/go.mod h1:58wg4IeuAJ6LVsLUeD2DWZZoc/bYi6dzhLHzxM41980= -github.com/hashicorp/terraform-json v0.17.0 h1:EiA1Wp07nknYQAiv+jIt4dX4Cq5crgP+TsTE45MjMmM= -github.com/hashicorp/terraform-json v0.17.0/go.mod h1:Huy6zt6euxaY9knPAFKjUITn8QxUFIe9VuSzb4zn/0o= -github.com/hashicorp/terraform-plugin-go v0.16.0 h1:DSOQ0rz5FUiVO4NUzMs8ln9gsPgHMTsfns7Nk+6gPuE= -github.com/hashicorp/terraform-plugin-go v0.16.0/go.mod h1:4sn8bFuDbt+2+Yztt35IbOrvZc0zyEi87gJzsTgCES8= +github.com/hashicorp/terraform-exec v0.24.0 h1:mL0xlk9H5g2bn0pPF6JQZk5YlByqSqrO5VoaNtAf8OE= +github.com/hashicorp/terraform-exec v0.24.0/go.mod h1:lluc/rDYfAhYdslLJQg3J0oDqo88oGQAdHR+wDqFvo4= +github.com/hashicorp/terraform-json v0.27.2 h1:BwGuzM6iUPqf9JYM/Z4AF1OJ5VVJEEzoKST/tRDBJKU= +github.com/hashicorp/terraform-json v0.27.2/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE= +github.com/hashicorp/terraform-plugin-go v0.29.0 h1:1nXKl/nSpaYIUBU1IG/EsDOX0vv+9JxAltQyDMpq5mU= +github.com/hashicorp/terraform-plugin-go v0.29.0/go.mod h1:vYZbIyvxyy0FWSmDHChCqKvI40cFTDGSb3D8D70i9GM= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.26.1 h1:G9WAfb8LHeCxu7Ae8nc1agZlQOSCUWsb610iAogBhCs= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.26.1/go.mod h1:xcOSYlRVdPLmDUoqPhO9fiO/YCN/l6MGYeTzGt5jgkQ= -github.com/hashicorp/terraform-registry-address v0.2.1 h1:QuTf6oJ1+WSflJw6WYOHhLgwUiQ0FrROpHPYFtwTYWM= -github.com/hashicorp/terraform-registry-address v0.2.1/go.mod h1:BSE9fIFzp0qWsJUUyGquo4ldV9k2n+psif6NYkBRS3Y= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1 h1:mlAq/OrMlg04IuJT7NpefI1wwtdpWudnEmjuQs04t/4= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1/go.mod h1:GQhpKVvvuwzD79e8/NZ+xzj+ZpWovdPAe8nfV/skwNU= +github.com/hashicorp/terraform-registry-address v0.4.0 h1:S1yCGomj30Sao4l5BMPjTGZmCNzuv7/GDTDX99E9gTk= +github.com/hashicorp/terraform-registry-address v0.4.0/go.mod h1:LRS1Ay0+mAiRkUyltGT+UHWkIqTFvigGn/LbMshfflE= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= -github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ= -github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= -github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= -github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= +github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= -github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= -github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= -github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= -github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= +github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= -github.com/skeema/knownhosts v1.1.0 h1:Wvr9V0MxhjRbl3f9nMnKnFfiWTJmtECJ9Njkea3ysW0= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= -github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= -github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= -github.com/zclconf/go-cty v1.13.2 h1:4GvrUxe/QUDYuJKAav4EYqdM47/kZa672LwmXFmEKT0= -github.com/zclconf/go-cty v1.13.2/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= +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.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0= +github.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U= +github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= +github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= 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.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= -golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= -golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= -golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +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= -golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= -golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= 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.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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= @@ -138,34 +192,48 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= 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.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= -golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +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.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= 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= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= -google.golang.org/grpc v1.56.0 h1:+y7Bs8rtMd07LeXmL3NxcTLn7mUkbKZqEpPhMNkwJEE= -google.golang.org/grpc v1.56.0/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= +google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/helper/acctest/random.go b/helper/acctest/random.go index abb778aa0..102c85ddd 100644 --- a/helper/acctest/random.go +++ b/helper/acctest/random.go @@ -20,10 +20,6 @@ import ( "golang.org/x/crypto/ssh" ) -func init() { - rand.Seed(time.Now().UTC().UnixNano()) -} - // Helpers for generating random tidbits for use in identifiers to prevent // collisions in acceptance tests. @@ -38,9 +34,9 @@ func RandomWithPrefix(name string) string { return fmt.Sprintf("%s-%d", name, RandInt()) } -// RandIntRange returns a random integer between min (inclusive) and max (exclusive) -func RandIntRange(min int, max int) int { - return rand.Intn(max-min) + min +// RandIntRange returns a random integer between minInt (inclusive) and maxInt (exclusive) +func RandIntRange(minInt int, maxInt int) int { + return rand.Intn(maxInt-minInt) + minInt } // RandString generates a random alphanumeric string of the length specified @@ -126,10 +122,8 @@ func RandTLSCert(orgName string) (string, string, error) { } // RandIpAddress returns a random IP address in the specified CIDR block. -// The prefix length must be less than 31. func RandIpAddress(s string) (string, error) { prefix, err := netip.ParsePrefix(s) - if err != nil { return "", err } @@ -138,47 +132,36 @@ func RandIpAddress(s string) (string, error) { return prefix.Addr().String(), nil } - prefixSizeExponent := uint(prefix.Addr().BitLen() - prefix.Bits()) - - if prefix.Addr().Is4() && prefixSizeExponent > 31 { - return "", fmt.Errorf("CIDR range is too large: %d", prefixSizeExponent) + // base address as byte slice + prefixBytes, err := prefix.Masked().Addr().MarshalBinary() + if err != nil { + return "", err } - // Prevent panics with rand.Int63n(). - if prefix.Addr().Is6() && prefixSizeExponent > 63 { - return "", fmt.Errorf("CIDR range is too large: %d", prefixSizeExponent) + // inverse mask (ones in the host bits) as byte slice + inverseMaskBytes, err := inverseMask(prefix.Bits(), len(prefixBytes)) + if err != nil { + return "", err } - // Calculate max random integer based on the prefix. - // Bit shift 1< byteLen*8 { + return nil, fmt.Errorf("cannot fit a %d-bit mask into %d bytes", bits, byteLen) + } + + iBits := (byteLen * 8) - bits + var result []byte + for iBits > 0 { + b := uint8((1 << iBits) - 1) + result = append([]byte{b}, result...) + iBits -= 8 + } + + return append(make([]byte, byteLen-len(result)), result...), nil +} + const ( // CharSetAlphaNum is the alphanumeric character set for use with // RandStringFromCharSet diff --git a/helper/acctest/random_test.go b/helper/acctest/random_test.go index effbaa82c..ebeab89e3 100644 --- a/helper/acctest/random_test.go +++ b/helper/acctest/random_test.go @@ -7,6 +7,7 @@ import ( "crypto/rsa" "net/netip" "regexp" + "slices" "testing" "golang.org/x/crypto/ssh" @@ -29,6 +30,10 @@ func TestRandIpAddress(t *testing.T) { expected *regexp.Regexp expectedErr string }{ + { + s: "0.0.0.0/0", + expected: regexp.MustCompile(`^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$`), + }, { s: "1.1.1.1/32", expected: regexp.MustCompile(`^1\.1\.1\.1$`), @@ -38,8 +43,8 @@ func TestRandIpAddress(t *testing.T) { expected: regexp.MustCompile(`^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$`), }, { - s: "0.0.0.0/0", - expectedErr: "CIDR range is too large: 32", + s: "10.0.0.0/15", + expected: regexp.MustCompile(`^10\.[01]\.\d{1,3}\.\d{1,3}$`), }, { s: "449d:e5f1:14b1:ddf3:8525:7e9e:4a0d:4a82/128", @@ -49,10 +54,6 @@ func TestRandIpAddress(t *testing.T) { s: "2001:db8::/112", expected: regexp.MustCompile(`^2001:db8::[[:xdigit:]]{1,4}$`), }, - { - s: "2001:db8::/64", - expectedErr: "CIDR range is too large: 64", - }, { s: "abcdefg", expectedErr: "netip.ParsePrefix(\"abcdefg\"): no '/'", @@ -95,8 +96,6 @@ func TestRandSSHKeyPair(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -138,3 +137,76 @@ func TestRandSSHKeyPair(t *testing.T) { }) } } + +func TestInverseMask(t *testing.T) { + t.Parallel() + + type testCase struct { + prefixLen int + byteLen int + expected []byte + } + + testCases := map[string]testCase{ + "0-bit_ipv4": { + prefixLen: 0, + byteLen: 4, + expected: []byte{255, 255, 255, 255}, + }, + "7-bit_ipv4": { + prefixLen: 7, + byteLen: 4, + expected: []byte{1, 255, 255, 255}, + }, + "8-bit_ipv4": { + prefixLen: 8, + byteLen: 4, + expected: []byte{0, 255, 255, 255}, + }, + "9-bit_ipv4": { + prefixLen: 9, + byteLen: 4, + expected: []byte{0, 127, 255, 255}, + }, + "27-bit_ipv4": { + prefixLen: 27, + byteLen: 4, + expected: []byte{0, 0, 0, 31}, + }, + "32-bit_ipv4": { + prefixLen: 32, + byteLen: 4, + expected: []byte{0, 0, 0, 0}, + }, + "32-bit_ipv6": { + prefixLen: 32, + byteLen: 16, + expected: []byte{0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + }, + "64-bit_ipv6": { + prefixLen: 64, + byteLen: 16, + expected: []byte{0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255}, + }, + "128-bit_ipv6": { + prefixLen: 128, + byteLen: 16, + expected: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + }, + } + + for tName, tCase := range testCases { + t.Run(tName, func(t *testing.T) { + t.Parallel() + + result, err := inverseMask(tCase.prefixLen, tCase.byteLen) + if err != nil { + t.Fatal(err) + } + + if slices.Compare(tCase.expected, result) != 0 { + t.Fatalf("expected %v, got %v", tCase.expected, result) + } + }) + } +} diff --git a/helper/resource/additional_cli_options.go b/helper/resource/additional_cli_options.go new file mode 100644 index 000000000..36dc0f89a --- /dev/null +++ b/helper/resource/additional_cli_options.go @@ -0,0 +1,29 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package resource + +// AdditionalCLIOptions allows an intentionally limited set of options to be passed +// to the Terraform CLI when executing test steps. +type AdditionalCLIOptions struct { + // Apply represents options to be passed to the `terraform apply` command. + Apply ApplyOptions + + // Plan represents options to be passed to the `terraform plan` command. + Plan PlanOptions +} + +// ApplyOptions represents options to be passed to the `terraform apply` command. +type ApplyOptions struct { + // AllowDeferral will pass the experimental `-allow-deferral` flag to the apply command. + AllowDeferral bool +} + +// PlanOptions represents options to be passed to the `terraform plan` command. +type PlanOptions struct { + // AllowDeferral will pass the experimental `-allow-deferral` flag to the plan command. + AllowDeferral bool + + // NoRefresh will pass the `-refresh=false` flag to the plan command. + NoRefresh bool +} diff --git a/helper/resource/environment_variables.go b/helper/resource/environment_variables.go index 981908799..2f8967fc6 100644 --- a/helper/resource/environment_variables.go +++ b/helper/resource/environment_variables.go @@ -32,4 +32,14 @@ const ( // type Config field includes a provider source, such as the terraform // configuration block required_providers attribute. EnvTfAccProviderNamespace = "TF_ACC_PROVIDER_NAMESPACE" + + // This is an undocumented compatibility flag. When this is set, a + // `Config`-mode test step will invoke a refresh before successful + // completion. + // + // This is a compatibility measure for test cases that have different -- + // but semantically-equal -- state representations in their test steps. + // When comparing two states, the testing framework is not aware of + // semantic equality or set equality. + EnvTfAccRefreshAfterApply = "TF_ACC_REFRESH_AFTER_APPLY" ) diff --git a/helper/resource/importstate/examplecloud_test.go b/helper/resource/importstate/examplecloud_test.go new file mode 100644 index 000000000..bf1e2747d --- /dev/null +++ b/helper/resource/importstate/examplecloud_test.go @@ -0,0 +1,622 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package importstate_test + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/datasource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" +) + +func examplecloudDataSource() testprovider.DataSource { + return testprovider.DataSource{ + ReadResponse: &datasource.ReadResponse{ + State: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "datasource-test"), + }, + ), + }, + SchemaResponse: &datasource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + ComputedStringAttribute("id"), + }, + }, + }, + }, + } +} + +func examplecloudResource() testprovider.Resource { + return testprovider.Resource{ + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "somevalue"), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + }, + )), + }, + ReadResponse: &resource.ReadResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "somevalue"), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + }, + )), + }, + ImportStateResponse: &resource.ImportStateResponse{ + State: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "somevalue"), + }, + ), + Identity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + }, + )), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + ComputedStringAttribute("id"), + RequiredStringAttribute("location"), + RequiredStringAttribute("name"), + }, + }, + }, + }, + IdentitySchemaResponse: &resource.IdentitySchemaResponse{ + Schema: &tfprotov6.ResourceIdentitySchema{ + Version: 1, + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + RequiredForImport: true, + }, + }, + }, + }, + } +} + +// examplecloudZone is a test resource that mimics a DNS zone resource. +func examplecloudZone() testprovider.Resource { + value := tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "5381dd14-6d75-4f32-9096-47f5500b1507"), + "name": tftypes.NewValue(tftypes.String, "example.net"), + }, + ) + + return testprovider.Resource{ + CreateResponse: &resource.CreateResponse{ + NewState: value, + }, + ReadResponse: &resource.ReadResponse{ + NewState: value, + }, + ImportStateResponse: &resource.ImportStateResponse{ + State: value, + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + ComputedStringAttribute("id"), + RequiredStringAttribute("name"), + }, + }, + }, + }, + } +} + +// examplecloudZoneRecord is a test resource that mimics a DNS zone record resource. +// It models a resource dependency; specifically, it depends on a DNS zone ID and will +// plan a replacement if the zone ID changes. +func examplecloudZoneRecord() testprovider.Resource { + value := tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "zone_id": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "f00911be-e188-433d-9ccd-d0393a9f5d05"), + "zone_id": tftypes.NewValue(tftypes.String, "5381dd14-6d75-4f32-9096-47f5500b1507"), + "name": tftypes.NewValue(tftypes.String, "www"), + }, + ) + + return testprovider.Resource{ + CreateResponse: &resource.CreateResponse{ + NewState: value, + }, + PlanChangeFunc: func(ctx context.Context, req resource.PlanChangeRequest, resp *resource.PlanChangeResponse) { + resp.RequiresReplace = []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("zone_id"), + } + }, + ReadResponse: &resource.ReadResponse{ + NewState: value, + }, + ImportStateResponse: &resource.ImportStateResponse{ + State: value, + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + ComputedStringAttribute("id"), + RequiredStringAttribute("zone_id"), + RequiredStringAttribute("name"), + }, + }, + }, + }, + } +} + +func examplecloudResourceWithEveryIdentitySchemaType() testprovider.Resource { + return testprovider.Resource{ + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "hostname": tftypes.String, + "cabinet": tftypes.String, + "unit": tftypes.Number, + "active": tftypes.Bool, + "tags": tftypes.List{ElementType: tftypes.String}, + "magic_numbers": tftypes.List{ElementType: tftypes.Number}, + "beep_boop": tftypes.List{ElementType: tftypes.Bool}, + }, + }, + map[string]tftypes.Value{ + "hostname": tftypes.NewValue(tftypes.String, "mail.example.net"), + "cabinet": tftypes.NewValue(tftypes.String, "A1"), + "unit": tftypes.NewValue(tftypes.Number, 14), + "active": tftypes.NewValue(tftypes.Bool, true), + "tags": tftypes.NewValue( + tftypes.List{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "storage"), + tftypes.NewValue(tftypes.String, "fast")}), + "magic_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 5), + tftypes.NewValue(tftypes.Number, 2)}), + "beep_boop": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Bool}, []tftypes.Value{ + tftypes.NewValue(tftypes.Bool, false), + tftypes.NewValue(tftypes.Bool, true), + }), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "cabinet": tftypes.String, + "unit": tftypes.Number, + "active": tftypes.Bool, + "tags": tftypes.List{ElementType: tftypes.String}, + "magic_numbers": tftypes.List{ElementType: tftypes.Number}, + "beep_boop": tftypes.List{ElementType: tftypes.Bool}, + }, + }, + map[string]tftypes.Value{ + "cabinet": tftypes.NewValue(tftypes.String, "A1"), + "unit": tftypes.NewValue(tftypes.Number, 14), + "active": tftypes.NewValue(tftypes.Bool, true), + "tags": tftypes.NewValue( + tftypes.List{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "storage"), + tftypes.NewValue(tftypes.String, "fast"), + }), + "magic_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 5), + tftypes.NewValue(tftypes.Number, 2)}), + "beep_boop": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Bool}, []tftypes.Value{ + tftypes.NewValue(tftypes.Bool, false), + tftypes.NewValue(tftypes.Bool, true), + }), + }, + )), + }, + ReadResponse: &resource.ReadResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "hostname": tftypes.String, + "cabinet": tftypes.String, + "unit": tftypes.Number, + "active": tftypes.Bool, + "tags": tftypes.List{ElementType: tftypes.String}, + "magic_numbers": tftypes.List{ElementType: tftypes.Number}, + "beep_boop": tftypes.List{ElementType: tftypes.Bool}, + }, + }, + map[string]tftypes.Value{ + "hostname": tftypes.NewValue(tftypes.String, "mail.example.net"), + "cabinet": tftypes.NewValue(tftypes.String, "A1"), + "unit": tftypes.NewValue(tftypes.Number, 14), + "active": tftypes.NewValue(tftypes.Bool, true), + "tags": tftypes.NewValue( + tftypes.List{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "storage"), + tftypes.NewValue(tftypes.String, "fast")}), + "magic_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 5), + tftypes.NewValue(tftypes.Number, 2)}), + "beep_boop": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Bool}, []tftypes.Value{ + tftypes.NewValue(tftypes.Bool, false), + tftypes.NewValue(tftypes.Bool, true), + }), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "cabinet": tftypes.String, + "unit": tftypes.Number, + "active": tftypes.Bool, + "tags": tftypes.List{ElementType: tftypes.String}, + "magic_numbers": tftypes.List{ElementType: tftypes.Number}, + "beep_boop": tftypes.List{ElementType: tftypes.Bool}, + }, + }, + map[string]tftypes.Value{ + "cabinet": tftypes.NewValue(tftypes.String, "A1"), + "unit": tftypes.NewValue(tftypes.Number, 14), + "active": tftypes.NewValue(tftypes.Bool, true), + "tags": tftypes.NewValue( + tftypes.List{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "storage"), + tftypes.NewValue(tftypes.String, "fast")}), + "magic_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 5), + tftypes.NewValue(tftypes.Number, 2)}), + "beep_boop": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Bool}, []tftypes.Value{ + tftypes.NewValue(tftypes.Bool, false), + tftypes.NewValue(tftypes.Bool, true), + }), + }, + )), + }, + ImportStateResponse: &resource.ImportStateResponse{ + State: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "hostname": tftypes.String, + "cabinet": tftypes.String, + "unit": tftypes.Number, + "active": tftypes.Bool, + "tags": tftypes.List{ElementType: tftypes.String}, + "magic_numbers": tftypes.List{ElementType: tftypes.Number}, + "beep_boop": tftypes.List{ElementType: tftypes.Bool}, + }, + }, + map[string]tftypes.Value{ + "hostname": tftypes.NewValue(tftypes.String, "mail.example.net"), + "cabinet": tftypes.NewValue(tftypes.String, "A1"), + "unit": tftypes.NewValue(tftypes.Number, 14), + "active": tftypes.NewValue(tftypes.Bool, true), + "tags": tftypes.NewValue( + tftypes.List{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "storage"), + tftypes.NewValue(tftypes.String, "fast")}), + "magic_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 5), + tftypes.NewValue(tftypes.Number, 2)}), + "beep_boop": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Bool}, []tftypes.Value{ + tftypes.NewValue(tftypes.Bool, false), + tftypes.NewValue(tftypes.Bool, true), + }), + }, + ), + Identity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "cabinet": tftypes.String, + "unit": tftypes.Number, + "active": tftypes.Bool, + "tags": tftypes.List{ElementType: tftypes.String}, + "magic_numbers": tftypes.List{ElementType: tftypes.Number}, + "beep_boop": tftypes.List{ElementType: tftypes.Bool}, + }, + }, + map[string]tftypes.Value{ + "cabinet": tftypes.NewValue(tftypes.String, "A1"), + "unit": tftypes.NewValue(tftypes.Number, 14), + "active": tftypes.NewValue(tftypes.Bool, true), + "tags": tftypes.NewValue( + tftypes.List{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "storage"), + tftypes.NewValue(tftypes.String, "fast")}), + "magic_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 5), + tftypes.NewValue(tftypes.Number, 2)}), + "beep_boop": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Bool}, []tftypes.Value{ + tftypes.NewValue(tftypes.Bool, false), + tftypes.NewValue(tftypes.Bool, true), + }), + }, + )), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + ComputedStringAttribute("hostname"), + RequiredStringAttribute("cabinet"), + RequiredNumberAttribute("unit"), + RequiredBoolAttribute("active"), + RequiredListAttribute("tags", tftypes.String), + OptionalComputedListAttribute("magic_numbers", tftypes.Number), + }, + }, + }, + }, + IdentitySchemaResponse: &resource.IdentitySchemaResponse{ + Schema: &tfprotov6.ResourceIdentitySchema{ + Version: 1, + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "cabinet", + Type: tftypes.String, + RequiredForImport: true, + }, + { + Name: "unit", + Type: tftypes.Number, + OptionalForImport: true, + }, + { + Name: "active", + Type: tftypes.Bool, + OptionalForImport: true, + }, + { + Name: "tags", + Type: tftypes.List{ + ElementType: tftypes.String, + }, + OptionalForImport: true, + }, + { + Name: "magic_numbers", + Type: tftypes.List{ + ElementType: tftypes.Number, + }, + OptionalForImport: true, + }, + }, + }, + }, + } +} + +func examplecloudResourceWithNullIdentityAttr() testprovider.Resource { + return testprovider.Resource{ + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "somevalue"), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "value_we_dont_always_need": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "value_we_dont_always_need": tftypes.NewValue(tftypes.String, nil), + }, + )), + }, + ReadResponse: &resource.ReadResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "somevalue"), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "value_we_dont_always_need": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "value_we_dont_always_need": tftypes.NewValue(tftypes.String, nil), + }, + )), + }, + ImportStateResponse: &resource.ImportStateResponse{ + State: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "somevalue"), + }, + ), + Identity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "value_we_dont_always_need": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "value_we_dont_always_need": tftypes.NewValue(tftypes.String, nil), + }, + )), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + ComputedStringAttribute("id"), + RequiredStringAttribute("location"), + RequiredStringAttribute("name"), + }, + }, + }, + }, + IdentitySchemaResponse: &resource.IdentitySchemaResponse{ + Schema: &tfprotov6.ResourceIdentitySchema{ + Version: 1, + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + RequiredForImport: true, + }, + { + Name: "value_we_dont_always_need", + Type: tftypes.String, + OptionalForImport: true, + }, + }, + }, + }, + } +} + +// This example resource, on update plans, will plan a different identity to test that +// our testing framework assertions catch an identity that differs after import/refresh. +func examplecloudResourceWithChangingIdentity() testprovider.Resource { + exampleCloudResource := examplecloudResource() + + exampleCloudResource.PlanChangeFunc = func(ctx context.Context, req resource.PlanChangeRequest, resp *resource.PlanChangeResponse) { + // Only on update + if !req.PriorState.IsNull() && !req.ProposedNewState.IsNull() { + resp.PlannedIdentity = teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "easteurope/someothervalue"), + }, + )) + } + } + + return exampleCloudResource +} diff --git a/helper/resource/importstate/import_block_as_first_step_test.go b/helper/resource/importstate/import_block_as_first_step_test.go new file mode 100644 index 000000000..e7dacef9f --- /dev/null +++ b/helper/resource/importstate/import_block_as_first_step_test.go @@ -0,0 +1,63 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package importstate_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestImportBlock_AsFirstStep(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_5_0), // ImportBlockWithID requires Terraform 1.5.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + { + ResourceName: "examplecloud_container.test", + ImportStateId: "examplecloud_container.test", + ImportState: true, + ImportStateKind: r.ImportBlockWithID, + Config: `resource "examplecloud_container" "test" { + name = "somevalue" + location = "westeurope" + } + + import { + to = examplecloud_container.test + id = "westeurope/somevalue" + } + `, + ImportStateConfigExact: true, + ImportPlanChecks: r.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("examplecloud_container.test", plancheck.ResourceActionNoop), + plancheck.ExpectKnownValue("examplecloud_container.test", tfjsonpath.New("id"), knownvalue.StringExact("westeurope/somevalue")), + plancheck.ExpectKnownValue("examplecloud_container.test", tfjsonpath.New("name"), knownvalue.StringExact("somevalue")), + plancheck.ExpectKnownValue("examplecloud_container.test", tfjsonpath.New("location"), knownvalue.StringExact("westeurope")), + }, + }, + }, + }, + }) +} diff --git a/helper/resource/importstate/import_block_for_resource_with_a_dependency_test.go b/helper/resource/importstate/import_block_for_resource_with_a_dependency_test.go new file mode 100644 index 000000000..2464929dd --- /dev/null +++ b/helper/resource/importstate/import_block_for_resource_with_a_dependency_test.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package importstate_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestImportBlockForResourceWithADependency(t *testing.T) { + t.Parallel() + + config := ` +resource "examplecloud_zone" "zone" { + name = "example.net" +} + +resource "examplecloud_zone_record" "record" { + zone_id = examplecloud_zone.zone.id + name = "www" +} +` + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_5_0), // ImportBlockWithID requires Terraform 1.5.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_zone": examplecloudZone(), + "examplecloud_zone_record": examplecloudZoneRecord(), + }, + }), + }, + Steps: []r.TestStep{ + { + Config: config, + }, + { + ImportState: true, + ImportStateKind: r.ImportBlockWithID, + ResourceName: "examplecloud_zone_record.record", + }, + }, + }) +} diff --git a/helper/resource/importstate/import_block_in_config_directory_test.go b/helper/resource/importstate/import_block_in_config_directory_test.go new file mode 100644 index 000000000..d298808e5 --- /dev/null +++ b/helper/resource/importstate/import_block_in_config_directory_test.go @@ -0,0 +1,76 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package importstate_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-testing/config" + "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/tfversion" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestImportBlock_InConfigDirectory(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_5_0), // ImportBlockWithID requires Terraform 1.5.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + { + ConfigDirectory: config.StaticDirectory(`testdata/1`), + }, + { + ResourceName: "examplecloud_container.test", + ImportState: true, + ImportStateKind: r.ImportBlockWithID, + }, + }, + }) +} + +func TestImportBlock_InConfigDirectory_ConfigExactTrue(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_5_0), // ImportBlockWithID requires Terraform 1.5.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + { + ConfigDirectory: config.StaticDirectory(`testdata/1`), + }, + { + ResourceName: "examplecloud_container.test", + ImportState: true, + ImportStateKind: r.ImportBlockWithID, + + // This content includes an import block with an ID so we will + // use the exact content + ConfigDirectory: config.StaticDirectory(`testdata/2_with_exact_import_config`), + ImportStateConfigExact: true, + }, + }, + }) +} diff --git a/helper/resource/importstate/import_block_in_config_file_test.go b/helper/resource/importstate/import_block_in_config_file_test.go new file mode 100644 index 000000000..7cfa7e1d3 --- /dev/null +++ b/helper/resource/importstate/import_block_in_config_file_test.go @@ -0,0 +1,104 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package importstate_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-testing/config" + "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/tfversion" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestImportBlock_InConfigFile(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_5_0), // ImportBlockWithID requires Terraform 1.5.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + { + ConfigFile: config.StaticFile(`testdata/1/examplecloud_container.tf`), + }, + { + ResourceName: "examplecloud_container.test", + ImportState: true, + ImportStateKind: r.ImportBlockWithID, + }, + }, + }) +} + +func TestImportBlock_WithResourceIdentity_InConfigFile(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), // ImportBlockWithResourceIdentity requires Terraform 1.12.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + { + ConfigFile: config.StaticFile(`testdata/1/examplecloud_container.tf`), + }, + { + ResourceName: "examplecloud_container.test", + ImportState: true, + ImportStateKind: r.ImportBlockWithResourceIdentity, + ConfigFile: config.StaticFile(`testdata/2/examplecloud_container.tf`), + }, + }, + }) +} + +func TestImportBlock_InConfigFile_ConfigExactTrue(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_5_0), // ImportBlockWithID requires Terraform 1.5.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + { + ConfigFile: config.StaticFile(`testdata/1/examplecloud_container.tf`), + }, + { + ResourceName: "examplecloud_container.test", + ImportState: true, + ImportStateKind: r.ImportBlockWithID, + + // This content includes an import block with an ID so we will + // use the exact content + ConfigFile: config.StaticFile(`testdata/examplecloud_container_with_exact_import_config_with_id.tf`), + ImportStateConfigExact: true, + }, + }, + }) +} diff --git a/helper/resource/importstate/import_block_with_id_test.go b/helper/resource/importstate/import_block_with_id_test.go new file mode 100644 index 000000000..b401deef5 --- /dev/null +++ b/helper/resource/importstate/import_block_with_id_test.go @@ -0,0 +1,489 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package importstate_test + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestImportBlock_WithID(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_5_0), // ImportBlockWithID requires Terraform 1.5.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + { + Config: ` + resource "examplecloud_container" "test" { + location = "westeurope" + name = "somevalue" + }`, + }, + { + ResourceName: "examplecloud_container.test", + ImportState: true, + ImportStateKind: r.ImportBlockWithID, + }, + }, + }) +} + +func TestImportBlock_WithID_ExpectError(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_5_0), // ImportBlockWithID requires Terraform 1.5.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + { + Config: ` + resource "examplecloud_container" "test" { + location = "westeurope" + name = "somevalue" + }`, + }, + { + Config: ` + resource "examplecloud_container" "test" { + location = "eastus" + name = "somevalue" + } + + import { + to = examplecloud_container.test + id = "westeurope/somevalue" + } + `, + ResourceName: "examplecloud_container.test", + ImportState: true, + ImportStateKind: r.ImportBlockWithID, + ImportStateConfigExact: true, + ExpectError: regexp.MustCompile(`importing resource examplecloud_container.test: expected a no-op import operation, got.*\["update"\] action with plan(.?)`), + }, + }, + }) +} + +func TestImportBlock_WithID_ExpectNonEmptyPlan(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_5_0), // ImportBlockWithID requires Terraform 1.5.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + { + Config: ` + resource "examplecloud_container" "test" { + location = "westeurope" + name = "somevalue" + }`, + }, + { + Config: ` + resource "examplecloud_container" "test" { + location = "eastus" + name = "somevalue" + } + + import { + to = examplecloud_container.test + id = "westeurope/somevalue" + } + `, + ResourceName: "examplecloud_container.test", + ImportState: true, + ImportStateKind: r.ImportBlockWithID, + ImportStateConfigExact: true, + ExpectNonEmptyPlan: true, + ImportPlanChecks: r.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("examplecloud_container.test", plancheck.ResourceActionUpdate), + // The location address is imported as "westeurope/somevalue", which will be updated by the config to "eastus" + plancheck.ExpectKnownValue("examplecloud_container.test", tfjsonpath.New("location"), knownvalue.StringExact("eastus")), + plancheck.ExpectUnknownValue("examplecloud_container.test", tfjsonpath.New("id")), + }, + }, + }, + }, + }) +} + +func TestImportBlock_WithID_FailWhenNotSupported(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + tfversion.SkipAbove(tfversion.Version1_4_0), // ImportBlockWithId requires Terraform 1.5.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + { + Config: ` + resource "examplecloud_container" "test" { + location = "westeurope" + name = "somevalue" + }`, + }, + { + ImportState: true, + ImportStateKind: r.ImportBlockWithID, + ResourceName: "examplecloud_container.test", + ExpectError: regexp.MustCompile(`Terraform 1.5.0`), + }, + }, + }) +} + +func TestImportBlock_WithID_SkipsDataSources(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_5_0), // ImportBlockWithID requires Terraform 1.5.0 or later + + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + DataSources: map[string]testprovider.DataSource{ + "examplecloud_thing": examplecloudDataSource(), + }, + Resources: map[string]testprovider.Resource{ + "examplecloud_thing": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + { + Config: ` + data "examplecloud_thing" "test" {} + resource "examplecloud_thing" "test" { + name = "somevalue" + location = "westeurope" + } + `, + }, + { + ResourceName: "examplecloud_thing.test", + ImportState: true, + ImportStateKind: r.ImportBlockWithID, + ImportStateCheck: func(is []*terraform.InstanceState) error { + if len(is) > 1 { + return fmt.Errorf("expected 1 state, got: %d", len(is)) + } + + return nil + }, + }, + }, + }) +} + +func TestImportBlock_WithID_WithBlankOptionalAttribute_GeneratesCorrectPlan(t *testing.T) { + /* + This test tries to imitate a real world example of behaviour we often see in the AzureRM provider which requires + the use of `ImportStateVerifyIgnore` when testing the import of a resource using the import command. + + A sensitive field e.g. a password can be supplied on create but isn't returned in the API response on a subsequent + read, resulting in a different value for password in the two states. + + In the AzureRM provider this is usually handled one of two ways, both requiring `ImportStateVerifyIgnore` to make + the test pass: + + 1. Property doesn't get set in the read + * in pluginSDK at create the config gets written to state because that's what we're expecting + * the subsequent read updates the values to create a post-apply diff and update computed values + * since we don't do anything to the property in the read the imported resource's state has the password missing + compared to the created resource's state + + 2. We retrieve the value from config and set that into state + * the config isn't available at import time using only the import command (I think?) so there is nothing to + retrieve and set into state when importing + + I also need to omit the `password` in the import config, otherwise the value in the config is used when importing the + with an import block and the test ends up passing regardless of whether `ImportStateVerifyIgnore` has been specified or not + */ + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_5_0), // ImportBlockWithID requires Terraform 1.5.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "name": tftypes.String, + "password": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "sometestid"), + "name": tftypes.NewValue(tftypes.String, "somename"), + "password": tftypes.NewValue(tftypes.String, "somevalue"), + }, + ), + }, + ImportStateResponse: &resource.ImportStateResponse{ + State: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "name": tftypes.String, + "password": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "sometestid"), + "name": tftypes.NewValue(tftypes.String, "somename"), + "password": tftypes.NewValue(tftypes.String, nil), // this simulates an absent property when importing + }, + ), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + { + Name: "name", + Type: tftypes.String, + Required: true, + }, + { + Name: "password", + Type: tftypes.String, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + Steps: []r.TestStep{ + { + Config: ` + resource "examplecloud_container" "test" { + name = "somename" + password = "somevalue" + }`, + }, + { + Config: ` + terraform { + required_providers { + examplecloud = { + source = "registry.terraform.io/hashicorp/examplecloud" + } + } + } + + resource "examplecloud_container" "test" { + name = "somename" + } + + import { + to = examplecloud_container.test + id = "sometestid" + + }`, + ResourceName: "examplecloud_container.test", + ImportState: true, + ImportStateKind: r.ImportBlockWithID, + ImportStateConfigExact: true, + }, + }, + }) +} + +func TestImportBlock_WithID_WithBlankComputedAttribute_GeneratesCorrectPlan(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_5_0), // ImportBlockWithID requires Terraform 1.5.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "name": tftypes.String, + "password": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "sometestid"), + "name": tftypes.NewValue(tftypes.String, "somename"), + "password": tftypes.NewValue(tftypes.String, "somevalue"), + }, + ), + }, + ImportStateResponse: &resource.ImportStateResponse{ + State: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "name": tftypes.String, + "password": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "sometestid"), + "name": tftypes.NewValue(tftypes.String, "somename"), + "password": tftypes.NewValue(tftypes.String, nil), // this simulates an absent property when importing + }, + ), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + { + Name: "name", + Type: tftypes.String, + Computed: true, + }, + { + Name: "password", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_container" "test" {}`, + }, + { + ResourceName: "examplecloud_container.test", + ImportState: true, + ImportStateKind: r.ImportBlockWithID, + }, + }, + }) +} + +func TestImportBlock_WithID_WithExternalProvider(t *testing.T) { + t.Parallel() + + config := ` +resource "random_string" "mystery_message" { + length = 31 +} +` + + configWithImportBlock := config + ` +import { + to = random_string.mystery_message + id = "It was a dark and stormy night." +} +` + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_5_0), // ImportBlockWithID requires Terraform 1.5.0 or later + }, + ExternalProviders: map[string]r.ExternalProvider{ + "random": { + Source: "hashicorp/random", + }, + }, + Steps: []r.TestStep{ + { + Config: config, + }, + { + ImportState: true, + ImportStateKind: r.ImportBlockWithID, + ImportStateConfigExact: true, + Config: configWithImportBlock, + ResourceName: "random_string.mystery_message", + ImportPlanChecks: r.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "random_string.mystery_message", + tfjsonpath.New("result"), + knownvalue.StringExact("It was a dark and stormy night.")), + }, + }, + }, + }, + }) +} diff --git a/helper/resource/importstate/import_block_with_resource_identity_test.go b/helper/resource/importstate/import_block_with_resource_identity_test.go new file mode 100644 index 000000000..c71a8b578 --- /dev/null +++ b/helper/resource/importstate/import_block_with_resource_identity_test.go @@ -0,0 +1,186 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package importstate_test + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestImportBlock_WithResourceIdentity(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), // ImportBlockWithResourceIdentity requires Terraform 1.12.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + { + Config: ` + resource "examplecloud_container" "test" { + location = "westeurope" + name = "somevalue" + }`, + }, + { + ResourceName: "examplecloud_container.test", + ImportState: true, + ImportStateKind: r.ImportBlockWithResourceIdentity, + }, + }, + }) +} + +func TestImportBlock_WithResourceIdentity_NullAttribute(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), // ImportBlockWithResourceIdentity requires Terraform 1.12.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": examplecloudResourceWithNullIdentityAttr(), + }, + }), + }, + Steps: []r.TestStep{ + { + Config: ` + resource "examplecloud_container" "test" { + location = "westeurope" + name = "somevalue" + }`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentity("examplecloud_container.test", map[string]knownvalue.Check{ + "id": knownvalue.StringExact("westeurope/somevalue"), + "value_we_dont_always_need": knownvalue.Null(), // This value will not be brought over to import config + }), + }, + }, + { + ResourceName: "examplecloud_container.test", + ImportState: true, + ImportStateKind: r.ImportBlockWithResourceIdentity, + }, + }, + }) +} + +func TestImportBlock_WithResourceIdentity_WithEveryType(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), // ImportBlockWithResourceIdentity requires Terraform 1.12.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": examplecloudResourceWithEveryIdentitySchemaType(), + }, + }), + }, + Steps: []r.TestStep{ + { + Config: ` + resource "examplecloud_container" "test" { + cabinet = "A1" + unit = 14 + tags = ["storage", "fast"] + active = true + }`, + }, + { + ResourceName: "examplecloud_container.test", + ImportState: true, + ImportStateKind: r.ImportBlockWithResourceIdentity, + }, + }, + }) +} + +func TestImportBlock_WithResourceIdentity_ChangingIdentityError(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), // ImportBlockWithResourceIdentity requires Terraform 1.12.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": examplecloudResourceWithChangingIdentity(), + }, + }), + }, + Steps: []r.TestStep{ + { + Config: ` + resource "examplecloud_container" "test" { + location = "westeurope" + name = "somevalue" + }`, + }, + { + ResourceName: "examplecloud_container.test", + ImportState: true, + ImportStateKind: r.ImportBlockWithResourceIdentity, + // The plan following the import will produce a different identity value then test step 1 + ExpectError: regexp.MustCompile(`expected identity values map\[id:westeurope/somevalue\], got map\[id:easteurope/someothervalue\]`), + }, + }, + }) +} + +func TestImportBlock_WithResourceIdentity_RequiresVersion1_12_0(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + tfversion.SkipAbove(tfversion.Version1_11_0), // ImportBlockWithResourceIdentity requires Terraform 1.12.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + { + Config: ` + resource "examplecloud_container" "test" { + location = "westeurope" + name = "somevalue" + }`, + }, + { + ResourceName: "examplecloud_container.test", + ImportState: true, + ImportStateKind: r.ImportBlockWithResourceIdentity, + ExpectError: regexp.MustCompile(`Terraform 1.12.0\S* or later`), + }, + }, + }) +} diff --git a/helper/resource/importstate/import_command_as_first_step_test.go b/helper/resource/importstate/import_command_as_first_step_test.go new file mode 100644 index 000000000..14db18828 --- /dev/null +++ b/helper/resource/importstate/import_command_as_first_step_test.go @@ -0,0 +1,62 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package importstate_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "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/terraform" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestImportCommand_AsFirstStep(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories needs Terraform 1.0.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + { + ResourceName: "examplecloud_container.test", + ImportStateId: "examplecloud_container.test", + ImportState: true, + Config: `resource "examplecloud_container" "test" { + name = "somevalue" + location = "westeurope" + }`, + ImportStatePersist: true, + ImportStateCheck: func(states []*terraform.InstanceState) error { + if len(states) != 1 { + return fmt.Errorf("expected 1 state; got %d", len(states)) + } + if states[0].ID != "westeurope/somevalue" { + return fmt.Errorf("unexpected ID: %s", states[0].ID) + } + if states[0].Attributes["name"] != "somevalue" { + return fmt.Errorf("unexpected name: %s", states[0].Attributes["name"]) + } + if states[0].Attributes["location"] != "westeurope" { + return fmt.Errorf("unexpected location: %s", states[0].Attributes["location"]) + } + return nil + }, + }, + }, + }) +} diff --git a/helper/resource/importstate/import_command_with_id_test.go b/helper/resource/importstate/import_command_with_id_test.go new file mode 100644 index 000000000..52e7731b2 --- /dev/null +++ b/helper/resource/importstate/import_command_with_id_test.go @@ -0,0 +1,398 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package importstate_test + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-testing/config" + 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/datasource" + "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/terraform" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestImportCommand_ImportStateCheckSkipsDataSources(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + DataSources: map[string]testprovider.DataSource{ + "examplecloud_thing": { + ReadResponse: &datasource.ReadResponse{ + State: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "datasource-test"), + }, + ), + }, + SchemaResponse: &datasource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + Resources: map[string]testprovider.Resource{ + "examplecloud_thing": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "resource-test"), + }, + ), + }, + ImportStateResponse: &resource.ImportStateResponse{ + State: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "resource-test"), + }, + ), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + Steps: []r.TestStep{ + { + Config: ` + data "examplecloud_thing" "test" {} + resource "examplecloud_thing" "test" {} + `, + }, + { + ResourceName: "examplecloud_thing.test", + ImportState: true, + ImportStateCheck: func(is []*terraform.InstanceState) error { + if len(is) > 1 { + return fmt.Errorf("expected 1 state, got: %d", len(is)) + } + + return nil + }, + }, + }, + }) +} + +func TestImportCommand_ImportStateVerify(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_thing": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "other": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "resource-test"), + "other": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ImportStateResponse: &resource.ImportStateResponse{ + State: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "other": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "resource-test"), + "other": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + { + Name: "other", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "test" {}`, + }, + { + ResourceName: "examplecloud_thing.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestImportCommand_InConfigFile(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + { + ConfigFile: config.StaticFile(`testdata/1/examplecloud_container.tf`), + }, + { + ResourceName: "examplecloud_container.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestImportCommand_InConfigDirectory(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + { + ConfigDirectory: config.StaticDirectory(`testdata/1`), + }, + { + ResourceName: "examplecloud_container.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestImportCommand_ImportStateVerify_Ignore(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_thing": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "create_only": tftypes.String, + "read_only": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "resource-test"), + "create_only": tftypes.NewValue(tftypes.String, "testvalue"), + "read_only": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ImportStateResponse: &resource.ImportStateResponse{ + State: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "create_only": tftypes.String, + "read_only": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "resource-test"), + "create_only": tftypes.NewValue(tftypes.String, nil), // intentional + "read_only": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "create_only", + Type: tftypes.String, + Computed: true, + }, + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + { + Name: "read_only", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "test" {}`, + }, + { + ResourceName: "examplecloud_thing.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"create_only"}, + }, + }, + }) +} + +func TestImportCommand_ExpectError(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "resource-test"), + }, + ), + }, + ImportStateResponse: &resource.ImportStateResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Invalid Import ID", + Detail: "Diagnostic details", + }, + }, + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" {}`, + ImportStateId: "invalid time string", + ResourceName: "test_resource.test", + ImportState: true, + ExpectError: regexp.MustCompile(`Error: Invalid Import ID`), + }, + }, + }) +} diff --git a/helper/resource/importstate/testdata/1/examplecloud_container.tf b/helper/resource/importstate/testdata/1/examplecloud_container.tf new file mode 100644 index 000000000..ccfb698e6 --- /dev/null +++ b/helper/resource/importstate/testdata/1/examplecloud_container.tf @@ -0,0 +1,7 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "examplecloud_container" "test" { + name = "somevalue" + location = "westeurope" +} diff --git a/helper/resource/importstate/testdata/2/examplecloud_container.tf b/helper/resource/importstate/testdata/2/examplecloud_container.tf new file mode 100644 index 000000000..ccfb698e6 --- /dev/null +++ b/helper/resource/importstate/testdata/2/examplecloud_container.tf @@ -0,0 +1,7 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "examplecloud_container" "test" { + name = "somevalue" + location = "westeurope" +} diff --git a/helper/resource/importstate/testdata/2_with_exact_import_config/examplecloud_container.tf b/helper/resource/importstate/testdata/2_with_exact_import_config/examplecloud_container.tf new file mode 100644 index 000000000..f7e9411f9 --- /dev/null +++ b/helper/resource/importstate/testdata/2_with_exact_import_config/examplecloud_container.tf @@ -0,0 +1,12 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "examplecloud_container" "test" { + name = "somevalue" + location = "westeurope" +} + +import { + to = examplecloud_container.test + id = "examplecloud_container.test" +} diff --git a/helper/resource/importstate/testdata/examplecloud_container_with_exact_import_config_with_id.tf b/helper/resource/importstate/testdata/examplecloud_container_with_exact_import_config_with_id.tf new file mode 100644 index 000000000..f7e9411f9 --- /dev/null +++ b/helper/resource/importstate/testdata/examplecloud_container_with_exact_import_config_with_id.tf @@ -0,0 +1,12 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "examplecloud_container" "test" { + name = "somevalue" + location = "westeurope" +} + +import { + to = examplecloud_container.test + id = "examplecloud_container.test" +} diff --git a/helper/resource/importstate/types_test.go b/helper/resource/importstate/types_test.go new file mode 100644 index 000000000..8532b40da --- /dev/null +++ b/helper/resource/importstate/types_test.go @@ -0,0 +1,66 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package importstate_test + +import ( + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func RequiredBoolAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.Bool, + Required: true, + } +} + +func OptionalComputedListAttribute(name string, elementType tftypes.Type) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.List{ElementType: elementType}, + Optional: true, + Computed: true, + } +} + +func RequiredListAttribute(name string, elementType tftypes.Type) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.List{ElementType: elementType}, + Required: true, + } +} + +func RequiredNumberAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.Number, + Required: true, + } +} + +func ComputedStringAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.String, + Computed: true, + } +} + +func OptionalStringAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.String, + Optional: true, + } +} + +func RequiredStringAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.String, + Required: true, + } +} diff --git a/helper/resource/plan_checks.go b/helper/resource/plan_checks.go index 712e3dbdf..c64c02ba8 100644 --- a/helper/resource/plan_checks.go +++ b/helper/resource/plan_checks.go @@ -5,9 +5,9 @@ package resource import ( "context" + "errors" tfjson "github.com/hashicorp/terraform-json" - "github.com/hashicorp/terraform-plugin-testing/internal/errorshim" "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/mitchellh/go-testing-interface" ) @@ -15,18 +15,14 @@ import ( func runPlanChecks(ctx context.Context, t testing.T, plan *tfjson.Plan, planChecks []plancheck.PlanCheck) error { t.Helper() - var result error + var result []error for _, planCheck := range planChecks { resp := plancheck.CheckPlanResponse{} planCheck.CheckPlan(ctx, plancheck.CheckPlanRequest{Plan: plan}, &resp) - if resp.Error != nil { - // TODO: Once Go 1.20 is the minimum supported version for this module, replace with `errors.Join` function - // - https://github.com/hashicorp/terraform-plugin-testing/issues/99 - result = errorshim.Join(result, resp.Error) - } + result = append(result, resp.Error) } - return result + return errors.Join(result...) } diff --git a/helper/resource/plugin.go b/helper/resource/plugin.go index 5c92f3ab9..86c3b77ba 100644 --- a/helper/resource/plugin.go +++ b/helper/resource/plugin.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/go-hclog" "github.com/hashicorp/terraform-exec/tfexec" + tfjson "github.com/hashicorp/terraform-json" "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -113,7 +114,42 @@ type providerFactories struct { protov6 protov6ProviderFactories } -func runProviderCommand(ctx context.Context, t testing.T, f func() error, wd *plugintest.WorkingDir, factories *providerFactories) error { +func runProviderCommandApplyRefreshOnly(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, factories *providerFactories) error { + t.Helper() + + fn := func() error { + return wd.Apply(ctx, tfexec.Refresh(true), tfexec.RefreshOnly(true)) + } + return runProviderCommand(ctx, t, wd, factories, fn) +} + +func runProviderCommandCreatePlan(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, factories *providerFactories) error { + t.Helper() + + fn := func() error { + return wd.CreatePlan(ctx) + } + return runProviderCommand(ctx, t, wd, factories, fn) +} + +func runProviderCommandSavedPlan(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, factories *providerFactories) (*tfjson.Plan, error) { + t.Helper() + + var plan *tfjson.Plan + fn := func() error { + var err error + plan, err = wd.SavedPlan(ctx) + return err + } + err := runProviderCommand(ctx, t, wd, factories, fn) + if err != nil { + return nil, err + } + + return plan, nil +} + +func runProviderCommand(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, factories *providerFactories, f func() error) error { // don't point to this as a test failure location // point to whatever called it t.Helper() @@ -178,14 +214,14 @@ func runProviderCommand(ctx context.Context, t testing.T, f func() error, wd *pl providerName = strings.TrimPrefix(providerName, "terraform-provider-") providerAddress := getProviderAddr(providerName) - logging.HelperResourceDebug(ctx, "Creating sdkv2 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + logging.HelperResourceTrace(ctx, "Creating sdkv2 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) provider, err := factory() if err != nil { return fmt.Errorf("unable to create provider %q from factory: %w", providerName, err) } - logging.HelperResourceDebug(ctx, "Created sdkv2 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + logging.HelperResourceTrace(ctx, "Created sdkv2 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) // keep track of the running factory, so we can make sure it's // shut down. @@ -215,14 +251,14 @@ func runProviderCommand(ctx context.Context, t testing.T, f func() error, wd *pl ProviderAddr: providerAddress, } - logging.HelperResourceDebug(ctx, "Starting sdkv2 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + logging.HelperResourceTrace(ctx, "Starting sdkv2 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) config, closeCh, err := plugin.DebugServe(ctx, opts) if err != nil { return fmt.Errorf("unable to serve provider %q: %w", providerName, err) } - logging.HelperResourceDebug(ctx, "Started sdkv2 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + logging.HelperResourceTrace(ctx, "Started sdkv2 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) tfexecConfig := tfexec.ReattachConfig{ Protocol: config.Protocol, @@ -272,14 +308,14 @@ func runProviderCommand(ctx context.Context, t testing.T, f func() error, wd *pl } } - logging.HelperResourceDebug(ctx, "Creating tfprotov5 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + logging.HelperResourceTrace(ctx, "Creating tfprotov5 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) provider, err := factory() if err != nil { return fmt.Errorf("unable to create provider %q from factory: %w", providerName, err) } - logging.HelperResourceDebug(ctx, "Created tfprotov5 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + logging.HelperResourceTrace(ctx, "Created tfprotov5 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) // keep track of the running factory, so we can make sure it's // shut down. @@ -303,14 +339,14 @@ func runProviderCommand(ctx context.Context, t testing.T, f func() error, wd *pl ProviderAddr: providerAddress, } - logging.HelperResourceDebug(ctx, "Starting tfprotov5 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + logging.HelperResourceTrace(ctx, "Starting tfprotov5 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) config, closeCh, err := plugin.DebugServe(ctx, opts) if err != nil { return fmt.Errorf("unable to serve provider %q: %w", providerName, err) } - logging.HelperResourceDebug(ctx, "Started tfprotov5 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + logging.HelperResourceTrace(ctx, "Started tfprotov5 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) tfexecConfig := tfexec.ReattachConfig{ Protocol: config.Protocol, @@ -361,14 +397,14 @@ func runProviderCommand(ctx context.Context, t testing.T, f func() error, wd *pl } } - logging.HelperResourceDebug(ctx, "Creating tfprotov6 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + logging.HelperResourceTrace(ctx, "Creating tfprotov6 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) provider, err := factory() if err != nil { return fmt.Errorf("unable to create provider %q from factory: %w", providerName, err) } - logging.HelperResourceDebug(ctx, "Created tfprotov6 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + logging.HelperResourceTrace(ctx, "Created tfprotov6 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) // keep track of the running factory, so we can make sure it's // shut down. @@ -388,14 +424,14 @@ func runProviderCommand(ctx context.Context, t testing.T, f func() error, wd *pl ProviderAddr: providerAddress, } - logging.HelperResourceDebug(ctx, "Starting tfprotov6 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + logging.HelperResourceTrace(ctx, "Starting tfprotov6 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) config, closeCh, err := plugin.DebugServe(ctx, opts) if err != nil { return fmt.Errorf("unable to serve provider %q: %w", providerName, err) } - logging.HelperResourceDebug(ctx, "Started tfprotov6 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + logging.HelperResourceTrace(ctx, "Started tfprotov6 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) tfexecConfig := tfexec.ReattachConfig{ Protocol: config.Protocol, @@ -441,7 +477,7 @@ func runProviderCommand(ctx context.Context, t testing.T, f func() error, wd *pl } logging.HelperResourceTrace(ctx, "Called wrapped Terraform CLI command") - logging.HelperResourceDebug(ctx, "Stopping providers") + logging.HelperResourceTrace(ctx, "Stopping providers") // cancel the servers so they'll return. Otherwise, this closeCh won't // get closed, and we'll hang here. diff --git a/helper/resource/plugin_test.go b/helper/resource/plugin_test.go index ac8244185..62823efd7 100644 --- a/helper/resource/plugin_test.go +++ b/helper/resource/plugin_test.go @@ -79,8 +79,6 @@ func TestProtoV5ProviderFactoriesMerge(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -154,8 +152,6 @@ func TestProtoV6ProviderFactoriesMerge(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -229,8 +225,6 @@ func TestSdkProviderFactoriesMerge(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -256,44 +250,38 @@ func TestRunProviderCommand(t *testing.T) { funcCalled := false helper := plugintest.AutoInitProviderHelper(ctx, currentDir) - err = runProviderCommand( - ctx, - t, - func() error { - funcCalled = true - return nil - }, - helper.RequireNewWorkingDir(ctx, t, ""), - &providerFactories{ - legacy: map[string]func() (*schema.Provider, error){ - "examplecloud": func() (*schema.Provider, error) { //nolint:unparam // required signature - return &schema.Provider{ - ResourcesMap: map[string]*schema.Resource{ - "examplecloud_thing": { - CreateContext: func(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { - d.SetId("id") - - return nil - }, - DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { - return nil - }, - ReadContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { - return nil - }, - Schema: map[string]*schema.Schema{ - "id": { - Computed: true, - Type: schema.TypeString, - }, + err = runProviderCommand(ctx, t, helper.RequireNewWorkingDir(ctx, t, ""), &providerFactories{ + legacy: map[string]func() (*schema.Provider, error){ + "examplecloud": func() (*schema.Provider, error) { //nolint:unparam // required signature + return &schema.Provider{ + ResourcesMap: map[string]*schema.Resource{ + "examplecloud_thing": { + CreateContext: func(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { + d.SetId("id") + + return nil + }, + DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + ReadContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + Schema: map[string]*schema.Schema{ + "id": { + Computed: true, + Type: schema.TypeString, }, }, }, - }, nil - }, + }, + }, nil }, }, - ) + }, func() error { + funcCalled = true + return nil + }) if err != nil { t.Fatal(err) diff --git a/helper/resource/query/examplecloud_list_test.go b/helper/resource/query/examplecloud_list_test.go new file mode 100644 index 000000000..7b18943ef --- /dev/null +++ b/helper/resource/query/examplecloud_list_test.go @@ -0,0 +1,91 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package query_test + +import ( + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/list" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" +) + +func examplecloudListResource() testprovider.ListResource { + return testprovider.ListResource{ + IncludeResource: true, + SchemaResponse: &list.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + ListResultsStream: &list.ListResultsStream{ + Results: func(push func(list.ListResult) bool) { + push(list.ListResult{ + Resource: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "somevalue"), + }, + )), + Identity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue1"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + }, + )), + }) + push(list.ListResult{ + Identity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue2"), + "location": tftypes.NewValue(tftypes.String, "westeurope2"), + }, + )), + }) + push(list.ListResult{ + Identity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue3"), + "location": tftypes.NewValue(tftypes.String, "westeurope3"), + }, + )), + }) + }, + }, + } +} diff --git a/helper/resource/query/examplecloud_test.go b/helper/resource/query/examplecloud_test.go new file mode 100644 index 000000000..3ad86c169 --- /dev/null +++ b/helper/resource/query/examplecloud_test.go @@ -0,0 +1,129 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package query_test + +import ( + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" +) + +func examplecloudResource() testprovider.Resource { + return testprovider.Resource{ + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "somevalue"), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "location": tftypes.NewValue(tftypes.String, "somelocation"), + }, + )), + }, + ReadResponse: &resource.ReadResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "somevalue"), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + }, + )), + }, + ImportStateResponse: &resource.ImportStateResponse{ + State: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "somevalue"), + }, + ), + Identity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "location": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + }, + )), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + ComputedStringAttribute("id"), + RequiredStringAttribute("location"), + RequiredStringAttribute("name"), + }, + }, + }, + }, + IdentitySchemaResponse: &resource.IdentitySchemaResponse{ + Schema: &tfprotov6.ResourceIdentitySchema{ + Version: 1, + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + RequiredForImport: true, + }, + { + Name: "location", + Type: tftypes.String, + RequiredForImport: true, + }, + }, + }, + }, + } +} diff --git a/helper/resource/query/query_checks.go b/helper/resource/query/query_checks.go new file mode 100644 index 000000000..8ef9a3e09 --- /dev/null +++ b/helper/resource/query/query_checks.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package query + +import ( + "context" + "errors" + "fmt" + + tfjson "github.com/hashicorp/terraform-json" + "github.com/mitchellh/go-testing-interface" + + "github.com/hashicorp/terraform-plugin-testing/querycheck" +) + +func RunQueryChecks(ctx context.Context, t testing.T, query []tfjson.LogMsg, queryChecks []querycheck.QueryResultCheck) error { + t.Helper() + + var result []error + + if query == nil { + result = append(result, fmt.Errorf("no query results found")) + } + + found := make([]tfjson.ListResourceFoundData, 0) + summary := tfjson.ListCompleteData{} + + for _, msg := range query { + switch v := msg.(type) { + case tfjson.ListResourceFoundMessage: + found = append(found, v.ListResourceFound) + case tfjson.ListCompleteMessage: + summary = v.ListComplete + // TODO diagnostics and errors? + default: + continue + } + } + + for _, queryCheck := range queryChecks { + resp := querycheck.CheckQueryResponse{} + queryCheck.CheckQuery(ctx, querycheck.CheckQueryRequest{ + Query: found, + QuerySummary: &summary, + }, &resp) + + result = append(result, resp.Error) + } + + return errors.Join(result...) +} diff --git a/helper/resource/query/query_checks_test.go b/helper/resource/query/query_checks_test.go new file mode 100644 index 000000000..2c17be8d2 --- /dev/null +++ b/helper/resource/query/query_checks_test.go @@ -0,0 +1,22 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package query + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-testing/querycheck" +) + +var _ querycheck.QueryResultCheck = &queryCheckSpy{} + +type queryCheckSpy struct { + err error + called bool +} + +func (s *queryCheckSpy) CheckQuery(ctx context.Context, req querycheck.CheckQueryRequest, resp *querycheck.CheckQueryResponse) { + s.called = true + resp.Error = s.err +} diff --git a/helper/resource/query/query_test.go b/helper/resource/query/query_test.go new file mode 100644 index 000000000..1fa76381d --- /dev/null +++ b/helper/resource/query/query_test.go @@ -0,0 +1,144 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package query_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/querycheck" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestQuery(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + ListResources: map[string]testprovider.ListResource{ + "examplecloud_containerette": examplecloudListResource(), + }, + Resources: map[string]testprovider.Resource{ + "examplecloud_containerette": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + { // config mode step 1 needs tf file with terraform providers block + Config: ` + resource "examplecloud_containerette" "primary" { + id = "westeurope/somevalue" + location = "westeurope" + name = "somevalue" + }`, + }, + { // Query mode step 2, operates on .tfquery.hcl files (needs tf file with terraform providers block) + // ```provider "examplecloud" {}``` has a slightly different syntax for a .tfquery.hcl file + // provider bock simulates a real providers workflow + // "config" in this case means configuration of the list resource/filters + + Query: true, + Config: ` + provider "examplecloud" {} + list "examplecloud_containerette" "test" { + provider = examplecloud + + config { + id = "westeurope/somevalue" + } + } + list "examplecloud_containerette" "test2" { + provider = examplecloud + + config { + id = "foo" + } + } + `, + QueryResultChecks: []querycheck.QueryResultCheck{ + querycheck.ExpectIdentity("examplecloud_containerette.test", map[string]knownvalue.Check{ + "id": knownvalue.StringExact("westeurope/somevalue1"), + "location": knownvalue.StringExact("westeurope"), + }), + querycheck.ExpectIdentity("examplecloud_containerette.test", map[string]knownvalue.Check{ + "id": knownvalue.StringExact("westeurope/somevalue2"), + "location": knownvalue.StringExact("westeurope2"), + }), + querycheck.ExpectIdentity("examplecloud_containerette.test", map[string]knownvalue.Check{ + "id": knownvalue.StringExact("westeurope/somevalue3"), + "location": knownvalue.StringExact("westeurope3"), + }), + querycheck.ExpectIdentity("examplecloud_containerette.test2", map[string]knownvalue.Check{ + "id": knownvalue.StringExact("westeurope/somevalue1"), + "location": knownvalue.StringExact("westeurope"), + }), + querycheck.ExpectIdentity("examplecloud_containerette.test2", map[string]knownvalue.Check{ + "id": knownvalue.StringExact("westeurope/somevalue2"), + "location": knownvalue.StringExact("westeurope2"), + }), + querycheck.ExpectIdentity("examplecloud_containerette.test2", map[string]knownvalue.Check{ + "id": knownvalue.StringExact("westeurope/somevalue3"), + "location": knownvalue.StringExact("westeurope3"), + }), + }, + }, + { + Query: true, + Config: ` + provider "examplecloud" {} + list "examplecloud_containerette" "test" { + provider = examplecloud + + config { + id = "westeurope/somevalue" + } + } + list "examplecloud_containerette" "test2" { + provider = examplecloud + + config { + id = "foo" + } + } + `, + QueryResultChecks: []querycheck.QueryResultCheck{ + querycheck.ExpectLength("examplecloud_containerette.test", 3), + querycheck.ExpectLength("examplecloud_containerette.test2", 3), + }, + }, + { + Query: true, + Config: ` + provider "examplecloud" {} + list "examplecloud_containerette" "test" { + provider = examplecloud + + config { + id = "westeurope/somevalue" + } + } + list "examplecloud_containerette" "test2" { + provider = examplecloud + + config { + id = "foo" + } + } + `, + QueryResultChecks: []querycheck.QueryResultCheck{ + querycheck.ExpectLengthAtLeast("examplecloud_containerette.test", 2), + querycheck.ExpectLengthAtLeast("examplecloud_containerette.test2", 1), + }, + }, + }, + }) +} diff --git a/helper/resource/query/types_test.go b/helper/resource/query/types_test.go new file mode 100644 index 000000000..7620d4d7d --- /dev/null +++ b/helper/resource/query/types_test.go @@ -0,0 +1,66 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package query_test + +import ( + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func RequiredBoolAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.Bool, + Required: true, + } +} + +func OptionalComputedListAttribute(name string, elementType tftypes.Type) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.List{ElementType: elementType}, + Optional: true, + Computed: true, + } +} + +func RequiredListAttribute(name string, elementType tftypes.Type) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.List{ElementType: elementType}, + Required: true, + } +} + +func RequiredNumberAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.Number, + Required: true, + } +} + +func ComputedStringAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.String, + Computed: true, + } +} + +func OptionalStringAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.String, + Optional: true, + } +} + +func RequiredStringAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.String, + Required: true, + } +} diff --git a/helper/resource/state_checks.go b/helper/resource/state_checks.go new file mode 100644 index 000000000..66c850eae --- /dev/null +++ b/helper/resource/state_checks.go @@ -0,0 +1,29 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package resource + +import ( + "context" + "errors" + + tfjson "github.com/hashicorp/terraform-json" + "github.com/mitchellh/go-testing-interface" + + "github.com/hashicorp/terraform-plugin-testing/statecheck" +) + +func runStateChecks(ctx context.Context, t testing.T, state *tfjson.State, stateChecks []statecheck.StateCheck) error { + t.Helper() + + var result []error + + for _, stateCheck := range stateChecks { + resp := statecheck.CheckStateResponse{} + stateCheck.CheckState(ctx, statecheck.CheckStateRequest{State: state}, &resp) + + result = append(result, resp.Error) + } + + return errors.Join(result...) +} diff --git a/helper/resource/state_checks_test.go b/helper/resource/state_checks_test.go new file mode 100644 index 000000000..e8ab33753 --- /dev/null +++ b/helper/resource/state_checks_test.go @@ -0,0 +1,22 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package resource + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-testing/statecheck" +) + +var _ statecheck.StateCheck = &stateCheckSpy{} + +type stateCheckSpy struct { + err error + called bool +} + +func (s *stateCheckSpy) CheckState(ctx context.Context, req statecheck.CheckStateRequest, resp *statecheck.CheckStateResponse) { + s.called = true + resp.Error = s.err +} diff --git a/helper/resource/state_shim.go b/helper/resource/state_shim.go index 381ca51d5..2cd8407ff 100644 --- a/helper/resource/state_shim.go +++ b/helper/resource/state_shim.go @@ -21,7 +21,7 @@ type shimmedState struct { } func shimStateFromJson(jsonState *tfjson.State) (*terraform.State, error) { - state := terraform.NewState() + state := terraform.NewState() //nolint:staticcheck // legacy usage state.TFVersion = jsonState.TerraformVersion if jsonState.Values == nil { @@ -62,27 +62,45 @@ func shimOutputState(so *tfjson.StateOutput) (*terraform.OutputState, error) { os.Value = v return os, nil } + switch firstElem := v[0].(type) { case string: elements := make([]interface{}, len(v)) for i, el := range v { - //nolint:forcetypeassert // Guaranteed by type switch - elements[i] = el.(string) + strElement, ok := el.(string) + // If the type of the element doesn't match the first elem, it's a tuple, return the original value + if !ok { + os.Value = v + return os, nil + } + elements[i] = strElement } os.Value = elements case bool: elements := make([]interface{}, len(v)) for i, el := range v { - //nolint:forcetypeassert // Guaranteed by type switch - elements[i] = el.(bool) + boolElement, ok := el.(bool) + // If the type of the element doesn't match the first elem, it's a tuple, return the original value + if !ok { + os.Value = v + return os, nil + } + + elements[i] = boolElement } os.Value = elements // unmarshalled number from JSON will always be json.Number case json.Number: elements := make([]interface{}, len(v)) for i, el := range v { - //nolint:forcetypeassert // Guaranteed by type switch - elements[i] = el.(json.Number) + numberElement, ok := el.(json.Number) + // If the type of the element doesn't match the first elem, it's a tuple, return the original value + if !ok { + os.Value = v + return os, nil + } + + elements[i] = numberElement } os.Value = elements case []interface{}: @@ -124,7 +142,7 @@ func (ss *shimmedState) shimStateModule(sm *tfjson.StateModule) error { } } - mod := ss.state.AddModule(path) + mod := ss.state.AddModule(path) //nolint:staticcheck // legacy usage for _, res := range sm.Resources { resourceState, err := shimResourceState(res) if err != nil { @@ -193,15 +211,36 @@ func shimResourceState(res *tfjson.StateResource) (*terraform.ResourceState, err } attributes := sf.Flatmap() - if _, ok := attributes["id"]; !ok { - return nil, fmt.Errorf("no %q found in attributes", "id") + // The instance state identifier was a Terraform versions 0.11 and earlier + // concept which helped core and the then SDK determine if the resource + // should be removed and as an identifier value in the human readable + // output. This concept unfortunately carried over to the testing logic when + // the testing logic was mostly changed to use the public, machine-readable + // JSON interface with Terraform, rather than reusing prior internal logic + // from Terraform. Using the "id" attribute value for this identifier was + // the default implementation and therefore those older versions of + // Terraform required the attribute. This is no longer necessary after + // Terraform versions 0.12 and later. + // + // If the "id" attribute is not found, set the instance state identifier to + // a synthetic value that can hopefully lead someone encountering the value + // to these comments. The prior logic used to raise an error if the + // attribute was not present, but this value should now only be present in + // legacy logic of this Go module, such as unintentionally exported logic in + // the terraform package, and not encountered during normal testing usage. + // + // Reference: https://github.com/hashicorp/terraform-plugin-testing/issues/84 + instanceStateID, ok := attributes["id"] + + if !ok { + instanceStateID = "id-attribute-not-set" } return &terraform.ResourceState{ Provider: res.ProviderName, Type: res.Type, Primary: &terraform.InstanceState{ - ID: attributes["id"], + ID: instanceStateID, Attributes: attributes, Meta: map[string]interface{}{ "schema_version": int(res.SchemaVersion), diff --git a/helper/resource/state_shim_test.go b/helper/resource/state_shim_test.go new file mode 100644 index 000000000..d1dec0752 --- /dev/null +++ b/helper/resource/state_shim_test.go @@ -0,0 +1,307 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package resource + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" +) + +func TestStateShimOutput_String(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + Steps: []TestStep{ + { + Config: `output "test" { + value = "hello world" + }`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "test", + knownvalue.StringExact("hello world"), + ), + }, + }, + }, + }) +} + +func TestStateShimOutput_List(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + Steps: []TestStep{ + { + Config: ` + output "list_of_strings" { + value = tolist(["hello", "world"]) + } + output "list_of_bools" { + value = tolist([true, false]) + } + output "list_of_numbers" { + value = tolist([1.23, 4.56, 500]) + } + output "list_of_objects" { + value = tolist([{a = "hey", b = "there"}, {a = "and", b = "another"}]) + } + output "list_of_lists" { + value = tolist([tolist(["hey", "there"]), tolist(["and", "another"])]) + }`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "list_of_strings", + knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.StringExact("hello"), + knownvalue.StringExact("world"), + }, + ), + ), + statecheck.ExpectKnownOutputValue( + "list_of_bools", + knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Bool(true), + knownvalue.Bool(false), + }, + ), + ), + statecheck.ExpectKnownOutputValue( + "list_of_numbers", + knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Float64Exact(1.23), + knownvalue.Float64Exact(4.56), + knownvalue.Float64Exact(500), + }, + ), + ), + statecheck.ExpectKnownOutputValue( + "list_of_objects", + knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "a": knownvalue.StringExact("hey"), + "b": knownvalue.StringExact("there"), + }), + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "a": knownvalue.StringExact("and"), + "b": knownvalue.StringExact("another"), + }), + }, + ), + ), + statecheck.ExpectKnownOutputValue( + "list_of_lists", + knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.StringExact("hey"), + knownvalue.StringExact("there"), + }, + ), + knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.StringExact("and"), + knownvalue.StringExact("another"), + }, + ), + }, + ), + ), + }, + }, + }, + }) +} + +// Ref: https://github.com/hashicorp/terraform-plugin-testing/issues/310 +func TestStateShimOutput_Tuple(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + Steps: []TestStep{ + { + Config: `output "test" { + value = [true, "hello", 1.23, ["hello", "world"], {a = false}] + }`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "test", + knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Bool(true), + knownvalue.StringExact("hello"), + knownvalue.Float64Exact(1.23), + knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.StringExact("hello"), + knownvalue.StringExact("world"), + }, + ), + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "a": knownvalue.Bool(false), + }), + }, + ), + ), + }, + }, + { + Config: `output "test" { + value = [{a = false}, true, "hello", 1.23, ["hello", "world"]] + }`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "test", + knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "a": knownvalue.Bool(false), + }), + knownvalue.Bool(true), + knownvalue.StringExact("hello"), + knownvalue.Float64Exact(1.23), + knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.StringExact("hello"), + knownvalue.StringExact("world"), + }, + ), + }, + ), + ), + }, + }, + { + Config: `output "test" { + value = [["hello", "world"], {a = false}, true, "hello", 1.23] + }`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "test", + knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.StringExact("hello"), + knownvalue.StringExact("world"), + }, + ), + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "a": knownvalue.Bool(false), + }), + knownvalue.Bool(true), + knownvalue.StringExact("hello"), + knownvalue.Float64Exact(1.23), + }, + ), + ), + }, + }, + { + Config: `output "test" { + value = [1.23, ["hello", "world"], {a = false}, true, "hello"] + }`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "test", + knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Float64Exact(1.23), + knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.StringExact("hello"), + knownvalue.StringExact("world"), + }, + ), + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "a": knownvalue.Bool(false), + }), + knownvalue.Bool(true), + knownvalue.StringExact("hello"), + }, + ), + ), + }, + }, + { + Config: `output "test" { + value = ["hello", 1.23, ["hello", "world"], {a = false}, true] + }`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "test", + knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.StringExact("hello"), + knownvalue.Float64Exact(1.23), + knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.StringExact("hello"), + knownvalue.StringExact("world"), + }, + ), + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "a": knownvalue.Bool(false), + }), + knownvalue.Bool(true), + }, + ), + ), + }, + }, + }, + }) +} + +func TestStateShimOutput_Object(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + Steps: []TestStep{ + { + Config: `output "test" { + value = { + a = "hey", + b = "there", + c = true, + d = 1.23, + } + }`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "test", + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "a": knownvalue.StringExact("hey"), + "b": knownvalue.StringExact("there"), + "c": knownvalue.Bool(true), + "d": knownvalue.Float64Exact(1.23), + }), + ), + }, + }, + }, + }) +} diff --git a/helper/resource/testcase_providers_test.go b/helper/resource/testcase_providers_test.go index 486ef1b5c..4b54d725d 100644 --- a/helper/resource/testcase_providers_test.go +++ b/helper/resource/testcase_providers_test.go @@ -16,6 +16,9 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" + "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/tfversion" ) func TestTestCaseProviderConfig(t *testing.T) { @@ -222,13 +225,12 @@ provider "test" {} "test": {}, }, }, - expected: `provider "test" {}`, + expected: ` +provider "test" {}`, }, } for name, test := range tests { - name, test := name, test - t.Run(name, func(t *testing.T) { t.Parallel() @@ -262,6 +264,9 @@ func TestTest_TestCase_ExternalProviders_NonHashiCorpNamespace(t *testing.T) { t.Parallel() Test(t, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ExternalProvider.Source is protocol version 6 + }, ExternalProviders: map[string]ExternalProvider{ // This can be set to any provider outside the hashicorp namespace. // bflad/scaffoldingtest happens to be a published version of @@ -283,6 +288,9 @@ func TestTest_TestCase_ExternalProvidersAndProviderFactories_NonHashiCorpNamespa t.Parallel() Test(t, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ExternalProvider.Source is protocol version 6 + }, ExternalProviders: map[string]ExternalProvider{ // This can be set to any provider outside the hashicorp namespace. // bflad/scaffoldingtest happens to be a published version of @@ -336,6 +344,9 @@ func TestTest_TestCase_ExternalProviders_Error(t *testing.T) { plugintest.TestExpectTFatal(t, func() { Test(&mockT{}, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version0_13_0), // ExternalProvider.Source + }, ExternalProviders: map[string]ExternalProvider{ "testnonexistent": { Source: "registry.terraform.io/hashicorp/testnonexistent", @@ -355,9 +366,7 @@ func TestTest_TestCase_ProtoV5ProviderFactories(t *testing.T) { Test(&mockT{}, TestCase{ ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ - "test": func() (tfprotov5.ProviderServer, error) { //nolint:unparam // required signature - return nil, nil - }, + "test": providerserver.NewProtov5ProviderServer(testprovider.Protov5Provider{}), }, Steps: []TestStep{ { @@ -391,9 +400,7 @@ func TestTest_TestCase_ProtoV6ProviderFactories(t *testing.T) { Test(&mockT{}, TestCase{ ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature - return nil, nil - }, + "test": providerserver.NewProviderServer(testprovider.Provider{}), }, Steps: []TestStep{ { @@ -428,7 +435,7 @@ func TestTest_TestCase_ProviderFactories(t *testing.T) { Test(&mockT{}, TestCase{ ProviderFactories: map[string]func() (*schema.Provider, error){ "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return nil, nil + return &schema.Provider{}, nil }, }, Steps: []TestStep{ diff --git a/helper/resource/testcase_test.go b/helper/resource/testcase_test.go new file mode 100644 index 000000000..cad9147fe --- /dev/null +++ b/helper/resource/testcase_test.go @@ -0,0 +1,122 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package resource + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/datasource" + "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/tfversion" +) + +// Reference: https://github.com/hashicorp/terraform-plugin-testing/issues/84 +func TestTestCase_NoDataSourceIdRequirement(t *testing.T) { + t.Parallel() + + UnitTest(t, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + }, + Steps: []TestStep{ + { + Check: ComposeAggregateTestCheckFunc( + TestCheckNoResourceAttr("data.test_datasource.test", "id"), + TestCheckResourceAttr("data.test_datasource.test", "not_id", "test"), + ), + Config: `data "test_datasource" "test" {}`, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + DataSources: map[string]testprovider.DataSource{ + "test_datasource": { + ReadResponse: &datasource.ReadResponse{ + State: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "not_id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "not_id": tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, + SchemaResponse: &datasource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "not_id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + }, + }, + }) +} + +// Reference: https://github.com/hashicorp/terraform-plugin-testing/issues/84 +func TestTestCase_NoResourceIdRequirement(t *testing.T) { + t.Parallel() + + UnitTest(t, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + }, + Steps: []TestStep{ + { + Check: ComposeAggregateTestCheckFunc( + TestCheckNoResourceAttr("test_resource.test", "id"), + TestCheckResourceAttr("test_resource.test", "not_id", "test"), + ), + Config: `resource "test_resource" "test" {}`, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "not_id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "not_id": tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "not_id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + }, + }, + }) +} diff --git a/helper/resource/testcase_validate.go b/helper/resource/testcase_validate.go index 7f2425394..6640f8c84 100644 --- a/helper/resource/testcase_validate.go +++ b/helper/resource/testcase_validate.go @@ -7,9 +7,18 @@ import ( "context" "fmt" + "github.com/mitchellh/go-testing-interface" + + "github.com/hashicorp/terraform-plugin-testing/config" "github.com/hashicorp/terraform-plugin-testing/internal/logging" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" ) +// hasProviders returns true if the TestCase has ExternalProviders set. +func (c TestCase) hasExternalProviders(_ context.Context) bool { + return len(c.ExternalProviders) > 0 +} + // hasProviders returns true if the TestCase has set any of the // ExternalProviders, ProtoV5ProviderFactories, ProtoV6ProviderFactories, // ProviderFactories, or Providers fields. @@ -42,7 +51,7 @@ func (c TestCase) hasProviders(_ context.Context) bool { // - No overlapping ExternalProviders and Providers entries // - No overlapping ExternalProviders and ProviderFactories entries // - TestStep validations performed by the (TestStep).validate() method. -func (c TestCase) validate(ctx context.Context) error { +func (c TestCase) validate(ctx context.Context, t testing.T) error { logging.HelperResourceTrace(ctx, "Validating TestCase") if len(c.Steps) == 0 { @@ -65,13 +74,30 @@ func (c TestCase) validate(ctx context.Context) error { } } + testCaseHasExternalProviders := c.hasExternalProviders(ctx) testCaseHasProviders := c.hasProviders(ctx) for stepIndex, step := range c.Steps { stepNumber := stepIndex + 1 // Use 1-based index for humans + + configRequest := teststep.PrepareConfigurationRequest{ + Directory: step.ConfigDirectory, + File: step.ConfigFile, + Raw: step.Config, + TestStepConfigRequest: config.TestStepConfigRequest{ + StepNumber: stepNumber, + TestName: t.Name(), + }, + }.Exec() + + stepConfiguration := teststep.Configuration(configRequest) + stepValidateReq := testStepValidateRequest{ - StepNumber: stepNumber, - TestCaseHasProviders: testCaseHasProviders, + StepConfiguration: stepConfiguration, + StepNumber: stepNumber, + TestCaseHasExternalProviders: testCaseHasExternalProviders, + TestCaseHasProviders: testCaseHasProviders, + TestName: t.Name(), } err := step.validate(ctx, stepValidateReq) diff --git a/helper/resource/testcase_validate_test.go b/helper/resource/testcase_validate_test.go index 83fc7fdca..7e79492be 100644 --- a/helper/resource/testcase_validate_test.go +++ b/helper/resource/testcase_validate_test.go @@ -14,6 +14,40 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) +func TestTestCaseHasExternalProviders(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + testCase TestCase + expected bool + }{ + "none": { + testCase: TestCase{}, + expected: false, + }, + "externalproviders": { + testCase: TestCase{ + ExternalProviders: map[string]ExternalProvider{ + "test": {}, // does not need to be real + }, + }, + expected: true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := test.testCase.hasExternalProviders(context.Background()) + + if got != test.expected { + t.Errorf("expected %t, got %t", test.expected, got) + } + }) + } +} + func TestTestCaseHasProviders(t *testing.T) { t.Parallel() @@ -68,8 +102,6 @@ func TestTestCaseHasProviders(t *testing.T) { } for name, test := range tests { - name, test := name, test - t.Run(name, func(t *testing.T) { t.Parallel() @@ -148,12 +180,10 @@ func TestTestCaseValidate(t *testing.T) { } for name, test := range tests { - name, test := name, test - t.Run(name, func(t *testing.T) { t.Parallel() - err := test.testCase.validate(context.Background()) + err := test.testCase.validate(context.Background(), t) if err != nil { if test.expectedError == nil { diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory/random.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory/random.tf new file mode 100644 index 000000000..b75cfbb6b --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory/random.tf @@ -0,0 +1,19 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.5.1" + } + } +} + +provider "random" {} + +resource "random_password" "test" { + length = 8 + + numeric = false +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist/random.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist/random.tf new file mode 100644 index 000000000..62dfb2248 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist/random.tf @@ -0,0 +1,19 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.2.0" + } + } +} + +provider "random" {} + +resource "random_password" "test" { + length = 8 + + numeric = false +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_MultipleFiles/provider.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_MultipleFiles/provider.tf new file mode 100644 index 000000000..4e7b51e38 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_MultipleFiles/provider.tf @@ -0,0 +1,4 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +provider "random" {} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_MultipleFiles/random.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_MultipleFiles/random.tf new file mode 100644 index 000000000..6ca8f0bb1 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_MultipleFiles/random.tf @@ -0,0 +1,8 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "random_password" "test" { + length = 8 + + numeric = false +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_MultipleFiles/terraform.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_MultipleFiles/terraform.tf new file mode 100644 index 000000000..52f5ef4ad --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_MultipleFiles/terraform.tf @@ -0,0 +1,11 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.2.0" + } + } +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_MultipleFiles_Vars/provider.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_MultipleFiles_Vars/provider.tf new file mode 100644 index 000000000..4e7b51e38 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_MultipleFiles_Vars/provider.tf @@ -0,0 +1,4 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +provider "random" {} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_MultipleFiles_Vars/random.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_MultipleFiles_Vars/random.tf new file mode 100644 index 000000000..3a652203f --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_MultipleFiles_Vars/random.tf @@ -0,0 +1,8 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "random_password" "test" { + length = var.length + + numeric = var.numeric +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_MultipleFiles_Vars/terraform.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_MultipleFiles_Vars/terraform.tf new file mode 100644 index 000000000..52f5ef4ad --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_MultipleFiles_Vars/terraform.tf @@ -0,0 +1,11 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.2.0" + } + } +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_MultipleFiles_Vars/vars.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_MultipleFiles_Vars/vars.tf new file mode 100644 index 000000000..3db921296 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_MultipleFiles_Vars/vars.tf @@ -0,0 +1,10 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +variable "length" { + type = number +} + +variable "numeric" { + type = bool +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_Vars/random.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_Vars/random.tf new file mode 100644 index 000000000..4aa0668d9 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_Vars/random.tf @@ -0,0 +1,19 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.2.0" + } + } +} + +provider "random" {} + +resource "random_password" "test" { + length = var.length + + numeric = var.numeric +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_Vars/vars.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_Vars/vars.tf new file mode 100644 index 000000000..3db921296 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_Vars/vars.tf @@ -0,0 +1,10 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +variable "length" { + type = number +} + +variable "numeric" { + type = bool +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_MultipleFiles/provider.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_MultipleFiles/provider.tf new file mode 100644 index 000000000..4e7b51e38 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_MultipleFiles/provider.tf @@ -0,0 +1,4 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +provider "random" {} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_MultipleFiles/random.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_MultipleFiles/random.tf new file mode 100644 index 000000000..6ca8f0bb1 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_MultipleFiles/random.tf @@ -0,0 +1,8 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "random_password" "test" { + length = 8 + + numeric = false +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_MultipleFiles/terraform.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_MultipleFiles/terraform.tf new file mode 100644 index 000000000..1aaa98022 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_MultipleFiles/terraform.tf @@ -0,0 +1,11 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.5.1" + } + } +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_MultipleFiles_Vars/provider.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_MultipleFiles_Vars/provider.tf new file mode 100644 index 000000000..4e7b51e38 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_MultipleFiles_Vars/provider.tf @@ -0,0 +1,4 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +provider "random" {} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_MultipleFiles_Vars/random.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_MultipleFiles_Vars/random.tf new file mode 100644 index 000000000..6ca8f0bb1 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_MultipleFiles_Vars/random.tf @@ -0,0 +1,8 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "random_password" "test" { + length = 8 + + numeric = false +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_MultipleFiles_Vars/terraform.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_MultipleFiles_Vars/terraform.tf new file mode 100644 index 000000000..1aaa98022 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_MultipleFiles_Vars/terraform.tf @@ -0,0 +1,11 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.5.1" + } + } +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_MultipleFiles_Vars/vars.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_MultipleFiles_Vars/vars.tf new file mode 100644 index 000000000..3db921296 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_MultipleFiles_Vars/vars.tf @@ -0,0 +1,10 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +variable "length" { + type = number +} + +variable "numeric" { + type = bool +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_Vars/random.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_Vars/random.tf new file mode 100644 index 000000000..66518ceeb --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_Vars/random.tf @@ -0,0 +1,19 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.5.1" + } + } +} + +provider "random" {} + +resource "random_password" "test" { + length = var.length + + numeric = var.numeric +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_Vars/vars.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_Vars/vars.tf new file mode 100644 index 000000000..3db921296 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_Vars/vars.tf @@ -0,0 +1,10 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +variable "length" { + type = number +} + +variable "numeric" { + type = bool +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory/1/random.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory/1/random.tf new file mode 100644 index 000000000..b75cfbb6b --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory/1/random.tf @@ -0,0 +1,19 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.5.1" + } + } +} + +provider "random" {} + +resource "random_password" "test" { + length = 8 + + numeric = false +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist/1/random.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist/1/random.tf new file mode 100644 index 000000000..62dfb2248 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist/1/random.tf @@ -0,0 +1,19 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.2.0" + } + } +} + +provider "random" {} + +resource "random_password" "test" { + length = 8 + + numeric = false +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_MultipleFiles/1/provider.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_MultipleFiles/1/provider.tf new file mode 100644 index 000000000..4e7b51e38 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_MultipleFiles/1/provider.tf @@ -0,0 +1,4 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +provider "random" {} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_MultipleFiles/1/random.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_MultipleFiles/1/random.tf new file mode 100644 index 000000000..6ca8f0bb1 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_MultipleFiles/1/random.tf @@ -0,0 +1,8 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "random_password" "test" { + length = 8 + + numeric = false +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_MultipleFiles/1/terraform.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_MultipleFiles/1/terraform.tf new file mode 100644 index 000000000..52f5ef4ad --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_MultipleFiles/1/terraform.tf @@ -0,0 +1,11 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.2.0" + } + } +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_MultipleFiles_Vars/1/provider.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_MultipleFiles_Vars/1/provider.tf new file mode 100644 index 000000000..4e7b51e38 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_MultipleFiles_Vars/1/provider.tf @@ -0,0 +1,4 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +provider "random" {} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_MultipleFiles_Vars/1/random.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_MultipleFiles_Vars/1/random.tf new file mode 100644 index 000000000..3a652203f --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_MultipleFiles_Vars/1/random.tf @@ -0,0 +1,8 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "random_password" "test" { + length = var.length + + numeric = var.numeric +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_MultipleFiles_Vars/1/terraform.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_MultipleFiles_Vars/1/terraform.tf new file mode 100644 index 000000000..52f5ef4ad --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_MultipleFiles_Vars/1/terraform.tf @@ -0,0 +1,11 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.2.0" + } + } +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_MultipleFiles_Vars/1/vars.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_MultipleFiles_Vars/1/vars.tf new file mode 100644 index 000000000..3db921296 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_MultipleFiles_Vars/1/vars.tf @@ -0,0 +1,10 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +variable "length" { + type = number +} + +variable "numeric" { + type = bool +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_Vars/1/random.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_Vars/1/random.tf new file mode 100644 index 000000000..4aa0668d9 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_Vars/1/random.tf @@ -0,0 +1,19 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.2.0" + } + } +} + +provider "random" {} + +resource "random_password" "test" { + length = var.length + + numeric = var.numeric +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_Vars/1/vars.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_Vars/1/vars.tf new file mode 100644 index 000000000..3db921296 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_Vars/1/vars.tf @@ -0,0 +1,10 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +variable "length" { + type = number +} + +variable "numeric" { + type = bool +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles/1/provider.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles/1/provider.tf new file mode 100644 index 000000000..4e7b51e38 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles/1/provider.tf @@ -0,0 +1,4 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +provider "random" {} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles/1/random.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles/1/random.tf new file mode 100644 index 000000000..6ca8f0bb1 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles/1/random.tf @@ -0,0 +1,8 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "random_password" "test" { + length = 8 + + numeric = false +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles/1/terraform.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles/1/terraform.tf new file mode 100644 index 000000000..1aaa98022 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles/1/terraform.tf @@ -0,0 +1,11 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.5.1" + } + } +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_StepNotHardcoded/1/provider_1.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_StepNotHardcoded/1/provider_1.tf new file mode 100644 index 000000000..4e7b51e38 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_StepNotHardcoded/1/provider_1.tf @@ -0,0 +1,4 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +provider "random" {} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_StepNotHardcoded/1/random_1.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_StepNotHardcoded/1/random_1.tf new file mode 100644 index 000000000..6ca8f0bb1 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_StepNotHardcoded/1/random_1.tf @@ -0,0 +1,8 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "random_password" "test" { + length = 8 + + numeric = false +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_StepNotHardcoded/1/terraform_1.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_StepNotHardcoded/1/terraform_1.tf new file mode 100644 index 000000000..52f5ef4ad --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_StepNotHardcoded/1/terraform_1.tf @@ -0,0 +1,11 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.2.0" + } + } +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_StepNotHardcoded/2/provider_2.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_StepNotHardcoded/2/provider_2.tf new file mode 100644 index 000000000..4e7b51e38 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_StepNotHardcoded/2/provider_2.tf @@ -0,0 +1,4 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +provider "random" {} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_StepNotHardcoded/2/random_2.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_StepNotHardcoded/2/random_2.tf new file mode 100644 index 000000000..f561904df --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_StepNotHardcoded/2/random_2.tf @@ -0,0 +1,8 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "random_password" "test" { + length = 9 + + numeric = false +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_StepNotHardcoded/2/terraform_2.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_StepNotHardcoded/2/terraform_2.tf new file mode 100644 index 000000000..1aaa98022 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_StepNotHardcoded/2/terraform_2.tf @@ -0,0 +1,11 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.5.1" + } + } +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars/1/provider.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars/1/provider.tf new file mode 100644 index 000000000..4e7b51e38 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars/1/provider.tf @@ -0,0 +1,4 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +provider "random" {} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars/1/random.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars/1/random.tf new file mode 100644 index 000000000..3a652203f --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars/1/random.tf @@ -0,0 +1,8 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "random_password" "test" { + length = var.length + + numeric = var.numeric +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars/1/terraform.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars/1/terraform.tf new file mode 100644 index 000000000..1aaa98022 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars/1/terraform.tf @@ -0,0 +1,11 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.5.1" + } + } +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars/1/vars.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars/1/vars.tf new file mode 100644 index 000000000..3db921296 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars/1/vars.tf @@ -0,0 +1,10 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +variable "length" { + type = number +} + +variable "numeric" { + type = bool +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars_StepNotHardcoded/1/provider_1.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars_StepNotHardcoded/1/provider_1.tf new file mode 100644 index 000000000..4e7b51e38 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars_StepNotHardcoded/1/provider_1.tf @@ -0,0 +1,4 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +provider "random" {} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars_StepNotHardcoded/1/random_1.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars_StepNotHardcoded/1/random_1.tf new file mode 100644 index 000000000..3a652203f --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars_StepNotHardcoded/1/random_1.tf @@ -0,0 +1,8 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "random_password" "test" { + length = var.length + + numeric = var.numeric +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars_StepNotHardcoded/1/terraform_1.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars_StepNotHardcoded/1/terraform_1.tf new file mode 100644 index 000000000..52f5ef4ad --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars_StepNotHardcoded/1/terraform_1.tf @@ -0,0 +1,11 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.2.0" + } + } +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars_StepNotHardcoded/1/vars_1.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars_StepNotHardcoded/1/vars_1.tf new file mode 100644 index 000000000..3db921296 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars_StepNotHardcoded/1/vars_1.tf @@ -0,0 +1,10 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +variable "length" { + type = number +} + +variable "numeric" { + type = bool +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars_StepNotHardcoded/2/provider_2.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars_StepNotHardcoded/2/provider_2.tf new file mode 100644 index 000000000..4e7b51e38 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars_StepNotHardcoded/2/provider_2.tf @@ -0,0 +1,4 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +provider "random" {} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars_StepNotHardcoded/2/random2.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars_StepNotHardcoded/2/random2.tf new file mode 100644 index 000000000..3a652203f --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars_StepNotHardcoded/2/random2.tf @@ -0,0 +1,8 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "random_password" "test" { + length = var.length + + numeric = var.numeric +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars_StepNotHardcoded/2/terraform_2.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars_StepNotHardcoded/2/terraform_2.tf new file mode 100644 index 000000000..1aaa98022 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars_StepNotHardcoded/2/terraform_2.tf @@ -0,0 +1,11 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.5.1" + } + } +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars_StepNotHardcoded/2/vars_2.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars_StepNotHardcoded/2/vars_2.tf new file mode 100644 index 000000000..3db921296 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars_StepNotHardcoded/2/vars_2.tf @@ -0,0 +1,10 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +variable "length" { + type = number +} + +variable "numeric" { + type = bool +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_StepNotHardcoded/1/random_1.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_StepNotHardcoded/1/random_1.tf new file mode 100644 index 000000000..62dfb2248 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_StepNotHardcoded/1/random_1.tf @@ -0,0 +1,19 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.2.0" + } + } +} + +provider "random" {} + +resource "random_password" "test" { + length = 8 + + numeric = false +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_StepNotHardcoded/2/random_2.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_StepNotHardcoded/2/random_2.tf new file mode 100644 index 000000000..0a734ea2a --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_StepNotHardcoded/2/random_2.tf @@ -0,0 +1,19 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.5.1" + } + } +} + +provider "random" {} + +resource "random_password" "test" { + length = 9 + + numeric = false +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_Vars/1/random.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_Vars/1/random.tf new file mode 100644 index 000000000..66518ceeb --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_Vars/1/random.tf @@ -0,0 +1,19 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.5.1" + } + } +} + +provider "random" {} + +resource "random_password" "test" { + length = var.length + + numeric = var.numeric +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_Vars/1/vars.tf b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_Vars/1/vars.tf new file mode 100644 index 000000000..3db921296 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_Vars/1/vars.tf @@ -0,0 +1,10 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +variable "length" { + type = number +} + +variable "numeric" { + type = bool +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigFile_TestNameFile/random.tf b/helper/resource/testdata/TestTest_ConfigFile_TestNameFile/random.tf new file mode 100644 index 000000000..b75cfbb6b --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigFile_TestNameFile/random.tf @@ -0,0 +1,19 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.5.1" + } + } +} + +provider "random" {} + +resource "random_password" "test" { + length = 8 + + numeric = false +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigFile_TestNameFile_AttributeDoesNotExist/random.tf b/helper/resource/testdata/TestTest_ConfigFile_TestNameFile_AttributeDoesNotExist/random.tf new file mode 100644 index 000000000..62dfb2248 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigFile_TestNameFile_AttributeDoesNotExist/random.tf @@ -0,0 +1,19 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.2.0" + } + } +} + +provider "random" {} + +resource "random_password" "test" { + length = 8 + + numeric = false +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigFile_TestNameFile_AttributeDoesNotExist_Vars/random.tf b/helper/resource/testdata/TestTest_ConfigFile_TestNameFile_AttributeDoesNotExist_Vars/random.tf new file mode 100644 index 000000000..663f29c5b --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigFile_TestNameFile_AttributeDoesNotExist_Vars/random.tf @@ -0,0 +1,27 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.2.0" + } + } +} + +provider "random" {} + +resource "random_password" "test" { + length = var.length + + numeric = var.numeric +} + +variable "length" { + type = number +} + +variable "numeric" { + type = bool +} diff --git a/helper/resource/testdata/TestTest_ConfigFile_TestNameFile_Vars/random.tf b/helper/resource/testdata/TestTest_ConfigFile_TestNameFile_Vars/random.tf new file mode 100644 index 000000000..413a27bcb --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigFile_TestNameFile_Vars/random.tf @@ -0,0 +1,27 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.5.1" + } + } +} + +provider "random" {} + +resource "random_password" "test" { + length = var.length + + numeric = var.numeric +} + +variable "length" { + type = number +} + +variable "numeric" { + type = bool +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigFile_TestStepFile/1/random.tf b/helper/resource/testdata/TestTest_ConfigFile_TestStepFile/1/random.tf new file mode 100644 index 000000000..b75cfbb6b --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigFile_TestStepFile/1/random.tf @@ -0,0 +1,19 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.5.1" + } + } +} + +provider "random" {} + +resource "random_password" "test" { + length = 8 + + numeric = false +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigFile_TestStepFile_AttributeDoesNotExist/1/random.tf b/helper/resource/testdata/TestTest_ConfigFile_TestStepFile_AttributeDoesNotExist/1/random.tf new file mode 100644 index 000000000..62dfb2248 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigFile_TestStepFile_AttributeDoesNotExist/1/random.tf @@ -0,0 +1,19 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.2.0" + } + } +} + +provider "random" {} + +resource "random_password" "test" { + length = 8 + + numeric = false +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigFile_TestStepFile_AttributeDoesNotExist_Vars/1/random.tf b/helper/resource/testdata/TestTest_ConfigFile_TestStepFile_AttributeDoesNotExist_Vars/1/random.tf new file mode 100644 index 000000000..09ef72465 --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigFile_TestStepFile_AttributeDoesNotExist_Vars/1/random.tf @@ -0,0 +1,27 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.2.0" + } + } +} + +provider "random" {} + +resource "random_password" "test" { + length = var.length + + numeric = var.numeric +} + +variable "length" { + type = number +} + +variable "numeric" { + type = bool +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_ConfigFile_TestStepFile_Vars/1/random.tf b/helper/resource/testdata/TestTest_ConfigFile_TestStepFile_Vars/1/random.tf new file mode 100644 index 000000000..413a27bcb --- /dev/null +++ b/helper/resource/testdata/TestTest_ConfigFile_TestStepFile_Vars/1/random.tf @@ -0,0 +1,27 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.5.1" + } + } +} + +provider "random" {} + +resource "random_password" "test" { + length = var.length + + numeric = var.numeric +} + +variable "length" { + type = number +} + +variable "numeric" { + type = bool +} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_TestStep_ProviderFactories_ConfigDirectory_TestNameDirectory/random.tf b/helper/resource/testdata/TestTest_TestStep_ProviderFactories_ConfigDirectory_TestNameDirectory/random.tf new file mode 100644 index 000000000..a47a750cc --- /dev/null +++ b/helper/resource/testdata/TestTest_TestStep_ProviderFactories_ConfigDirectory_TestNameDirectory/random.tf @@ -0,0 +1,4 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "random_id" "test" {} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_TestStep_ProviderFactories_ConfigDirectory_TestStepDirectory/1/random.tf b/helper/resource/testdata/TestTest_TestStep_ProviderFactories_ConfigDirectory_TestStepDirectory/1/random.tf new file mode 100644 index 000000000..a47a750cc --- /dev/null +++ b/helper/resource/testdata/TestTest_TestStep_ProviderFactories_ConfigDirectory_TestStepDirectory/1/random.tf @@ -0,0 +1,4 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "random_id" "test" {} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_TestStep_ProviderFactories_ConfigFile_TestNameFile/random.tf b/helper/resource/testdata/TestTest_TestStep_ProviderFactories_ConfigFile_TestNameFile/random.tf new file mode 100644 index 000000000..a47a750cc --- /dev/null +++ b/helper/resource/testdata/TestTest_TestStep_ProviderFactories_ConfigFile_TestNameFile/random.tf @@ -0,0 +1,4 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "random_id" "test" {} \ No newline at end of file diff --git a/helper/resource/testdata/TestTest_TestStep_ProviderFactories_ConfigFile_TestStepFile/1/random.tf b/helper/resource/testdata/TestTest_TestStep_ProviderFactories_ConfigFile_TestStepFile/1/random.tf new file mode 100644 index 000000000..a47a750cc --- /dev/null +++ b/helper/resource/testdata/TestTest_TestStep_ProviderFactories_ConfigFile_TestStepFile/1/random.tf @@ -0,0 +1,4 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "random_id" "test" {} \ No newline at end of file diff --git a/helper/resource/testdata/fixtures/random_id/random.tf b/helper/resource/testdata/fixtures/random_id/random.tf new file mode 100644 index 000000000..a47a750cc --- /dev/null +++ b/helper/resource/testdata/fixtures/random_id/random.tf @@ -0,0 +1,4 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "random_id" "test" {} \ No newline at end of file diff --git a/helper/resource/testdata/fixtures/random_password_3.2.0/random.tf b/helper/resource/testdata/fixtures/random_password_3.2.0/random.tf new file mode 100644 index 000000000..62dfb2248 --- /dev/null +++ b/helper/resource/testdata/fixtures/random_password_3.2.0/random.tf @@ -0,0 +1,19 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.2.0" + } + } +} + +provider "random" {} + +resource "random_password" "test" { + length = 8 + + numeric = false +} \ No newline at end of file diff --git a/helper/resource/testdata/fixtures/random_password_3.2.0_multiple_files/provider.tf b/helper/resource/testdata/fixtures/random_password_3.2.0_multiple_files/provider.tf new file mode 100644 index 000000000..4e7b51e38 --- /dev/null +++ b/helper/resource/testdata/fixtures/random_password_3.2.0_multiple_files/provider.tf @@ -0,0 +1,4 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +provider "random" {} \ No newline at end of file diff --git a/helper/resource/testdata/fixtures/random_password_3.2.0_multiple_files/random.tf b/helper/resource/testdata/fixtures/random_password_3.2.0_multiple_files/random.tf new file mode 100644 index 000000000..6ca8f0bb1 --- /dev/null +++ b/helper/resource/testdata/fixtures/random_password_3.2.0_multiple_files/random.tf @@ -0,0 +1,8 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "random_password" "test" { + length = 8 + + numeric = false +} \ No newline at end of file diff --git a/helper/resource/testdata/fixtures/random_password_3.2.0_multiple_files/terraform.tf b/helper/resource/testdata/fixtures/random_password_3.2.0_multiple_files/terraform.tf new file mode 100644 index 000000000..52f5ef4ad --- /dev/null +++ b/helper/resource/testdata/fixtures/random_password_3.2.0_multiple_files/terraform.tf @@ -0,0 +1,11 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.2.0" + } + } +} \ No newline at end of file diff --git a/helper/resource/testdata/fixtures/random_password_3.2.0_multiple_files_vars/provider.tf b/helper/resource/testdata/fixtures/random_password_3.2.0_multiple_files_vars/provider.tf new file mode 100644 index 000000000..4e7b51e38 --- /dev/null +++ b/helper/resource/testdata/fixtures/random_password_3.2.0_multiple_files_vars/provider.tf @@ -0,0 +1,4 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +provider "random" {} \ No newline at end of file diff --git a/helper/resource/testdata/fixtures/random_password_3.2.0_multiple_files_vars/random.tf b/helper/resource/testdata/fixtures/random_password_3.2.0_multiple_files_vars/random.tf new file mode 100644 index 000000000..3a652203f --- /dev/null +++ b/helper/resource/testdata/fixtures/random_password_3.2.0_multiple_files_vars/random.tf @@ -0,0 +1,8 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "random_password" "test" { + length = var.length + + numeric = var.numeric +} \ No newline at end of file diff --git a/helper/resource/testdata/fixtures/random_password_3.2.0_multiple_files_vars/terraform.tf b/helper/resource/testdata/fixtures/random_password_3.2.0_multiple_files_vars/terraform.tf new file mode 100644 index 000000000..52f5ef4ad --- /dev/null +++ b/helper/resource/testdata/fixtures/random_password_3.2.0_multiple_files_vars/terraform.tf @@ -0,0 +1,11 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.2.0" + } + } +} \ No newline at end of file diff --git a/helper/resource/testdata/fixtures/random_password_3.2.0_multiple_files_vars/vars.tf b/helper/resource/testdata/fixtures/random_password_3.2.0_multiple_files_vars/vars.tf new file mode 100644 index 000000000..3db921296 --- /dev/null +++ b/helper/resource/testdata/fixtures/random_password_3.2.0_multiple_files_vars/vars.tf @@ -0,0 +1,10 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +variable "length" { + type = number +} + +variable "numeric" { + type = bool +} \ No newline at end of file diff --git a/helper/resource/testdata/fixtures/random_password_3.2.0_vars/random.tf b/helper/resource/testdata/fixtures/random_password_3.2.0_vars/random.tf new file mode 100644 index 000000000..4aa0668d9 --- /dev/null +++ b/helper/resource/testdata/fixtures/random_password_3.2.0_vars/random.tf @@ -0,0 +1,19 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.2.0" + } + } +} + +provider "random" {} + +resource "random_password" "test" { + length = var.length + + numeric = var.numeric +} \ No newline at end of file diff --git a/helper/resource/testdata/fixtures/random_password_3.2.0_vars/vars.tf b/helper/resource/testdata/fixtures/random_password_3.2.0_vars/vars.tf new file mode 100644 index 000000000..3db921296 --- /dev/null +++ b/helper/resource/testdata/fixtures/random_password_3.2.0_vars/vars.tf @@ -0,0 +1,10 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +variable "length" { + type = number +} + +variable "numeric" { + type = bool +} \ No newline at end of file diff --git a/helper/resource/testdata/fixtures/random_password_3.2.0_vars_single_file/random.tf b/helper/resource/testdata/fixtures/random_password_3.2.0_vars_single_file/random.tf new file mode 100644 index 000000000..09ef72465 --- /dev/null +++ b/helper/resource/testdata/fixtures/random_password_3.2.0_vars_single_file/random.tf @@ -0,0 +1,27 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.2.0" + } + } +} + +provider "random" {} + +resource "random_password" "test" { + length = var.length + + numeric = var.numeric +} + +variable "length" { + type = number +} + +variable "numeric" { + type = bool +} \ No newline at end of file diff --git a/helper/resource/testdata/fixtures/random_password_3.5.1/random.tf b/helper/resource/testdata/fixtures/random_password_3.5.1/random.tf new file mode 100644 index 000000000..b75cfbb6b --- /dev/null +++ b/helper/resource/testdata/fixtures/random_password_3.5.1/random.tf @@ -0,0 +1,19 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.5.1" + } + } +} + +provider "random" {} + +resource "random_password" "test" { + length = 8 + + numeric = false +} \ No newline at end of file diff --git a/helper/resource/testdata/fixtures/random_password_3.5.1_multiple_files/provider.tf b/helper/resource/testdata/fixtures/random_password_3.5.1_multiple_files/provider.tf new file mode 100644 index 000000000..4e7b51e38 --- /dev/null +++ b/helper/resource/testdata/fixtures/random_password_3.5.1_multiple_files/provider.tf @@ -0,0 +1,4 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +provider "random" {} \ No newline at end of file diff --git a/helper/resource/testdata/fixtures/random_password_3.5.1_multiple_files/random.tf b/helper/resource/testdata/fixtures/random_password_3.5.1_multiple_files/random.tf new file mode 100644 index 000000000..6ca8f0bb1 --- /dev/null +++ b/helper/resource/testdata/fixtures/random_password_3.5.1_multiple_files/random.tf @@ -0,0 +1,8 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "random_password" "test" { + length = 8 + + numeric = false +} \ No newline at end of file diff --git a/helper/resource/testdata/fixtures/random_password_3.5.1_multiple_files/terraform.tf b/helper/resource/testdata/fixtures/random_password_3.5.1_multiple_files/terraform.tf new file mode 100644 index 000000000..1aaa98022 --- /dev/null +++ b/helper/resource/testdata/fixtures/random_password_3.5.1_multiple_files/terraform.tf @@ -0,0 +1,11 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.5.1" + } + } +} \ No newline at end of file diff --git a/helper/resource/testdata/fixtures/random_password_3.5.1_multiple_files_vars/provider.tf b/helper/resource/testdata/fixtures/random_password_3.5.1_multiple_files_vars/provider.tf new file mode 100644 index 000000000..4e7b51e38 --- /dev/null +++ b/helper/resource/testdata/fixtures/random_password_3.5.1_multiple_files_vars/provider.tf @@ -0,0 +1,4 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +provider "random" {} \ No newline at end of file diff --git a/helper/resource/testdata/fixtures/random_password_3.5.1_multiple_files_vars/random.tf b/helper/resource/testdata/fixtures/random_password_3.5.1_multiple_files_vars/random.tf new file mode 100644 index 000000000..3a652203f --- /dev/null +++ b/helper/resource/testdata/fixtures/random_password_3.5.1_multiple_files_vars/random.tf @@ -0,0 +1,8 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "random_password" "test" { + length = var.length + + numeric = var.numeric +} \ No newline at end of file diff --git a/helper/resource/testdata/fixtures/random_password_3.5.1_multiple_files_vars/terraform.tf b/helper/resource/testdata/fixtures/random_password_3.5.1_multiple_files_vars/terraform.tf new file mode 100644 index 000000000..1aaa98022 --- /dev/null +++ b/helper/resource/testdata/fixtures/random_password_3.5.1_multiple_files_vars/terraform.tf @@ -0,0 +1,11 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.5.1" + } + } +} \ No newline at end of file diff --git a/helper/resource/testdata/fixtures/random_password_3.5.1_multiple_files_vars/vars.tf b/helper/resource/testdata/fixtures/random_password_3.5.1_multiple_files_vars/vars.tf new file mode 100644 index 000000000..3db921296 --- /dev/null +++ b/helper/resource/testdata/fixtures/random_password_3.5.1_multiple_files_vars/vars.tf @@ -0,0 +1,10 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +variable "length" { + type = number +} + +variable "numeric" { + type = bool +} \ No newline at end of file diff --git a/helper/resource/testdata/fixtures/random_password_3.5.1_vars/random.tf b/helper/resource/testdata/fixtures/random_password_3.5.1_vars/random.tf new file mode 100644 index 000000000..66518ceeb --- /dev/null +++ b/helper/resource/testdata/fixtures/random_password_3.5.1_vars/random.tf @@ -0,0 +1,19 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.5.1" + } + } +} + +provider "random" {} + +resource "random_password" "test" { + length = var.length + + numeric = var.numeric +} \ No newline at end of file diff --git a/helper/resource/testdata/fixtures/random_password_3.5.1_vars/vars.tf b/helper/resource/testdata/fixtures/random_password_3.5.1_vars/vars.tf new file mode 100644 index 000000000..3db921296 --- /dev/null +++ b/helper/resource/testdata/fixtures/random_password_3.5.1_vars/vars.tf @@ -0,0 +1,10 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +variable "length" { + type = number +} + +variable "numeric" { + type = bool +} \ No newline at end of file diff --git a/helper/resource/testdata/fixtures/random_password_3.5.1_vars_single_file/random.tf b/helper/resource/testdata/fixtures/random_password_3.5.1_vars_single_file/random.tf new file mode 100644 index 000000000..413a27bcb --- /dev/null +++ b/helper/resource/testdata/fixtures/random_password_3.5.1_vars_single_file/random.tf @@ -0,0 +1,27 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.5.1" + } + } +} + +provider "random" {} + +resource "random_password" "test" { + length = var.length + + numeric = var.numeric +} + +variable "length" { + type = number +} + +variable "numeric" { + type = bool +} \ No newline at end of file diff --git a/helper/resource/testing.go b/helper/resource/testing.go index be7c01c6c..61c03ff72 100644 --- a/helper/resource/testing.go +++ b/helper/resource/testing.go @@ -15,7 +15,8 @@ import ( "strings" "time" - "github.com/hashicorp/go-multierror" + "github.com/hashicorp/terraform-plugin-testing/querycheck" + "github.com/mitchellh/go-testing-interface" "github.com/hashicorp/terraform-plugin-go/tfprotov5" @@ -23,7 +24,9 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-testing/config" "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/hashicorp/terraform-plugin-testing/tfversion" @@ -108,7 +111,7 @@ func AddTestSweepers(name string, s *Sweeper) { // Sweeper flags added to the "go test" command: // // -sweep: Comma-separated list of locations/regions to run available sweepers. -// -sweep-allow-failues: Enable to allow other sweepers to run after failures. +// -sweep-allow-failures: Enable to allow other sweepers to run after failures. // -sweep-run: Comma-separated list of resource type sweepers to run. Defaults // to all sweepers. // @@ -162,7 +165,7 @@ func runSweepers(regions []string, sweepers map[string]*Sweeper, allowFailures b log.Printf("Sweeper Tests for region (%s) ran successfully:\n", region) for sweeper, sweeperErr := range regionSweeperRunList { if sweeperErr == nil { - fmt.Printf("\t- %s\n", sweeper) + log.Printf("\t- %s\n", sweeper) } else { regionSweeperErrorFound = true } @@ -173,7 +176,7 @@ func runSweepers(regions []string, sweepers map[string]*Sweeper, allowFailures b log.Printf("Sweeper Tests for region (%s) ran unsuccessfully:\n", region) for sweeper, sweeperErr := range regionSweeperRunList { if sweeperErr != nil { - fmt.Printf("\t- %s: %s\n", sweeper, sweeperErr) + log.Printf("\t- %s: %s\n", sweeper, sweeperErr) } } } @@ -407,6 +410,11 @@ type TestCase struct { // ErrorCheck allows providers the option to handle errors such as skipping // tests based on certain errors. + // + // This functionality is only intended for provider-controlled error + // messaging. While in certain scenarios this can also catch testing logic + // error messages, those messages are not protected by compatibility + // promises. ErrorCheck ErrorCheckFunc // Steps are the apply sequences done within the context of the @@ -435,6 +443,10 @@ type TestCase struct { // set to "1", to persist any working directory files. Otherwise, this directory is // automatically cleaned up at the end of the TestCase. WorkingDir string + + // AdditionalCLIOptions allows an intentionally limited set of options to be passed + // to the Terraform CLI when executing test steps. + AdditionalCLIOptions *AdditionalCLIOptions } // ExternalProvider holds information about third-party providers that should @@ -444,6 +456,37 @@ type ExternalProvider struct { Source string // the provider source } +type ImportStateKind byte + +const ( + // ImportCommandWithID tests import by using the ID string with the `terraform import` command + ImportCommandWithID ImportStateKind = iota + + // ImportBlockWithID tests import by using the ID string in an import configuration block with the `terraform plan` command + ImportBlockWithID + + // ImportBlockWithResourceIdentity imports the state using an import block with a resource identity + ImportBlockWithResourceIdentity +) + +// plannable reports whether this kind indicates the use of plannable import blocks +func (kind ImportStateKind) plannable() bool { + return kind == ImportBlockWithID || kind == ImportBlockWithResourceIdentity +} + +// resourceIdentity reports whether this kind indicates the use of resource identity in import blocks +func (kind ImportStateKind) resourceIdentity() bool { + return kind == ImportBlockWithResourceIdentity +} + +func (kind ImportStateKind) String() string { + return map[ImportStateKind]string{ + ImportCommandWithID: "ImportCommandWithID", + ImportBlockWithID: "ImportBlockWithID", + ImportBlockWithResourceIdentity: "ImportBlockWithResourceIdentity", + }[kind] +} + // TestStep is a single apply sequence of a test, done within the // context of a state. // @@ -490,12 +533,70 @@ type TestStep struct { // Config a string of the configuration to give to Terraform. If this // is set, then the TestCase will execute this step with the same logic - // as a `terraform apply`. + // as a `terraform apply`. If both Config and ConfigDirectory are set + // an error will be returned. // // JSON Configuration Syntax can be used and is assumed whenever Config // contains valid JSON. + // + // Only one of Config, ConfigDirectory or ConfigFile can be set + // otherwise an error will be returned. Config string + // ConfigDirectory is a function which returns a function that + // accepts config.TestStepProviderConfig and returns a string + // representing a directory that contains Terraform + // configuration files. + // + // There are helper functions in the [config] package that can be used, + // such as: + // + // - [config.StaticDirectory] + // - [config.TestNameDirectory] + // - [config.TestStepDirectory] + // + // When running Terraform operations for the test, Terraform will + // be executed with copies of the files of this directory as its + // working directory. Only one of Config, ConfigDirectory or + // ConfigFile can be set otherwise an error will be returned. + ConfigDirectory config.TestStepConfigFunc + + // ConfigFile is a function which returns a function that + // accepts config.TestStepProviderConfig and returns a string + // representing a file that contains Terraform configuration. + // + // There are helper functions in the [config] package that can be used, + // such as: + // + // - [config.StaticFile] + // - [config.TestNameFile] + // - [config.TestStepFile] + // + // When running Terraform operations for the test, Terraform will + // be executed with a copy of the file as its working directory. + // Only one of Config, ConfigDirectory or ConfigFile can be set + // otherwise an error will be returned. + ConfigFile config.TestStepConfigFunc + + // ImportStateConfigExact indicates that the test framework should use the exact + // content of the Config, ConfigFile, or ConfigDirectory inputs and should + // not modify it at test run time. + // + // The default is false. At test run time, the test framework will generate + // specific kinds of configuration, such as import blocks, and append them + // to the given Config, ConfigFile, or ConfigDirectory inputs. Using this + // default improves test readability and removes duplication of setup. + ImportStateConfigExact bool + + // ConfigVariables is a map defining variables for use in conjunction + // with Terraform configuration. If this map is populated then it + // will be used to assemble an *.auto.tfvars.json which will be + // written into the working directory. Any variables that are + // defined within the Terraform configuration that have a matching + // variable definition in *.auto.tfvars.json will have their value + // substituted when the acceptance test is executed. + ConfigVariables config.Variables + // Check is called after the Config is applied. Use this step to // make your own API calls to check the status of things, and to // inspect the format of the ResourceState itself. @@ -516,6 +617,11 @@ type TestStep struct { // ExpectError allows the construction of test cases that we expect to fail // with an error. The specified regexp must match against the error for the // test to pass. + // + // This functionality is only intended for provider-controlled error + // messaging. While in certain scenarios this can also catch testing logic + // error messages, those messages are not protected by compatibility + // promises. ExpectError *regexp.Regexp // ConfigPlanChecks allows assertions to be made against the plan file at different points of a Config (apply) test using a plan check. @@ -532,6 +638,14 @@ type TestStep struct { // [plancheck]: https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck RefreshPlanChecks RefreshPlanChecks + // ConfigStateChecks allow assertions to be made against the state file during a Config (apply) test using a state check. + // Custom state checks can be created by implementing the [statecheck.StateCheck] interface, or by using a StateCheck implementation from the provided [statecheck] package. + ConfigStateChecks []statecheck.StateCheck + + // QueryResultChecks allow assertions to be made against a collection of found resources that were returned by a query using a query check. + // Custom query checks can be created by implementing the [querycheck.QueryResultCheck] interface, or by using a QueryResultCheck implementation from the provided [querycheck] package. + QueryResultChecks []querycheck.QueryResultCheck + // PlanOnly can be set to only run `plan` with this configuration, and not // actually apply it. This is useful for ensuring config changes result in // no-op plans @@ -566,6 +680,13 @@ type TestStep struct { // ID of that resource. ImportState bool + // ImportStateKind controls the method of import that is used in combination with the other import-related fields on the TestStep struct. + // + // - By default, ImportCommandWithID is used, which tests import by using the ID string with the `terraform import` command. This was the original behavior prior to introducing the ImportStateKind field. + // - ImportBlockWithID tests import by using the ID string in an import configuration block with the `terraform plan` command. + // - ImportBlockWithResourceIdentity imports the state using an import configuration block with a resource identity. + ImportStateKind ImportStateKind + // ImportStateId is the ID to perform an ImportState operation with. // This is optional. If it isn't set, then the resource ID is automatically // determined by inspecting the state for ResourceName's ID. @@ -599,15 +720,36 @@ type TestStep struct { // Terraform version specific logic in provider testing. ImportStateCheck ImportStateCheckFunc + // ImportPlanChecks allows assertions to be made against the plan file at different points of a plannable import test using a plan check. + // Custom plan checks can be created by implementing the [PlanCheck] interface, or by using a PlanCheck implementation from the provided [plancheck] package + // + // [PlanCheck]: https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck#PlanCheck + // [plancheck]: https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck + ImportPlanChecks ImportPlanChecks + // ImportStateVerify, if true, will also check that the state values // that are finally put into the state after import match for all the // IDs returned by the Import. Note that this checks for strict equality // and does not respect DiffSuppressFunc or CustomizeDiff. // + // By default, the prior resource state and import resource state are + // matched by the "id" attribute. If the "id" attribute is not implemented + // or another attribute more uniquely identifies the resource, set the + // ImportStateVerifyIdentifierAttribute field to adjust the attribute for + // matching. + // + // If certain attributes cannot be correctly imported, set the + // ImportStateVerifyIgnore field. + ImportStateVerify bool + + // ImportStateVerifyIdentifierAttribute is the resource attribute for + // matching the prior resource state and import resource state during import + // verification. By default, the "id" attribute is used. + ImportStateVerifyIdentifierAttribute string + // ImportStateVerifyIgnore is a list of prefixes of fields that should // not be verified to be equal. These can be set to ephemeral fields or // fields that can't be refreshed and don't matter. - ImportStateVerify bool ImportStateVerifyIgnore []string // ImportStatePersist, if true, will update the persisted state with the @@ -699,6 +841,9 @@ type TestStep struct { // for performing import testing where the prior TestStep configuration // contained a provider outside the one under test. ExternalProviders map[string]ExternalProvider + + // If true, the test step will run the query command + Query bool } // ConfigPlanChecks defines the different points in a Config TestStep when plan checks can be run. @@ -716,6 +861,13 @@ type ConfigPlanChecks struct { PostApplyPostRefresh []plancheck.PlanCheck } +// ImportPlanChecks defines the different points in an Import TestStep when plan checks can be run. +type ImportPlanChecks struct { + // PreApply runs all plan checks in the slice. This occurs after the plan of an Import test is computed. This slice cannot be populated + // with TestStep.PlanOnly, as there is no PreApply plan run with that flag set. All errors by plan checks in this slice are aggregated, reported, and will result in a test failure. + PreApply []plancheck.PlanCheck +} + // RefreshPlanChecks defines the different points in a Refresh TestStep when plan checks can be run. type RefreshPlanChecks struct { // PostRefresh runs all plan checks in the slice. This occurs after the refresh of the Refresh test is run. @@ -743,11 +895,6 @@ func ParallelTest(t testing.T, c TestCase) { // set to some non-empty value. This is to avoid test cases surprising // a user by creating real resources. // -// Tests will fail unless the verbose flag (`go test -v`, or explicitly -// the "-test.v" flag) is set. Because some acceptance tests take quite -// long, we require the verbose flag so users are able to see progress -// output. -// // Use the ParallelTest() function to automatically set (*testing.T).Parallel() // to enable testing concurrency. Use the UnitTest() function to automatically // set the TestCase type IsUnitTest field. @@ -774,7 +921,7 @@ func Test(t testing.T, c TestCase) { ctx := context.Background() ctx = logging.InitTestContext(ctx, t) - err := c.validate(ctx) + err := c.validate(ctx, t) if err != nil { logging.HelperResourceError(ctx, @@ -835,11 +982,7 @@ func Test(t testing.T, c TestCase) { // This is done after creating the helper because a working directory is required // to retrieve the Terraform version. if c.TerraformVersionChecks != nil { - logging.HelperResourceDebug(ctx, "Calling TestCase Terraform version checks") - runTFVersionChecks(ctx, t, helper.TerraformVersion(), c.TerraformVersionChecks) - - logging.HelperResourceDebug(ctx, "Called TestCase Terraform version checks") } runNewTest(ctx, t, c, helper) @@ -859,17 +1002,17 @@ func UnitTest(t testing.T, c TestCase) { Test(t, c) } -func testResource(c TestStep, state *terraform.State) (*terraform.ResourceState, error) { +func testResource(name string, state *terraform.State) (*terraform.ResourceState, error) { for _, m := range state.Modules { if len(m.Resources) > 0 { - if v, ok := m.Resources[c.ResourceName]; ok { + if v, ok := m.Resources[name]; ok { return v, nil } } } return nil, fmt.Errorf( - "Resource specified by ResourceName couldn't be found: %s", c.ResourceName) + "Resource specified by ResourceName couldn't be found: %s", name) } // ComposeTestCheckFunc lets you compose multiple TestCheckFuncs into @@ -884,7 +1027,7 @@ func ComposeTestCheckFunc(fs ...TestCheckFunc) TestCheckFunc { return func(s *terraform.State) error { for i, f := range fs { if err := f(s); err != nil { - return fmt.Errorf("Check %d/%d error: %s", i+1, len(fs), err) + return fmt.Errorf("Check %d/%d error: %w", i+1, len(fs), err) } } @@ -902,15 +1045,15 @@ func ComposeTestCheckFunc(fs ...TestCheckFunc) TestCheckFunc { // TestCheckFuncs and aggregates failures. func ComposeAggregateTestCheckFunc(fs ...TestCheckFunc) TestCheckFunc { return func(s *terraform.State) error { - var result *multierror.Error + var result []error for i, f := range fs { if err := f(s); err != nil { - result = multierror.Append(result, fmt.Errorf("Check %d/%d error: %s", i+1, len(fs), err)) + result = append(result, fmt.Errorf("Check %d/%d error: %w", i+1, len(fs), err)) } } - return result.ErrorOrNil() + return errors.Join(result...) } } @@ -956,6 +1099,44 @@ func ComposeAggregateTestCheckFunc(fs ...TestCheckFunc) TestCheckFunc { // attributes using the special key syntax, checking a list, map, or set // attribute directly is not supported. Use TestCheckResourceAttr with // the special .# or .% key syntax for those situations instead. +// +// An experimental interface exists to potentially replace the +// TestCheckResourceAttrSet functionality in the future and feedback +// would be appreciated. This example performs the same check as +// TestCheckResourceAttrSet with that experimental interface, by +// using [ExpectKnownValue] with [knownvalue.NotNull]: +// +// package example_test +// +// import ( +// "testing" +// +// "github.com/hashicorp/terraform-plugin-testing/helper/resource" +// "github.com/hashicorp/terraform-plugin-testing/knownvalue" +// "github.com/hashicorp/terraform-plugin-testing/statecheck" +// "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +// ) +// +// func TestExpectKnownValue_CheckState_AttributeFound(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" {}`, +// ConfigStateChecks: []statecheck.StateCheck{ +// statecheck.ExpectKnownValue( +// "test_resource.one", +// tfjsonpath.New("computed_attribute"), +// knownvalue.NotNull(), +// ), +// }, +// }, +// }, +// }) +// } func TestCheckResourceAttrSet(name, key string) TestCheckFunc { return checkIfIndexesIntoTypeSet(key, func(s *terraform.State) error { is, err := primaryInstanceState(s, name) @@ -1055,6 +1236,44 @@ func testCheckResourceAttrSet(is *terraform.InstanceState, name string, key stri // - Boolean: "false" or "true". // - Float/Integer: Stringified number, such as "1.2" or "123". // - String: No conversion necessary. +// +// An experimental interface exists to potentially replace the +// TestCheckResourceAttr functionality in the future and feedback +// would be appreciated. This example performs the same check as +// TestCheckResourceAttr with that experimental interface, by +// using [statecheck.ExpectKnownValue]: +// +// package example_test +// +// import ( +// "testing" +// +// "github.com/hashicorp/terraform-plugin-testing/helper/resource" +// "github.com/hashicorp/terraform-plugin-testing/knownvalue" +// "github.com/hashicorp/terraform-plugin-testing/statecheck" +// "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +// ) +// +// func TestExpectKnownValue_CheckState_Bool(t *testing.T) { +// t.Parallel() +// +// resource.Test(t, resource.TestCase{ +// // Provider definition omitted. +// Steps: []resource.TestStep{ +// { +// // Example resource containing a computed boolean attribute named "computed_attribute" +// Config: `resource "test_resource" "one" {}`, +// ConfigStateChecks: []statecheck.StateCheck{ +// statecheck.ExpectKnownValue( +// "test_resource.one", +// tfjsonpath.New("computed_attribute"), +// knownvalue.Bool(true), +// ), +// }, +// }, +// }, +// }) +// } func TestCheckResourceAttr(name, key, value string) TestCheckFunc { return checkIfIndexesIntoTypeSet(key, func(s *terraform.State) error { is, err := primaryInstanceState(s, name) @@ -1138,6 +1357,43 @@ func testCheckResourceAttr(is *terraform.InstanceState, name string, key string, // when using TestCheckResourceAttrWith and a value is found for the given name and key. // // When this function returns an error, TestCheckResourceAttrWith will fail the check. +// +// An experimental interface exists to potentially replace the +// CheckResourceAttrWithFunc functionality in the future and feedback +// would be appreciated. This example performs the same check as +// TestCheckResourceAttrWith with that experimental interface, by +// using [statecheck.ExpectKnownValue] in combination with +// [knownvalue.StringRegexp]: +// +// package example_test +// +// import ( +// "testing" +// +// "github.com/hashicorp/terraform-plugin-testing/helper/resource" +// "github.com/hashicorp/terraform-plugin-testing/statecheck" +// "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +// ) +// +// func TestExpectKnownValue_CheckState_String_Custom(t *testing.T) { +// t.Parallel() +// +// resource.Test(t, resource.TestCase{ +// // Provider definition omitted. +// Steps: []resource.TestStep{ +// { +// // Example resource containing a computed string attribute named "computed_attribute" +// Config: `resource "test_resource" "one" {}`, +// ConfigStateChecks: []statecheck.StateCheck{ +// statecheck.ExpectKnownValue( +// "test_resource.one", +// tfjsonpath.New("computed_attribute"), +// knownvalue.StringRegexp(regexp.MustCompile("str")), +// }, +// }, +// }, +// }) +// } type CheckResourceAttrWithFunc func(value string) error // TestCheckResourceAttrWith ensures a value stored in state for the @@ -1175,6 +1431,43 @@ type CheckResourceAttrWithFunc func(value string) error // and it's provided with the attribute value to apply a custom checking logic, // if it was found in the state. The function must return an error for the // check to fail, or `nil` to succeed. +// +// An experimental interface exists to potentially replace the +// TestCheckResourceAttrWith functionality in the future and feedback +// would be appreciated. This example performs the same check as +// TestCheckResourceAttrWith with that experimental interface, by +// using [statecheck.ExpectKnownValue] in combination with +// [knownvalue.StringRegexp]: +// +// package example_test +// +// import ( +// "testing" +// +// "github.com/hashicorp/terraform-plugin-testing/helper/resource" +// "github.com/hashicorp/terraform-plugin-testing/statecheck" +// "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +// ) +// +// func TestExpectKnownValue_CheckState_String_Custom(t *testing.T) { +// t.Parallel() +// +// resource.Test(t, resource.TestCase{ +// // Provider definition omitted. +// Steps: []resource.TestStep{ +// { +// // Example resource containing a computed string attribute named "computed_attribute" +// Config: `resource "test_resource" "one" {}`, +// ConfigStateChecks: []statecheck.StateCheck{ +// statecheck.ExpectKnownValue( +// "test_resource.one", +// tfjsonpath.New("computed_attribute"), +// knownvalue.StringRegexp(regexp.MustCompile("str")), +// }, +// }, +// }, +// }) +// } func TestCheckResourceAttrWith(name, key string, checkValueFunc CheckResourceAttrWithFunc) TestCheckFunc { return checkIfIndexesIntoTypeSet(key, func(s *terraform.State) error { is, err := primaryInstanceState(s, name) @@ -1227,6 +1520,43 @@ func TestCheckResourceAttrWith(name, key string, checkValueFunc CheckResourceAtt // attributes using the special key syntax, checking a list, map, or set // attribute directly is not supported. Use TestCheckResourceAttr with // the special .# or .% key syntax for those situations instead. +// +// An experimental interface exists to potentially replace the +// TestCheckNoResourceAttr functionality in the future and feedback +// would be appreciated. This example performs the same check as +// TestCheckNoResourceAttr with that experimental interface, by +// using [statecheck.ExpectKnownValue] with [knownvalue.Null]: +// +// package example_test +// +// import ( +// "testing" +// +// "github.com/hashicorp/terraform-plugin-testing/helper/resource" +// "github.com/hashicorp/terraform-plugin-testing/statecheck" +// "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +// ) +// +// func TestExpectKnownValue_CheckState_AttributeNull(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" that has a null value +// Config: `resource "test_resource" "one" {}`, +// ConfigStateChecks: []statecheck.StateCheck{ +// statecheck.ExpectKnownValue( +// "test_resource.one", +// tfjsonpath.New("computed_attribute"), +// knownvalue.Null(), +// ), +// }, +// }, +// }, +// }) +// } func TestCheckNoResourceAttr(name, key string) TestCheckFunc { return checkIfIndexesIntoTypeSet(key, func(s *terraform.State) error { is, err := primaryInstanceState(s, name) @@ -1331,6 +1661,43 @@ func testCheckNoResourceAttr(is *terraform.InstanceState, name string, key strin // using the regexp.MustCompile() function, which will automatically ensure the // regular expression is supported by the Go regular expression handlers during // compilation. +// +// An experimental interface exists to potentially replace the +// TestMatchResourceAttr functionality in the future and feedback +// would be appreciated. This example performs the same check as +// TestMatchResourceAttr with that experimental interface, by +// using [statecheck.ExpectKnownValue] in combination with +// [knownvalue.StringRegexp]: +// +// package example_test +// +// import ( +// "testing" +// +// "github.com/hashicorp/terraform-plugin-testing/helper/resource" +// "github.com/hashicorp/terraform-plugin-testing/statecheck" +// "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +// ) +// +// func TestExpectKnownValue_CheckState_String_Custom(t *testing.T) { +// t.Parallel() +// +// resource.Test(t, resource.TestCase{ +// // Provider definition omitted. +// Steps: []resource.TestStep{ +// { +// // Example resource containing a computed string attribute named "computed_attribute" +// Config: `resource "test_resource" "one" {}`, +// ConfigStateChecks: []statecheck.StateCheck{ +// statecheck.ExpectKnownValue( +// "test_resource.one", +// tfjsonpath.New("computed_attribute"), +// knownvalue.StringRegexp(regexp.MustCompile("str")), +// }, +// }, +// }, +// }) +// } func TestMatchResourceAttr(name, key string, r *regexp.Regexp) TestCheckFunc { return checkIfIndexesIntoTypeSet(key, func(s *terraform.State) error { is, err := primaryInstanceState(s, name) @@ -1532,6 +1899,100 @@ func testCheckResourceAttrPair(isFirst *terraform.InstanceState, nameFirst strin } // TestCheckOutput checks an output in the Terraform configuration +// +// An experimental interface exists to potentially replace the +// TestCheckOutput functionality in the future and feedback +// would be appreciated. This example performs the same check as +// TestCheckOutput with that experimental interface, by +// using [statecheck.ExpectKnownOutputValue]: +// +// package example_test +// +// import ( +// "testing" +// +// "github.com/hashicorp/terraform-plugin-testing/helper/resource" +// "github.com/hashicorp/terraform-plugin-testing/knownvalue" +// "github.com/hashicorp/terraform-plugin-testing/statecheck" +// "github.com/hashicorp/terraform-plugin-testing/tfversion" +// ) +// +// func TestExpectKnownOutputValue_CheckState_Bool(t *testing.T) { +// t.Parallel() +// +// resource.Test(t, resource.TestCase{ +// TerraformVersionChecks: []tfversion.TerraformVersionCheck{ +// tfversion.SkipBelow(tfversion.Version1_8_0), +// }, +// // Provider definition omitted. +// Steps: []resource.TestStep{ +// { +// // Example provider containing a provider-defined function named "bool" +// Config: `output "test" { +// value = provider::example::bool(true) +// }`, +// ConfigStateChecks: []statecheck.StateCheck{ +// statecheck.ExpectKnownOutputValue("test", knownvalue.Bool(true)), +// }, +// }, +// }, +// }) +// } +// +// An experimental interface exists to potentially replace the +// TestCheckOutput functionality in the future and feedback +// would be appreciated. This example performs the same check as +// TestCheckOutput with that experimental interface, by using +// [statecheck.ExpectKnownOutputValueAtPath]: +// +// package example_test +// +// import ( +// "testing" +// +// "github.com/hashicorp/terraform-plugin-testing/helper/resource" +// "github.com/hashicorp/terraform-plugin-testing/knownvalue" +// "github.com/hashicorp/terraform-plugin-testing/statecheck" +// "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +// ) +// +// func TestExpectKnownOutputValueAtPath_CheckState_Bool(t *testing.T) { +// t.Parallel() +// +// resource.Test(t, resource.TestCase{ +// // Provider definition omitted. +// Steps: []resource.TestStep{ +// { +// // Example resource containing a computed boolean attribute named "computed_attribute" +// Config: `resource "test_resource" "one" {} +// +// // Generally, it is not necessary to use an output to test a resource attribute, +// // the resource attribute should be tested directly instead, by inspecting the +// // value of the resource attribute. For instance: +// // +// // ConfigStateChecks: []statecheck.StateCheck{ +// // statecheck.ExpectKnownValue( +// // "test_resource.one", +// // tfjsonpath.New("computed_attribute"), +// // knownvalue.Bool(true), +// // ), +// // }, +// // +// // This is only shown as an example. +// output test_resource_one_output { +// value = test_resource.one +// }`, +// ConfigStateChecks: []statecheck.StateCheck{ +// statecheck.ExpectKnownOutputValueAtPath( +// "test_resource_one_output", +// tfjsonpath.New("computed_attribute"), +// knownvalue.Bool(true), +// ), +// }, +// }, +// }, +// }) +// } func TestCheckOutput(name, value string) TestCheckFunc { return func(s *terraform.State) error { ms := s.RootModule() @@ -1552,6 +2013,64 @@ func TestCheckOutput(name, value string) TestCheckFunc { } } +// TestMatchOutput ensures a value matching a regular expression is +// stored in state for the given name. State value checking is only +// recommended for testing Computed attributes and attribute defaults. +// +// An experimental interface exists to potentially replace the +// TestMatchOutput functionality in the future and feedback +// would be appreciated. This example performs the same check as +// TestMatchOutput with that experimental interface, by using +// [statecheck.ExpectKnownOutputValueAtPath] in combination with +// [knownvalue.StringRegexp]: +// +// package example_test +// +// import ( +// "testing" +// +// "github.com/hashicorp/terraform-plugin-testing/helper/resource" +// "github.com/hashicorp/terraform-plugin-testing/statecheck" +// "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +// ) +// +// func TestExpectKnownOutputValueAtPath_CheckState_String_Custom(t *testing.T) { +// t.Parallel() +// +// resource.Test(t, resource.TestCase{ +// // Provider definition omitted. +// Steps: []resource.TestStep{ +// { +// // Example resource containing a computed string attribute named "computed_attribute" +// Config: `resource "test_resource" "one" {} +// +// // Generally, it is not necessary to use an output to test a resource attribute, +// // the resource attribute should be tested directly instead, by inspecting the +// // value of the resource attribute. For instance: +// // +// // ConfigStateChecks: []statecheck.StateCheck{ +// // statecheck.ExpectKnownValue( +// // "test_resource.one", +// // tfjsonpath.New("computed_attribute"), +// // knownvalue.StringRegexp(regexp.MustCompile("str")), +// // ), +// // }, +// // +// // This is only shown as an example. +// output test_resource_one_output { +// value = test_resource.one +// }`, +// ConfigStateChecks: []statecheck.StateCheck{ +// statecheck.ExpectKnownOutputValueAtPath( +// "test_resource_one_output", +// tfjsonpath.New("computed_attribute"), +// knownvalue.StringRegexp(regexp.MustCompile("str"), +// ), +// }, +// }, +// }, +// }) +// } func TestMatchOutput(name string, r *regexp.Regexp) TestCheckFunc { return func(s *terraform.State) error { ms := s.RootModule() @@ -1596,7 +2115,7 @@ func modulePrimaryInstanceState(ms *terraform.ModuleState, name string) (*terraf // modulePathPrimaryInstanceState returns the primary instance state for the // given resource name in a given module path. func modulePathPrimaryInstanceState(s *terraform.State, mp addrs.ModuleInstance, name string) (*terraform.InstanceState, error) { - ms := s.ModuleByPath(mp) + ms := s.ModuleByPath(mp) //nolint:staticcheck // legacy usage if ms == nil { return nil, fmt.Errorf("No module found at: %s", mp) } @@ -1607,7 +2126,7 @@ func modulePathPrimaryInstanceState(s *terraform.State, mp addrs.ModuleInstance, // primaryInstanceState returns the primary instance state for the given // resource name in the root module. func primaryInstanceState(s *terraform.State, name string) (*terraform.InstanceState, error) { - ms := s.RootModule() + ms := s.RootModule() //nolint:staticcheck // legacy usage return modulePrimaryInstanceState(ms, name) } @@ -1626,7 +2145,7 @@ func indexesIntoTypeSet(key string) bool { func checkIfIndexesIntoTypeSet(key string, f TestCheckFunc) TestCheckFunc { return func(s *terraform.State) error { err := f(s) - if err != nil && s.IsBinaryDrivenTest && indexesIntoTypeSet(key) { + if err != nil && indexesIntoTypeSet(key) { return fmt.Errorf("Error in test check: %s\nTest check address %q likely indexes into TypeSet\nThis is currently not possible in the SDK", err, key) } return err @@ -1636,7 +2155,7 @@ func checkIfIndexesIntoTypeSet(key string, f TestCheckFunc) TestCheckFunc { func checkIfIndexesIntoTypeSetPair(keyFirst, keySecond string, f TestCheckFunc) TestCheckFunc { return func(s *terraform.State) error { err := f(s) - if err != nil && s.IsBinaryDrivenTest && (indexesIntoTypeSet(keyFirst) || indexesIntoTypeSet(keySecond)) { + if err != nil && (indexesIntoTypeSet(keyFirst) || indexesIntoTypeSet(keySecond)) { return fmt.Errorf("Error in test check: %s\nTest check address %q or %q likely indexes into TypeSet\nThis is currently not possible in the SDK", err, keyFirst, keySecond) } return err diff --git a/helper/resource/testing_new.go b/helper/resource/testing_new.go index da4785f0a..c94a97c05 100644 --- a/helper/resource/testing_new.go +++ b/helper/resource/testing_new.go @@ -13,20 +13,24 @@ import ( "strings" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/go-version" tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-plugin-testing/helper/resource/query" "github.com/mitchellh/go-testing-interface" + "github.com/hashicorp/terraform-plugin-testing/config" "github.com/hashicorp/terraform-plugin-testing/internal/logging" "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" "github.com/hashicorp/terraform-plugin-testing/terraform" ) func runPostTestDestroy(ctx context.Context, t testing.T, c TestCase, wd *plugintest.WorkingDir, providers *providerFactories, statePreDestroy *terraform.State) error { t.Helper() - err := runProviderCommand(ctx, t, func() error { + err := runProviderCommand(ctx, t, wd, providers, func() error { return wd.Destroy(ctx) - }, wd, providers) + }) if err != nil { return err } @@ -60,15 +64,17 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest } defer func() { + t.Helper() + var statePreDestroy *terraform.State var err error - err = runProviderCommand(ctx, t, func() error { - statePreDestroy, err = getState(ctx, t, wd) + err = runProviderCommand(ctx, t, wd, providers, func() error { + _, statePreDestroy, err = getState(ctx, t, wd) if err != nil { return err } return nil - }, wd, providers) + }) if err != nil { logging.HelperResourceError(ctx, "Error retrieving state, there may be dangling resources", @@ -92,8 +98,16 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest wd.Close() }() + // Return value from c.ProviderConfig() is assigned to Raw as this was previously being + // passed to wd.SetConfig() when the second argument accept a configuration string. if c.hasProviders(ctx) { - err := wd.SetConfig(ctx, c.providerConfig(ctx, false)) + config := teststep.Configuration( + teststep.ConfigurationRequest{ + Raw: teststep.Pointer(c.providerConfig(ctx, false)), + }, + ) + + err := wd.SetConfig(ctx, config, nil) if err != nil { logging.HelperResourceError(ctx, @@ -103,9 +117,9 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest t.Fatalf("TestCase error setting provider configuration: %s", err) } - err = runProviderCommand(ctx, t, func() error { + err = runProviderCommand(ctx, t, wd, providers, func() error { return wd.Init(ctx) - }, wd, providers) + }) if err != nil { logging.HelperResourceError(ctx, @@ -116,11 +130,9 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest } } - logging.HelperResourceDebug(ctx, "Starting TestSteps") - // use this to track last step successfully applied // acts as default for import tests - var appliedCfg string + var appliedCfg teststep.Config var stepNumber int for stepIndex, step := range c.Steps { @@ -129,6 +141,19 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest } stepNumber = stepIndex + 1 // 1-based indexing for humans + + configRequest := teststep.PrepareConfigurationRequest{ + Directory: step.ConfigDirectory, + File: step.ConfigFile, + Raw: step.Config, + TestStepConfigRequest: config.TestStepConfigRequest{ + StepNumber: stepNumber, + TestName: t.Name(), + }, + }.Exec() + + cfg := teststep.Configuration(configRequest) + ctx = logging.TestStepNumberContext(ctx, stepNumber) logging.HelperResourceDebug(ctx, "Starting TestStep") @@ -160,7 +185,7 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest } } - if step.Config != "" && !step.Destroy && len(step.Taint) > 0 { + if cfg != nil && !step.Destroy && len(step.Taint) > 0 { err := testStepTaint(ctx, step, wd) if err != nil { @@ -172,16 +197,67 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest } } - if step.hasProviders(ctx) { + hasProviders, err := step.hasProviders(ctx, stepIndex, t.Name()) + + if err != nil { + logging.HelperResourceError(ctx, + "TestStep error checking for providers", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("TestStep %d/%d error checking for providers: %s", stepNumber, len(c.Steps), err) + } + + if hasProviders { providers = &providerFactories{ legacy: sdkProviderFactories(c.ProviderFactories).merge(step.ProviderFactories), protov5: protov5ProviderFactories(c.ProtoV5ProviderFactories).merge(step.ProtoV5ProviderFactories), protov6: protov6ProviderFactories(c.ProtoV6ProviderFactories).merge(step.ProtoV6ProviderFactories), } - providerCfg := step.providerConfig(ctx, step.configHasProviderBlock(ctx)) + var hasProviderBlock bool + + if cfg != nil { + hasProviderBlock, err = cfg.HasProviderBlock(ctx) + + if err != nil { + logging.HelperResourceError(ctx, + "TestStep error determining whether configuration contains provider block", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("TestStep %d/%d error determining whether configuration contains provider block: %s", stepNumber, len(c.Steps), err) + } + } + + var testStepConfig teststep.Config + + rawCfg, err := step.providerConfig(ctx, hasProviderBlock, helper.TerraformVersion()) + + if err != nil { + logging.HelperResourceError(ctx, + "TestStep error generating provider configuration", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("TestStep %d/%d error generating provider configuration: %s", stepNumber, len(c.Steps), err) + } + + // Return value from step.providerConfig() is assigned to Raw as this was previously being + // passed to wd.SetConfig() directly when the second argument to wd.SetConfig() accepted a + // configuration string. + confRequest := teststep.PrepareConfigurationRequest{ + Directory: step.ConfigDirectory, + File: step.ConfigFile, + Raw: rawCfg, + TestStepConfigRequest: config.TestStepConfigRequest{ + StepNumber: stepNumber, + TestName: t.Name(), + }, + }.Exec() + + testStepConfig = teststep.Configuration(confRequest) - err := wd.SetConfig(ctx, providerCfg) + if !step.Query { + err = wd.SetConfig(ctx, testStepConfig, step.ConfigVariables) + } if err != nil { logging.HelperResourceError(ctx, @@ -191,15 +267,9 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest t.Fatalf("TestStep %d/%d error setting test provider configuration: %s", stepNumber, len(c.Steps), err) } - err = runProviderCommand( - ctx, - t, - func() error { - return wd.Init(ctx) - }, - wd, - providers, - ) + err = runProviderCommand(ctx, t, wd, providers, func() error { + return wd.Init(ctx) + }) if err != nil { logging.HelperResourceError(ctx, @@ -214,7 +284,7 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest if step.ImportState { logging.HelperResourceTrace(ctx, "TestStep is ImportState mode") - err := testStepNewImportState(ctx, t, helper, wd, step, appliedCfg, providers) + err := testStepNewImportState(ctx, t, helper, wd, step, appliedCfg, providers, stepNumber) if step.ExpectError != nil { logging.HelperResourceDebug(ctx, "Checking TestStep ExpectError") if err == nil { @@ -289,10 +359,48 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest continue } - if step.Config != "" { + if step.Query { + logging.HelperResourceTrace(ctx, "TestStep is Query mode") + + queryConfigRequest := teststep.ConfigurationRequest{ + Raw: &step.Config, + } + err := wd.SetQuery(ctx, teststep.Configuration(queryConfigRequest), step.ConfigVariables) + if err != nil { + t.Fatalf("Step %d/%d error setting query: %s", stepNumber, len(c.Steps), err) + } + + err = runProviderCommand(ctx, t, wd, providers, func() error { + return wd.Init(ctx) + }) + if err != nil { + t.Fatalf("Step %d/%d error running init: %s", stepNumber, len(c.Steps), err) + } + + var queryOut []tfjson.LogMsg + err = runProviderCommand(ctx, t, wd, providers, func() error { + var err error + queryOut, err = wd.Query(ctx) + return err + }) + if err != nil { + fmt.Printf("Step %d/%d Query Output:\n%s\n", stepNumber, len(c.Steps), queryOut) + t.Fatalf("Step %d/%d error running query: %s", stepNumber, len(c.Steps), err) + } + + err = query.RunQueryChecks(ctx, t, queryOut, step.QueryResultChecks) + if err != nil { + t.Fatalf("Step %d/%d error running query checks: %s", stepNumber, len(c.Steps), err) + } + + fmt.Printf("Step %d/%d Query Output:\n%s\n", stepNumber, len(c.Steps), queryOut) + continue + } + + if cfg != nil { logging.HelperResourceTrace(ctx, "TestStep is Config mode") - err := testStepNewConfig(ctx, t, c, wd, step, providers) + err := testStepNewConfig(ctx, t, c, wd, step, providers, stepIndex, helper) if step.ExpectError != nil { logging.HelperResourceDebug(ctx, "Checking TestStep ExpectError") @@ -326,7 +434,53 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest } } - appliedCfg = step.mergedConfig(ctx, c) + var hasTerraformBlock bool + var hasProviderBlock bool + + if cfg != nil { + hasTerraformBlock, err = cfg.HasTerraformBlock(ctx) + + if err != nil { + logging.HelperResourceError(ctx, + "Error determining whether configuration contains terraform block", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("Error determining whether configuration contains terraform block: %s", err) + } + + hasProviderBlock, err = cfg.HasProviderBlock(ctx) + + if err != nil { + logging.HelperResourceError(ctx, + "Error determining whether configuration contains provider block", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("Error determining whether configuration contains provider block: %s", err) + } + } + + mergedConfig, err := step.mergedConfig(ctx, c, hasTerraformBlock, hasProviderBlock, helper.TerraformVersion()) + + if err != nil { + logging.HelperResourceError(ctx, + "Error generating merged configuration", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("Error generating merged configuration: %s", err) + } + + // Preserve the step config for future test steps to use (import state) + confRequest := teststep.PrepareConfigurationRequest{ + Directory: step.ConfigDirectory, + File: step.ConfigFile, + Raw: mergedConfig, + TestStepConfigRequest: config.TestStepConfigRequest{ + StepNumber: stepNumber, + TestName: t.Name(), + }, + }.Exec() + + appliedCfg = teststep.Configuration(confRequest) logging.HelperResourceDebug(ctx, "Finished TestStep") @@ -341,25 +495,25 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest } } -func getState(ctx context.Context, t testing.T, wd *plugintest.WorkingDir) (*terraform.State, error) { +func getState(ctx context.Context, t testing.T, wd *plugintest.WorkingDir) (*tfjson.State, *terraform.State, error) { t.Helper() jsonState, err := wd.State(ctx) if err != nil { - return nil, err + return nil, nil, err } state, err := shimStateFromJson(jsonState) if err != nil { t.Fatal(err) } - return state, nil + return jsonState, state, nil } func stateIsEmpty(state *terraform.State) bool { - return state.Empty() || !state.HasResources() + return state.Empty() || !state.HasResources() //nolint:staticcheck // legacy usage } -func planIsEmpty(plan *tfjson.Plan) bool { +func planIsEmpty(plan *tfjson.Plan, tfVersion *version.Version) bool { for _, rc := range plan.ResourceChanges { for _, a := range rc.Change.Actions { if a != tfjson.ActionNoop { @@ -367,43 +521,112 @@ func planIsEmpty(plan *tfjson.Plan) bool { } } } + + if tfVersion.LessThan(expectNonEmptyPlanOutputChangesMinTFVersion) { + return true + } + + for _, change := range plan.OutputChanges { + if !change.Actions.NoOp() { + return false + } + } + return true } -func testIDRefresh(ctx context.Context, t testing.T, c TestCase, wd *plugintest.WorkingDir, step TestStep, r *terraform.ResourceState, providers *providerFactories) error { +func testIDRefresh(ctx context.Context, t testing.T, c TestCase, wd *plugintest.WorkingDir, step TestStep, r *terraform.ResourceState, providers *providerFactories, stepIndex int, helper *plugintest.Helper) error { t.Helper() // Build the state. The state is just the resource with an ID. There // are no attributes. We only set what is needed to perform a refresh. - state := terraform.NewState() + state := terraform.NewState() //nolint:staticcheck // legacy usage state.RootModule().Resources = make(map[string]*terraform.ResourceState) state.RootModule().Resources[c.IDRefreshName] = &terraform.ResourceState{} + configRequest := teststep.PrepareConfigurationRequest{ + Directory: step.ConfigDirectory, + File: step.ConfigFile, + Raw: step.Config, + TestStepConfigRequest: config.TestStepConfigRequest{ + StepNumber: stepIndex + 1, + TestName: t.Name(), + }, + }.Exec() + + cfg := teststep.Configuration(configRequest) + + var hasProviderBlock bool + + if cfg != nil { + var err error + + hasProviderBlock, err = cfg.HasProviderBlock(ctx) + + if err != nil { + logging.HelperResourceError(ctx, + "Error determining whether configuration contains provider block for import test config", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("Error determining whether configuration contains provider block for import test config: %s", err) + } + } + + // Return value from c.ProviderConfig() is assigned to Raw as this was previously being + // passed to wd.SetConfig() when the second argument accept a configuration string. + testStepConfig := teststep.Configuration( + teststep.ConfigurationRequest{ + Raw: teststep.Pointer(c.providerConfig(ctx, hasProviderBlock)), + }, + ) + // Temporarily set the config to a minimal provider config for the refresh // test. After the refresh we can reset it. - err := wd.SetConfig(ctx, c.providerConfig(ctx, step.configHasProviderBlock(ctx))) + err := wd.SetConfig(ctx, testStepConfig, step.ConfigVariables) if err != nil { t.Fatalf("Error setting import test config: %s", err) } + + rawCfg, err := step.providerConfig(ctx, hasProviderBlock, helper.TerraformVersion()) + + if err != nil { + t.Fatalf("Error generating import provider config: %s", err) + } + defer func() { - err = wd.SetConfig(ctx, step.Config) + t.Helper() + + confRequest := teststep.PrepareConfigurationRequest{ + Directory: step.ConfigDirectory, + File: step.ConfigFile, + Raw: rawCfg, + TestStepConfigRequest: config.TestStepConfigRequest{ + StepNumber: stepIndex + 1, + TestName: t.Name(), + }, + }.Exec() + + testStepConfigDefer := teststep.Configuration(confRequest) + + err = wd.SetConfig(ctx, testStepConfigDefer, step.ConfigVariables) + if err != nil { t.Fatalf("Error resetting test config: %s", err) } }() // Refresh! - err = runProviderCommand(ctx, t, func() error { + err = runProviderCommand(ctx, t, wd, providers, func() error { err = wd.Refresh(ctx) if err != nil { t.Fatalf("Error running terraform refresh: %s", err) } - state, err = getState(ctx, t, wd) + _, state, err = getState(ctx, t, wd) if err != nil { return err } return nil - }, wd, providers) + }) if err != nil { return err } @@ -464,7 +687,7 @@ func copyWorkingDir(ctx context.Context, t testing.T, stepNumber int, wd *plugin dest := filepath.Join(workingDir, fmt.Sprintf("%s%s", "step_", strconv.Itoa(stepNumber))) baseDir := wd.BaseDir() - rootBaseDir := strings.TrimLeft(baseDir, workingDir) + rootBaseDir := strings.TrimPrefix(baseDir, workingDir) err := plugintest.CopyDir(workingDir, dest, rootBaseDir) if err != nil { diff --git a/helper/resource/testing_new_config.go b/helper/resource/testing_new_config.go index add09bcc7..eecc87ac0 100644 --- a/helper/resource/testing_new_config.go +++ b/helper/resource/testing_new_config.go @@ -7,31 +7,100 @@ import ( "context" "errors" "fmt" + "os" + "github.com/hashicorp/terraform-exec/tfexec" tfjson "github.com/hashicorp/terraform-json" "github.com/mitchellh/go-testing-interface" + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-plugin-testing/tfversion" "github.com/hashicorp/terraform-plugin-testing/internal/logging" "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" ) -func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugintest.WorkingDir, step TestStep, providers *providerFactories) error { +// expectNonEmptyPlanOutputChangesMinTFVersion is used to keep compatibility for +// Terraform 0.12 and 0.13 after enabling ExpectNonEmptyPlan to check output +// changes. Those older versions will always show outputs being created. +var expectNonEmptyPlanOutputChangesMinTFVersion = tfversion.Version0_14_0 + +func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugintest.WorkingDir, step TestStep, providers *providerFactories, stepIndex int, helper *plugintest.Helper) error { t.Helper() - err := wd.SetConfig(ctx, step.mergedConfig(ctx, c)) + // When `refreshAfterApply` is true, a `Config`-mode test step will invoke + // a refresh before successful completion. This is a compatibility measure + // for test cases that have different -- but semantically-equal -- state + // representations in their test steps. When comparing two states, the + // testing framework is not aware of semantic equality or set equality. + _, refreshAfterApply := os.LookupEnv(EnvTfAccRefreshAfterApply) + + configRequest := teststep.PrepareConfigurationRequest{ + Directory: step.ConfigDirectory, + File: step.ConfigFile, + Raw: step.Config, + TestStepConfigRequest: config.TestStepConfigRequest{ + StepNumber: stepIndex + 1, + TestName: t.Name(), + }, + }.Exec() + + cfg := teststep.Configuration(configRequest) + + var hasTerraformBlock bool + var hasProviderBlock bool + + if cfg != nil { + var err error + + hasTerraformBlock, err = cfg.HasTerraformBlock(ctx) + + if err != nil { + logging.HelperResourceError(ctx, + "Error determining whether configuration contains terraform block", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("Error determining whether configuration contains terraform block: %s", err) + } + + hasProviderBlock, err = cfg.HasProviderBlock(ctx) + + if err != nil { + logging.HelperResourceError(ctx, + "Error determining whether configuration contains provider block", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("Error determining whether configuration contains provider block: %s", err) + } + } + + mergedConfig, err := step.mergedConfig(ctx, c, hasTerraformBlock, hasProviderBlock, helper.TerraformVersion()) + if err != nil { - return fmt.Errorf("Error setting config: %w", err) + logging.HelperResourceError(ctx, + "Error generating merged configuration", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("Error generating merged configuration: %s", err) } - // require a refresh before applying - // failing to do this will result in data sources not being updated - err = runProviderCommand(ctx, t, func() error { - return wd.Refresh(ctx) - }, wd, providers) + confRequest := teststep.PrepareConfigurationRequest{ + Directory: step.ConfigDirectory, + File: step.ConfigFile, + Raw: mergedConfig, + TestStepConfigRequest: config.TestStepConfigRequest{ + StepNumber: stepIndex + 1, + TestName: t.Name(), + }, + }.Exec() + + testStepConfig := teststep.Configuration(confRequest) + + err = wd.SetConfig(ctx, testStepConfig, step.ConfigVariables) if err != nil { - return fmt.Errorf("Error running pre-apply refresh: %w", err) + return fmt.Errorf("Error setting config: %w", err) } // If this step is a PlanOnly step, skip over this first Plan and @@ -41,12 +110,23 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint logging.HelperResourceDebug(ctx, "Running Terraform CLI plan and apply") // Plan! - err := runProviderCommand(ctx, t, func() error { + err := runProviderCommand(ctx, t, wd, providers, func() error { + var opts []tfexec.PlanOption if step.Destroy { - return wd.CreateDestroyPlan(ctx) + opts = append(opts, tfexec.Destroy(true)) } - return wd.CreatePlan(ctx) - }, wd, providers) + + if c.AdditionalCLIOptions != nil { + if c.AdditionalCLIOptions.Plan.AllowDeferral { + opts = append(opts, tfexec.AllowDeferral(true)) + } + if c.AdditionalCLIOptions.Plan.NoRefresh { + opts = append(opts, tfexec.Refresh(false)) + } + } + + return wd.CreatePlan(ctx, opts...) + }) if err != nil { return fmt.Errorf("Error running pre-apply plan: %w", err) } @@ -54,11 +134,11 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint // Run pre-apply plan checks if len(step.ConfigPlanChecks.PreApply) > 0 { var plan *tfjson.Plan - err = runProviderCommand(ctx, t, func() error { + err = runProviderCommand(ctx, t, wd, providers, func() error { var err error plan, err = wd.SavedPlan(ctx) return err - }, wd, providers) + }) if err != nil { return fmt.Errorf("Error retrieving pre-apply plan: %w", err) } @@ -73,21 +153,47 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint // that the destroy steps can verify their behavior in the // check function var stateBeforeApplication *terraform.State - err = runProviderCommand(ctx, t, func() error { - stateBeforeApplication, err = getState(ctx, t, wd) + + if step.Check != nil && step.Destroy { + // Refresh the state before shimming it for destroy checks later. + // This re-implements previously existing test step logic for the + // specific situation that a provider developer has applied a + // resource with a previous schema version and is destroying it with + // a provider that has a newer schema version. Without this refresh + // the shim logic will return an error such as: + // + // Failed to marshal state to json: schema version 0 for null_resource.test in state does not match version 1 from the provider + err := runProviderCommand(ctx, t, wd, providers, func() error { + return wd.Refresh(ctx) + }) + if err != nil { - return err + return fmt.Errorf("Error running pre-apply refresh: %w", err) + } + + err = runProviderCommand(ctx, t, wd, providers, func() error { + _, stateBeforeApplication, err = getState(ctx, t, wd) + if err != nil { + return err + } + return nil + }) + + if err != nil { + return fmt.Errorf("Error retrieving pre-apply state: %w", err) } - return nil - }, wd, providers) - if err != nil { - return fmt.Errorf("Error retrieving pre-apply state: %w", err) } // Apply the diff, creating real resources - err = runProviderCommand(ctx, t, func() error { - return wd.Apply(ctx) - }, wd, providers) + err = runProviderCommand(ctx, t, wd, providers, func() error { + var opts []tfexec.ApplyOption + + if c.AdditionalCLIOptions != nil && c.AdditionalCLIOptions.Apply.AllowDeferral { + opts = append(opts, tfexec.AllowDeferral(true)) + } + + return wd.Apply(ctx, opts...) + }) if err != nil { if step.Destroy { return fmt.Errorf("Error running destroy: %w", err) @@ -95,133 +201,201 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint return fmt.Errorf("Error running apply: %w", err) } - // Get the new state - var state *terraform.State - err = runProviderCommand(ctx, t, func() error { - state, err = getState(ctx, t, wd) - if err != nil { - return err - } - return nil - }, wd, providers) - if err != nil { - return fmt.Errorf("Error retrieving state after apply: %w", err) - } - // Run any configured checks if step.Check != nil { logging.HelperResourceTrace(ctx, "Using TestStep Check") - state.IsBinaryDrivenTest = true if step.Destroy { if err := step.Check(stateBeforeApplication); err != nil { return fmt.Errorf("Check failed: %w", err) } } else { + var state *terraform.State + + err := runProviderCommand(ctx, t, wd, providers, func() error { + _, state, err = getState(ctx, t, wd) + if err != nil { + return err + } + return nil + }) + + if err != nil { + return fmt.Errorf("Error retrieving state after apply: %w", err) + } + if err := step.Check(state); err != nil { return fmt.Errorf("Check failed: %w", err) } } } + + // Run state checks + if len(step.ConfigStateChecks) > 0 { + var state *tfjson.State + + err = runProviderCommand(ctx, t, wd, providers, func() error { + var err error + state, err = wd.State(ctx) + return err + }) + + if err != nil { + return fmt.Errorf("Error retrieving post-apply, post-refresh state: %w", err) + } + + err = runStateChecks(ctx, t, state, step.ConfigStateChecks) + if err != nil { + return fmt.Errorf("Post-apply refresh state check(s) failed:\n%w", err) + } + } } // Test for perpetual diffs by performing a plan, a refresh, and another plan logging.HelperResourceDebug(ctx, "Running Terraform CLI plan to check for perpetual differences") // do a plan - err = runProviderCommand(ctx, t, func() error { + err = runProviderCommand(ctx, t, wd, providers, func() error { + opts := []tfexec.PlanOption{ + tfexec.Refresh(false), + } if step.Destroy { - return wd.CreateDestroyPlan(ctx) + opts = append(opts, tfexec.Destroy(true)) } - return wd.CreatePlan(ctx) - }, wd, providers) + + if c.AdditionalCLIOptions != nil { + if c.AdditionalCLIOptions.Plan.AllowDeferral { + opts = append(opts, tfexec.AllowDeferral(true)) + } + if c.AdditionalCLIOptions.Plan.NoRefresh { + opts = append(opts, tfexec.Refresh(false)) + } + } + + return wd.CreatePlan(ctx, opts...) + }) if err != nil { - return fmt.Errorf("Error running post-apply plan: %w", err) + if step.PlanOnly { + return fmt.Errorf("Error running non-refresh plan: %w", err) + } + + return fmt.Errorf("Error running post-apply non-refresh plan: %w", err) } var plan *tfjson.Plan - err = runProviderCommand(ctx, t, func() error { + err = runProviderCommand(ctx, t, wd, providers, func() error { var err error plan, err = wd.SavedPlan(ctx) return err - }, wd, providers) + }) if err != nil { - return fmt.Errorf("Error retrieving post-apply plan: %w", err) + if step.PlanOnly { + return fmt.Errorf("Error reading saved non-refresh plan: %w", err) + } + + return fmt.Errorf("Error reading saved post-apply non-refresh plan: %w", err) } // Run post-apply, pre-refresh plan checks if len(step.ConfigPlanChecks.PostApplyPreRefresh) > 0 { err = runPlanChecks(ctx, t, plan, step.ConfigPlanChecks.PostApplyPreRefresh) if err != nil { + if step.PlanOnly { + return fmt.Errorf("Non-refresh plan checks(s) failed:\n%w", err) + } + return fmt.Errorf("Post-apply, pre-refresh plan check(s) failed:\n%w", err) } } - if !planIsEmpty(plan) && !step.ExpectNonEmptyPlan { + if !planIsEmpty(plan, helper.TerraformVersion()) && !step.ExpectNonEmptyPlan { var stdout string - err = runProviderCommand(ctx, t, func() error { + err = runProviderCommand(ctx, t, wd, providers, func() error { var err error stdout, err = wd.SavedPlanRawStdout(ctx) return err - }, wd, providers) + }) if err != nil { - return fmt.Errorf("Error retrieving formatted plan output: %w", err) + return fmt.Errorf("Error reading saved human-readable non-refresh plan output: %w", err) } - return fmt.Errorf("After applying this test step, the plan was not empty.\nstdout:\n\n%s", stdout) - } - // do a refresh - if !step.Destroy || (step.Destroy && !step.PreventPostDestroyRefresh) { - err := runProviderCommand(ctx, t, func() error { - return wd.Refresh(ctx) - }, wd, providers) - if err != nil { - return fmt.Errorf("Error running post-apply refresh: %w", err) + if step.PlanOnly { + return fmt.Errorf("The non-refresh plan was not empty.\nstdout:\n\n%s", stdout) } + + return fmt.Errorf("After applying this test step, the non-refresh plan was not empty.\nstdout:\n\n%s", stdout) } // do another plan - err = runProviderCommand(ctx, t, func() error { + err = runProviderCommand(ctx, t, wd, providers, func() error { + var opts []tfexec.PlanOption if step.Destroy { - return wd.CreateDestroyPlan(ctx) + opts = append(opts, tfexec.Destroy(true)) + + if step.PreventPostDestroyRefresh { + opts = append(opts, tfexec.Refresh(false)) + } } - return wd.CreatePlan(ctx) - }, wd, providers) + + if c.AdditionalCLIOptions != nil { + if c.AdditionalCLIOptions.Plan.AllowDeferral { + opts = append(opts, tfexec.AllowDeferral(true)) + } + if c.AdditionalCLIOptions.Plan.NoRefresh { + opts = append(opts, tfexec.Refresh(false)) + } + } + + return wd.CreatePlan(ctx, opts...) + }) if err != nil { - return fmt.Errorf("Error running second post-apply plan: %w", err) + if step.PlanOnly { + return fmt.Errorf("Error running refresh plan: %w", err) + } + + return fmt.Errorf("Error running post-apply refresh plan: %w", err) } - err = runProviderCommand(ctx, t, func() error { + err = runProviderCommand(ctx, t, wd, providers, func() error { var err error plan, err = wd.SavedPlan(ctx) return err - }, wd, providers) + }) if err != nil { - return fmt.Errorf("Error retrieving second post-apply plan: %w", err) + if step.PlanOnly { + return fmt.Errorf("Error reading refresh plan: %w", err) + } + + return fmt.Errorf("Error reading post-apply refresh plan: %w", err) } // Run post-apply, post-refresh plan checks if len(step.ConfigPlanChecks.PostApplyPostRefresh) > 0 { err = runPlanChecks(ctx, t, plan, step.ConfigPlanChecks.PostApplyPostRefresh) if err != nil { - return fmt.Errorf("Post-apply, post-refresh plan check(s) failed:\n%w", err) + return fmt.Errorf("Post-apply refresh plan check(s) failed:\n%w", err) } } // check if plan is empty - if !planIsEmpty(plan) && !step.ExpectNonEmptyPlan { + if !planIsEmpty(plan, helper.TerraformVersion()) && !step.ExpectNonEmptyPlan { var stdout string - err = runProviderCommand(ctx, t, func() error { + err = runProviderCommand(ctx, t, wd, providers, func() error { var err error stdout, err = wd.SavedPlanRawStdout(ctx) return err - }, wd, providers) + }) if err != nil { - return fmt.Errorf("Error retrieving formatted second plan output: %w", err) + return fmt.Errorf("Error reading human-readable refresh plan output: %w", err) } - return fmt.Errorf("After applying this test step and performing a `terraform refresh`, the plan was not empty.\nstdout\n\n%s", stdout) - } else if step.ExpectNonEmptyPlan && planIsEmpty(plan) { - return errors.New("Expected a non-empty plan, but got an empty plan") + + if step.PlanOnly { + return fmt.Errorf("The refresh plan was not empty.\nstdout\n\n%s", stdout) + } + + return fmt.Errorf("After applying this test step, the refresh plan was not empty.\nstdout\n\n%s", stdout) + } else if step.ExpectNonEmptyPlan && planIsEmpty(plan, helper.TerraformVersion()) { + return errors.New("Expected a non-empty plan, but got an empty refresh plan") } // ID-ONLY REFRESH @@ -232,18 +406,19 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint var state *terraform.State - err = runProviderCommand(ctx, t, func() error { - state, err = getState(ctx, t, wd) + err = runProviderCommand(ctx, t, wd, providers, func() error { + _, state, err = getState(ctx, t, wd) if err != nil { return err } return nil - }, wd, providers) + }) if err != nil { return err } + //nolint:staticcheck // legacy usage if state.Empty() { return nil } @@ -268,12 +443,27 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint // this fails. If refresh isn't read-only, then this will have // caught a different bug. if idRefreshCheck != nil { - if err := testIDRefresh(ctx, t, c, wd, step, idRefreshCheck, providers); err != nil { + fmt.Println("Not Writing by testing ID Refresh") + if err := testIDRefresh(ctx, t, c, wd, step, idRefreshCheck, providers, stepIndex, helper); err != nil { return fmt.Errorf( "[ERROR] Test: ID-only test failed: %s", err) } } } + if refreshAfterApply && !step.Destroy && !step.PlanOnly { + if len(c.Steps) > stepIndex+1 { + // If the next step is a refresh, then we have no need to refresh here + if !c.Steps[stepIndex+1].RefreshState { + // Log a searchable message to easily determine when this is no longer being used + logging.HelperResourceDebug(ctx, EnvTfAccRefreshAfterApply+": running apply -refresh-only -refresh=true") + err := runProviderCommandApplyRefreshOnly(ctx, t, wd, providers) + if err != nil { + return fmt.Errorf("Error running apply refresh-only: %w", err) + } + } + } + } + return nil } diff --git a/helper/resource/testing_new_config_test.go b/helper/resource/testing_new_config_test.go index d6ea46be9..147a24dfa 100644 --- a/helper/resource/testing_new_config_test.go +++ b/helper/resource/testing_new_config_test.go @@ -8,47 +8,382 @@ import ( "regexp" "testing" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-testing/internal/testing/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/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfversion" ) func TestTest_TestStep_ExpectError_NewConfig(t *testing.T) { t.Parallel() - Test(t, TestCase{ + UnitTest(t, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + }, + 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: "id", + Type: tftypes.String, + Required: true, + }, + }, + }, + }, + }, + ValidateConfigResponse: &resource.ValidateConfigResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Invalid Attribute Value", + Detail: "Diagnostic details", + }, + }, + }, + }, + }, + }), + }, + Steps: []TestStep{ + { + Config: `resource "test_resource" "test" { + id = "invalid-value" + }`, + ExpectError: regexp.MustCompile(`Error: Invalid Attribute Value`), + }, + }, + }) +} + +func Test_ExpectNonEmptyPlan_EmptyPlanError(t *testing.T) { + t.Parallel() + + UnitTest(t, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_4_0), + }, ExternalProviders: map[string]ExternalProvider{ - "random": { - Source: "registry.terraform.io/hashicorp/random", - VersionConstraint: "3.4.3", + "terraform": {Source: "terraform.io/builtin/terraform"}, + }, + Steps: []TestStep{ + { + Config: `resource "terraform_data" "test" {}`, + ExpectNonEmptyPlan: true, + ExpectError: regexp.MustCompile("Expected a non-empty plan, but got an empty refresh plan"), }, }, + }) +} + +func Test_ExpectNonEmptyPlan_PreRefresh_ResourceChanges(t *testing.T) { + t.Parallel() + + UnitTest(t, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_4_0), + }, + ExternalProviders: map[string]ExternalProvider{ + "terraform": {Source: "terraform.io/builtin/terraform"}, + }, Steps: []TestStep{ { - Config: `resource "random_string" "one" { - length = 2 - min_upper = 4 + Config: `resource "terraform_data" "test" { + # Never recommended for real world configurations, but tests + # the intended behavior. + input = timestamp() }`, - ExpectError: regexp.MustCompile(`Error: Invalid Attribute Value`), + ConfigPlanChecks: ConfigPlanChecks{ + // Verification of that the behavior is being caught pre + // refresh. We want to ensure ExpectNonEmptyPlan allows test + // to pass if pre refresh also has changes. + PostApplyPreRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("terraform_data.test", plancheck.ResourceActionUpdate), + }, + }, + ExpectNonEmptyPlan: true, }, }, }) } -func Test_ConfigPlanChecks_PreApply_Called(t *testing.T) { +func Test_ExpectNonEmptyPlan_PostRefresh_OutputChanges(t *testing.T) { t.Parallel() - spy1 := &planCheckSpy{} - spy2 := &planCheckSpy{} - Test(t, TestCase{ + UnitTest(t, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipAbove(tfversion.Version0_14_0), // outputs before 0.14 always show as created + }, + // Avoid our own validation that requires at least one provider config. ExternalProviders: map[string]ExternalProvider{ - "random": { - Source: "registry.terraform.io/hashicorp/random", + "terraform": {Source: "terraform.io/builtin/terraform"}, + }, + Steps: []TestStep{ + { + Config: `output "test" { value = timestamp() }`, + ExpectNonEmptyPlan: false, // compatibility compromise for 0.12 and 0.13 }, }, + }) + + UnitTest(t, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version0_14_0), // outputs before 0.14 always show as created + }, + // Avoid our own validation that requires at least one provider config. + ExternalProviders: map[string]ExternalProvider{ + "terraform": {Source: "terraform.io/builtin/terraform"}, + }, + Steps: []TestStep{ + { + Config: `output "test" { value = timestamp() }`, + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func Test_ExpectNonEmptyPlan_PostRefresh_ResourceChanges(t *testing.T) { + t.Parallel() + + UnitTest(t, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test"), // intentionally same + }, + ), + }, + ReadResponse: &resource.ReadResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "not-test"), // intentionally different + }, + ), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Required: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + Steps: []TestStep{ + { + Config: `resource "test_resource" "test" { + # Post create refresh intentionally changes configured value + # which is an errant resource implementation. Create should + # account for the correct post creation state, preventing an + # immediate difference next Terraform run for practitioners. + # This errant resource behavior verifies the expected + # behavior of ExpectNonEmptyPlan for post refresh planning. + id = "test" + }`, + ConfigPlanChecks: ConfigPlanChecks{ + // Verification of that the behavior is being caught post + // refresh. We want to ensure ExpectNonEmptyPlan is being + // triggered after the pre refresh plan shows no changes. + PostApplyPreRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("test_resource.test", plancheck.ResourceActionNoop), + }, + }, + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func Test_NonEmptyPlan_PreRefresh_Error(t *testing.T) { + t.Parallel() + + UnitTest(t, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_4_0), + }, + ExternalProviders: map[string]ExternalProvider{ + "terraform": {Source: "terraform.io/builtin/terraform"}, + }, + Steps: []TestStep{ + { + Config: `resource "terraform_data" "test" { + # Never recommended for real world configurations, but tests + # the intended behavior. + input = timestamp() + }`, + ConfigPlanChecks: ConfigPlanChecks{ + // Verification of that the behavior is being caught pre + // refresh. + PostApplyPreRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("terraform_data.test", plancheck.ResourceActionUpdate), + }, + }, + ExpectNonEmptyPlan: false, // intentional + ExpectError: regexp.MustCompile("After applying this test step, the non-refresh plan was not empty."), + }, + }, + }) +} + +func Test_NonEmptyPlan_PostRefresh_Error(t *testing.T) { + t.Parallel() + + UnitTest(t, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test"), // intentionally same + }, + ), + }, + ReadResponse: &resource.ReadResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "not-test"), // intentionally different + }, + ), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Required: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, Steps: []TestStep{ { - Config: `resource "random_string" "one" { - length = 16 + Config: `resource "test_resource" "test" { + # Post create refresh intentionally changes configured value + # which is an errant resource implementation. Create should + # account for the correct post creation state, preventing an + # immediate difference next Terraform run for practitioners. + # This errant resource behavior verifies the expected + # behavior of ExpectNonEmptyPlan for post refresh planning. + id = "test" }`, + ConfigPlanChecks: ConfigPlanChecks{ + // Verification of that the behavior is being caught post + // refresh. + PostApplyPreRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("test_resource.test", plancheck.ResourceActionNoop), + }, + }, + ExpectNonEmptyPlan: false, // intentional + ExpectError: regexp.MustCompile("After applying this test step, the refresh plan was not empty."), + }, + }, + }) +} + +func Test_ConfigPlanChecks_PreApply_Called(t *testing.T) { + t.Parallel() + + spy1 := &planCheckSpy{} + spy2 := &planCheckSpy{} + UnitTest(t, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + Steps: []TestStep{ + { + Config: `resource "test_resource" "test" {}`, ConfigPlanChecks: ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ spy1, @@ -78,17 +413,46 @@ func Test_ConfigPlanChecks_PreApply_Errors(t *testing.T) { spy3 := &planCheckSpy{ err: errors.New("spy3 check failed"), } - Test(t, TestCase{ - ExternalProviders: map[string]ExternalProvider{ - "random": { - Source: "registry.terraform.io/hashicorp/random", - }, + UnitTest(t, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), }, Steps: []TestStep{ { - Config: `resource "random_string" "one" { - length = 16 - }`, + Config: `resource "test_resource" "test" {}`, ConfigPlanChecks: ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ spy1, @@ -107,17 +471,46 @@ func Test_ConfigPlanChecks_PostApplyPreRefresh_Called(t *testing.T) { spy1 := &planCheckSpy{} spy2 := &planCheckSpy{} - Test(t, TestCase{ - ExternalProviders: map[string]ExternalProvider{ - "random": { - Source: "registry.terraform.io/hashicorp/random", - }, + UnitTest(t, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), }, Steps: []TestStep{ { - Config: `resource "random_string" "one" { - length = 16 - }`, + Config: `resource "test_resource" "test" {}`, ConfigPlanChecks: ConfigPlanChecks{ PostApplyPreRefresh: []plancheck.PlanCheck{ spy1, @@ -147,17 +540,46 @@ func Test_ConfigPlanChecks_PostApplyPreRefresh_Errors(t *testing.T) { spy3 := &planCheckSpy{ err: errors.New("spy3 check failed"), } - Test(t, TestCase{ - ExternalProviders: map[string]ExternalProvider{ - "random": { - Source: "registry.terraform.io/hashicorp/random", - }, + UnitTest(t, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), }, Steps: []TestStep{ { - Config: `resource "random_string" "one" { - length = 16 - }`, + Config: `resource "test_resource" "test" {}`, ConfigPlanChecks: ConfigPlanChecks{ PostApplyPreRefresh: []plancheck.PlanCheck{ spy1, @@ -176,17 +598,46 @@ func Test_ConfigPlanChecks_PostApplyPostRefresh_Called(t *testing.T) { spy1 := &planCheckSpy{} spy2 := &planCheckSpy{} - Test(t, TestCase{ - ExternalProviders: map[string]ExternalProvider{ - "random": { - Source: "registry.terraform.io/hashicorp/random", - }, + UnitTest(t, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), }, Steps: []TestStep{ { - Config: `resource "random_string" "one" { - length = 16 - }`, + Config: `resource "test_resource" "test" {}`, ConfigPlanChecks: ConfigPlanChecks{ PostApplyPostRefresh: []plancheck.PlanCheck{ spy1, @@ -216,17 +667,46 @@ func Test_ConfigPlanChecks_PostApplyPostRefresh_Errors(t *testing.T) { spy3 := &planCheckSpy{ err: errors.New("spy3 check failed"), } - Test(t, TestCase{ - ExternalProviders: map[string]ExternalProvider{ - "random": { - Source: "registry.terraform.io/hashicorp/random", - }, + UnitTest(t, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), }, Steps: []TestStep{ { - Config: `resource "random_string" "one" { - length = 16 - }`, + Config: `resource "test_resource" "test" {}`, ConfigPlanChecks: ConfigPlanChecks{ PostApplyPostRefresh: []plancheck.PlanCheck{ spy1, @@ -239,3 +719,126 @@ func Test_ConfigPlanChecks_PostApplyPostRefresh_Errors(t *testing.T) { }, }) } + +func Test_ConfigStateChecks_Called(t *testing.T) { + t.Parallel() + + spy1 := &stateCheckSpy{} + spy2 := &stateCheckSpy{} + UnitTest(t, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + Steps: []TestStep{ + { + Config: `resource "test_resource" "test" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + spy1, + spy2, + }, + }, + }, + }) + + if !spy1.called { + t.Error("expected ConfigStateChecks spy1 to be called at least once") + } + + if !spy2.called { + t.Error("expected ConfigStateChecks spy2 to be called at least once") + } +} + +func Test_ConfigStateChecks_Errors(t *testing.T) { + t.Parallel() + + spy1 := &stateCheckSpy{} + spy2 := &stateCheckSpy{ + err: errors.New("spy2 check failed"), + } + spy3 := &stateCheckSpy{ + err: errors.New("spy3 check failed"), + } + UnitTest(t, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + Steps: []TestStep{ + { + Config: `resource "test_resource" "test" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + spy1, + spy2, + spy3, + }, + ExpectError: regexp.MustCompile(`.*?(spy2 check failed)\n.*?(spy3 check failed)`), + }, + }, + }) +} diff --git a/helper/resource/testing_new_import_state.go b/helper/resource/testing_new_import_state.go index 027359ada..d2d71311c 100644 --- a/helper/resource/testing_new_import_state.go +++ b/helper/resource/testing_new_import_state.go @@ -5,36 +5,54 @@ package resource import ( "context" + "encoding/json" "fmt" "reflect" "strings" + "github.com/hashicorp/go-version" + + tfjson "github.com/hashicorp/terraform-json" + "github.com/google/go-cmp/cmp" "github.com/mitchellh/go-testing-interface" - "github.com/hashicorp/terraform-plugin-testing/terraform" - + "github.com/hashicorp/terraform-plugin-testing/config" "github.com/hashicorp/terraform-plugin-testing/internal/logging" "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-plugin-testing/tfversion" ) -func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest.Helper, wd *plugintest.WorkingDir, step TestStep, cfg string, providers *providerFactories) error { +func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest.Helper, testCaseWorkingDir *plugintest.WorkingDir, step TestStep, priorStepCfg teststep.Config, providers *providerFactories, stepNumber int) error { t.Helper() - if step.ResourceName == "" { + // step.ImportStateKind implicitly defaults to the zero-value (ImportCommandWithID) for backward compatibility + kind := step.ImportStateKind + importStatePersist := step.ImportStatePersist + + if err := importStatePreconditions(t, helper, step); err != nil { + return err + } + + resourceName := step.ResourceName + if resourceName == "" { t.Fatal("ResourceName is required for an import state test") } // get state from check sequence var state *terraform.State + var stateJSON *tfjson.State var err error - err = runProviderCommand(ctx, t, func() error { - state, err = getState(ctx, t, wd) + + err = runProviderCommand(ctx, t, testCaseWorkingDir, providers, func() error { + stateJSON, state, err = getState(ctx, t, testCaseWorkingDir) if err != nil { return err } return nil - }, wd, providers) + }) if err != nil { t.Fatalf("Error getting state: %s", err) } @@ -63,7 +81,7 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest default: logging.HelperResourceTrace(ctx, "Using resource identifier for import identifier") - resource, err := testResource(step, state) + resource, err := testResource(resourceName, state) if err != nil { t.Fatal(err) } @@ -78,87 +96,178 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest logging.HelperResourceTrace(ctx, fmt.Sprintf("Using import identifier: %s", importId)) - // Create working directory for import tests - if step.Config == "" { - logging.HelperResourceTrace(ctx, "Using prior TestStep Config for import") + var priorIdentityValues map[string]any + + if kind.plannable() && kind.resourceIdentity() { + priorIdentityValues = identityValuesFromStateValues(stateJSON.Values, resourceName) + if len(priorIdentityValues) == 0 { + return fmt.Errorf("importing resource %s: expected prior state to have resource identity values, got none", resourceName) + } + } - step.Config = cfg - if step.Config == "" { + testStepConfigRequest := config.TestStepConfigRequest{ + StepNumber: stepNumber, + TestName: t.Name(), + } + testStepConfig := teststep.Configuration(teststep.PrepareConfigurationRequest{ + Directory: step.ConfigDirectory, + File: step.ConfigFile, + Raw: step.Config, + TestStepConfigRequest: testStepConfigRequest, + }.Exec()) + + // If the current import state test step doesn't have configuration, use the prior test step config + if testStepConfig == nil { + if priorStepCfg == nil { t.Fatal("Cannot import state with no specified config") } + + logging.HelperResourceTrace(ctx, "Using prior TestStep Config for import") + + testStepConfig = priorStepCfg } - var importWd *plugintest.WorkingDir + switch { + case step.ImportStateConfigExact: + break + + case kind.plannable() && kind.resourceIdentity(): + testStepConfig = appendImportBlockWithIdentity(testStepConfig, resourceName, priorIdentityValues) - // Use the same working directory to persist the state from import - if step.ImportStatePersist { - importWd = wd + case kind.plannable(): + testStepConfig = appendImportBlock(testStepConfig, resourceName, importId) + } + + var workingDir *plugintest.WorkingDir + if importStatePersist { + workingDir = testCaseWorkingDir } else { - importWd = helper.RequireNewWorkingDir(ctx, t, "") - defer importWd.Close() + workingDir = helper.RequireNewWorkingDir(ctx, t, "") + defer workingDir.Close() } - err = importWd.SetConfig(ctx, step.Config) + err = workingDir.SetConfig(ctx, testStepConfig, step.ConfigVariables) if err != nil { t.Fatalf("Error setting test config: %s", err) } - logging.HelperResourceDebug(ctx, "Running Terraform CLI init and import") + if kind.plannable() { + if stepNumber > 1 { + err = workingDir.CopyState(ctx, testCaseWorkingDir.StateFilePath()) + if err != nil { + t.Fatalf("copying state: %s", err) + } + + err = runProviderCommand(ctx, t, workingDir, providers, func() error { + return workingDir.RemoveResource(ctx, resourceName) + }) + if err != nil { + t.Fatalf("removing resource %s from copied state: %s", resourceName, err) + } + } + } - if !step.ImportStatePersist { - err = runProviderCommand(ctx, t, func() error { - return importWd.Init(ctx) - }, importWd, providers) + if !importStatePersist { + err = runProviderCommand(ctx, t, workingDir, providers, func() error { + return workingDir.Init(ctx) + }) if err != nil { t.Fatalf("Error running init: %s", err) } } - err = runProviderCommand(ctx, t, func() error { - return importWd.Import(ctx, step.ResourceName, importId) - }, importWd, providers) + if kind.plannable() { + return testImportBlock(ctx, t, workingDir, providers, resourceName, step, priorIdentityValues) + } else { + return testImportCommand(ctx, t, workingDir, providers, resourceName, importId, step, state) + } +} + +func testImportBlock(ctx context.Context, t testing.T, workingDir *plugintest.WorkingDir, providers *providerFactories, resourceName string, step TestStep, priorIdentityValues map[string]any) error { + kind := step.ImportStateKind + + err := runProviderCommandCreatePlan(ctx, t, workingDir, providers) + if err != nil { + return fmt.Errorf("generating plan with import config: %s", err) + } + + plan, err := runProviderCommandSavedPlan(ctx, t, workingDir, providers) + if err != nil { + return fmt.Errorf("reading generated plan with import config: %s", err) + } + + logging.HelperResourceDebug(ctx, fmt.Sprintf("ImportBlockWithId: %d resource changes", len(plan.ResourceChanges))) + + // Verify reasonable things about the plan + var resourceChangeUnderTest *tfjson.ResourceChange + + if len(plan.ResourceChanges) == 0 { + return fmt.Errorf("importing resource %s: expected a resource change, got no changes", resourceName) + } + + for _, change := range plan.ResourceChanges { + if change.Address == resourceName { + resourceChangeUnderTest = change + } + } + + if resourceChangeUnderTest == nil || resourceChangeUnderTest.Change == nil || resourceChangeUnderTest.Change.Actions == nil { + return fmt.Errorf("importing resource %s: expected a resource change, got no changes", resourceName) + } + + change := resourceChangeUnderTest.Change + actions := change.Actions + importing := change.Importing + + switch { + case importing == nil: + return fmt.Errorf("importing resource %s: expected an import operation, got %q action with plan \nstdout:\n\n%s", resourceChangeUnderTest.Address, actions, savedPlanRawStdout(ctx, t, workingDir, providers)) + // By default we want to ensure there isn't a proposed plan after importing, but for some resources this is unavoidable. + // An example would be importing a resource that cannot read it's entire value back from the remote API. + case !step.ExpectNonEmptyPlan && !actions.NoOp(): + return fmt.Errorf("importing resource %s: expected a no-op import operation, got %q action with plan \nstdout:\n\n%s", resourceChangeUnderTest.Address, actions, savedPlanRawStdout(ctx, t, workingDir, providers)) + } + + if err := runPlanChecks(ctx, t, plan, step.ImportPlanChecks.PreApply); err != nil { + return err + } + + if kind.resourceIdentity() { + newIdentityValues := identityValuesFromStateValues(plan.PlannedValues, resourceName) + if !cmp.Equal(priorIdentityValues, newIdentityValues) { + return fmt.Errorf("importing resource %s: expected identity values %v, got %v", resourceName, priorIdentityValues, newIdentityValues) + } + } + + return nil +} + +func testImportCommand(ctx context.Context, t testing.T, workingDir *plugintest.WorkingDir, providers *providerFactories, resourceName string, importId string, step TestStep, state *terraform.State) error { + err := runProviderCommand(ctx, t, workingDir, providers, func() error { + return workingDir.Import(ctx, resourceName, importId) + }) if err != nil { return err } var importState *terraform.State - err = runProviderCommand(ctx, t, func() error { - importState, err = getState(ctx, t, importWd) + err = runProviderCommand(ctx, t, workingDir, providers, func() error { + _, importState, err = getState(ctx, t, workingDir) if err != nil { return err } return nil - }, importWd, providers) + }) if err != nil { t.Fatalf("Error getting state: %s", err) } + logging.HelperResourceDebug(ctx, fmt.Sprintf("State after import: %d resources in the root module", len(importState.RootModule().Resources))) + // Go through the imported state and verify if step.ImportStateCheck != nil { logging.HelperResourceTrace(ctx, "Using TestStep ImportStateCheck") - - var states []*terraform.InstanceState - for address, r := range importState.RootModule().Resources { - if strings.HasPrefix(address, "data.") { - continue - } - - if r.Primary == nil { - continue - } - - is := r.Primary.DeepCopy() - is.Ephemeral.Type = r.Type // otherwise the check function cannot see the type - states = append(states, is) - } - - logging.HelperResourceDebug(ctx, "Calling TestStep ImportStateCheck") - - if err := step.ImportStateCheck(states); err != nil { - t.Fatal(err) - } - - logging.HelperResourceDebug(ctx, "Called TestStep ImportStateCheck") + runImportStateCheckFunction(ctx, t, importState, step) } // Verify that all the states match @@ -182,12 +291,33 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest } } + identifierAttribute := step.ImportStateVerifyIdentifierAttribute + + if identifierAttribute == "" { + identifierAttribute = "id" + } + for _, r := range newResources { + rIdentifier, ok := r.Primary.Attributes[identifierAttribute] + + if !ok { + t.Fatalf("ImportStateVerify: New resource missing identifier attribute %q, ensure attribute value is properly set or use ImportStateVerifyIdentifierAttribute to choose different attribute", identifierAttribute) + } + // Find the existing resource var oldR *terraform.ResourceState for _, r2 := range oldResources { + if r2.Primary == nil || r2.Type != r.Type || r2.Provider != r.Provider { + continue + } - if r2.Primary != nil && r2.Primary.ID == r.Primary.ID && r2.Type == r.Type && r2.Provider == r.Provider { + r2Identifier, ok := r2.Primary.Attributes[identifierAttribute] + + if !ok { + t.Fatalf("ImportStateVerify: Old resource missing identifier attribute %q, ensure attribute value is properly set or use ImportStateVerifyIdentifierAttribute to choose different attribute", identifierAttribute) + } + + if r2Identifier == rIdentifier { oldR = r2 break } @@ -195,7 +325,7 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest if oldR == nil || oldR.Primary == nil { t.Fatalf( "Failed state verification, resource with ID %s not found", - r.Primary.ID) + rIdentifier) } // don't add empty flatmapped containers, so we can more easily @@ -280,3 +410,162 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest return nil } + +func appendImportBlock(config teststep.Config, resourceName string, importID string) teststep.Config { + return config.Append( + fmt.Sprintf(``+"\n"+ + `import {`+"\n"+ + ` to = %s`+"\n"+ + ` id = %q`+"\n"+ + `}`, + resourceName, importID)) +} + +func appendImportBlockWithIdentity(config teststep.Config, resourceName string, identityValues map[string]any) teststep.Config { + configBuilder := strings.Builder{} + configBuilder.WriteString(fmt.Sprintf(``+"\n"+ + `import {`+"\n"+ + ` to = %s`+"\n"+ + ` identity = {`+"\n", + resourceName)) + + for k, v := range identityValues { + // It's valid for identity attributes to be null, we can just omit it from config + if v == nil { + continue + } + + switch v := v.(type) { + case bool: + configBuilder.WriteString(fmt.Sprintf(` %q = %t`+"\n", k, v)) + + case []any: + var quotedV []string + for _, v := range v { + quotedV = append(quotedV, fmt.Sprintf(`%q`, v)) + } + configBuilder.WriteString(fmt.Sprintf(` %q = [%s]`+"\n", k, strings.Join(quotedV, ", "))) + + case json.Number: + configBuilder.WriteString(fmt.Sprintf(` %q = %s`+"\n", k, v)) + + case string: + configBuilder.WriteString(fmt.Sprintf(` %q = %q`+"\n", k, v)) + + default: + panic(fmt.Sprintf("unexpected type %T for identity value %q", v, k)) + } + } + + configBuilder.WriteString(` }` + "\n") + configBuilder.WriteString(`}` + "\n") + + return config.Append(configBuilder.String()) +} + +func importStatePreconditions(t testing.T, helper *plugintest.Helper, step TestStep) error { + t.Helper() + + kind := step.ImportStateKind + versionUnderTest := *helper.TerraformVersion().Core() + resourceIdentityMinimumVersion := version.Must(version.NewVersion("1.12.0")) + + // Instead of calling [t.Fatal], we return an error. This package's unit tests can use [TestStep.ExpectError] to match + // on the error message. An alternative, [plugintest.TestExpectTFatal], does not have access to logged error messages, + // so it is open to false positives on this complex code path. + // + // Multiple cases may match, so check the most specific cases first + switch { + case kind.resourceIdentity() && versionUnderTest.LessThan(resourceIdentityMinimumVersion): + return fmt.Errorf( + `ImportState steps using resource identity require Terraform 1.12.0 or later. Either ` + + `upgrade the Terraform version running the test or add a ` + "`TerraformVersionChecks`" + ` to ` + + `the test case to skip this test.` + "\n\n" + + `https://developer.hashicorp.com/terraform/plugin/testing/acceptance-tests/tfversion-checks#skip-version-checks`) + + case kind.plannable() && versionUnderTest.LessThan(tfversion.Version1_5_0): + return fmt.Errorf( + `ImportState steps using plannable import blocks require Terraform 1.5.0 or later. Either ` + + `upgrade the Terraform version running the test or add a ` + "`TerraformVersionChecks`" + ` to ` + + `the test case to skip this test.` + "\n\n" + + `https://developer.hashicorp.com/terraform/plugin/testing/acceptance-tests/tfversion-checks#skip-version-checks`) + + case kind.plannable() && step.ImportStatePersist: + return fmt.Errorf(`ImportStatePersist is not supported with plannable import blocks`) + + case kind.plannable() && step.ImportStateVerify: + return fmt.Errorf(`ImportStateVerify is not supported with plannable import blocks`) + } + + return nil +} + +func resourcesFromState(stateValues *tfjson.StateValues) []*tfjson.StateResource { + if stateValues == nil || stateValues.RootModule == nil { + return []*tfjson.StateResource{} + } + + return stateValues.RootModule.Resources +} + +func identityValuesFromStateValues(stateValues *tfjson.StateValues, resourceName string) map[string]any { + var resource *tfjson.StateResource + resources := resourcesFromState(stateValues) + + for _, r := range resources { + if r.Address == resourceName { + resource = r + break + } + } + + if resource == nil || len(resource.IdentityValues) == 0 { + return map[string]any{} + } + + return resource.IdentityValues +} + +func runImportStateCheckFunction(ctx context.Context, t testing.T, importState *terraform.State, step TestStep) { + t.Helper() + + var states []*terraform.InstanceState + for address, r := range importState.RootModule().Resources { + if strings.HasPrefix(address, "data.") { + continue + } + + if r.Primary == nil { + continue + } + + is := r.Primary.DeepCopy() //nolint:staticcheck // legacy usage + is.Ephemeral.Type = r.Type // otherwise the check function cannot see the type + states = append(states, is) + } + + logging.HelperResourceTrace(ctx, "Calling TestStep ImportStateCheck") + + if err := step.ImportStateCheck(states); err != nil { + t.Fatal(err) + } + + logging.HelperResourceTrace(ctx, "Called TestStep ImportStateCheck") +} + +func savedPlanRawStdout(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, providers *providerFactories) string { + t.Helper() + + var stdout string + + err := runProviderCommand(ctx, t, wd, providers, func() error { + var err error + stdout, err = wd.SavedPlanRawStdout(ctx) + return err + }) + + if err != nil { + return fmt.Sprintf("error retrieving formatted plan output: %s", err) + } + return stdout +} diff --git a/helper/resource/testing_new_import_state_test.go b/helper/resource/testing_new_import_state_test.go deleted file mode 100644 index f864792ff..000000000 --- a/helper/resource/testing_new_import_state_test.go +++ /dev/null @@ -1,222 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package resource - -import ( - "context" - "fmt" - "regexp" - "testing" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - - "github.com/hashicorp/terraform-plugin-testing/terraform" -) - -func TestTest_TestStep_ImportStateCheck_SkipDataSourceState(t *testing.T) { - t.Parallel() - - UnitTest(t, TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "examplecloud": func() (*schema.Provider, error) { //nolint:unparam // required signature - return &schema.Provider{ - DataSourcesMap: map[string]*schema.Resource{ - "examplecloud_thing": { - ReadContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { - d.SetId("datasource-test") - - return nil - }, - Schema: map[string]*schema.Schema{ - "id": { - Type: schema.TypeString, - Computed: true, - }, - }, - }, - }, - ResourcesMap: map[string]*schema.Resource{ - "examplecloud_thing": { - CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { - d.SetId("resource-test") - - return nil - }, - DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { - return nil - }, - ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { - return nil - }, - Schema: map[string]*schema.Schema{ - "id": { - Computed: true, - Type: schema.TypeString, - }, - }, - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - }, - }, - }, - }, nil - }, - }, - Steps: []TestStep{ - { - Config: ` - data "examplecloud_thing" "test" {} - resource "examplecloud_thing" "test" {} - `, - }, - { - ResourceName: "examplecloud_thing.test", - ImportState: true, - ImportStateCheck: func(is []*terraform.InstanceState) error { - if len(is) > 1 { - return fmt.Errorf("expected 1 state, got: %d", len(is)) - } - - return nil - }, - }, - }, - }) -} - -func TestTest_TestStep_ImportStateVerify(t *testing.T) { - t.Parallel() - - UnitTest(t, TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "examplecloud": func() (*schema.Provider, error) { //nolint:unparam // required signature - return &schema.Provider{ - ResourcesMap: map[string]*schema.Resource{ - "examplecloud_thing": { - CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { - d.SetId("resource-test") - - return nil - }, - DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { - return nil - }, - ReadContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { - _ = d.Set("other", "testvalue") - - return nil - }, - Schema: map[string]*schema.Schema{ - "other": { - Computed: true, - Type: schema.TypeString, - }, - "id": { - Computed: true, - Type: schema.TypeString, - }, - }, - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - }, - }, - }, - }, nil - }, - }, - Steps: []TestStep{ - { - Config: `resource "examplecloud_thing" "test" {}`, - }, - { - ResourceName: "examplecloud_thing.test", - ImportState: true, - ImportStateVerify: true, - }, - }, - }) -} - -func TestTest_TestStep_ImportStateVerifyIgnore(t *testing.T) { - t.Parallel() - - UnitTest(t, TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "examplecloud": func() (*schema.Provider, error) { //nolint:unparam // required signature - return &schema.Provider{ - ResourcesMap: map[string]*schema.Resource{ - "examplecloud_thing": { - CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { - d.SetId("resource-test") - - _ = d.Set("create_only", "testvalue") - - return nil - }, - DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { - return nil - }, - ReadContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { - _ = d.Set("read_only", "testvalue") - - return nil - }, - Schema: map[string]*schema.Schema{ - "create_only": { - Computed: true, - Type: schema.TypeString, - }, - "read_only": { - Computed: true, - Type: schema.TypeString, - }, - "id": { - Computed: true, - Type: schema.TypeString, - }, - }, - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - }, - }, - }, - }, nil - }, - }, - Steps: []TestStep{ - { - Config: `resource "examplecloud_thing" "test" {}`, - }, - { - ResourceName: "examplecloud_thing.test", - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"create_only"}, - }, - }, - }) -} - -func TestTest_TestStep_ExpectError_ImportState(t *testing.T) { - t.Parallel() - - Test(t, TestCase{ - ExternalProviders: map[string]ExternalProvider{ - "random": { - Source: "registry.terraform.io/hashicorp/time", - VersionConstraint: "0.9.1", - }, - }, - Steps: []TestStep{ - { - Config: `resource "time_static" "one" {}`, - ImportStateId: "invalid time string", - ResourceName: "time_static.one", - ImportState: true, - ExpectError: regexp.MustCompile(`Error: Import time static error`), - }, - }, - }) -} diff --git a/helper/resource/testing_new_refresh_state.go b/helper/resource/testing_new_refresh_state.go index 86073b165..b1971a289 100644 --- a/helper/resource/testing_new_refresh_state.go +++ b/helper/resource/testing_new_refresh_state.go @@ -21,32 +21,32 @@ func testStepNewRefreshState(ctx context.Context, t testing.T, wd *plugintest.Wo var err error // Explicitly ensure prior state exists before refresh. - err = runProviderCommand(ctx, t, func() error { - _, err = getState(ctx, t, wd) + err = runProviderCommand(ctx, t, wd, providers, func() error { + _, _, err = getState(ctx, t, wd) if err != nil { return err } return nil - }, wd, providers) + }) if err != nil { t.Fatalf("Error getting state: %s", err) } - err = runProviderCommand(ctx, t, func() error { + err = runProviderCommand(ctx, t, wd, providers, func() error { return wd.Refresh(ctx) - }, wd, providers) + }) if err != nil { return err } var refreshState *terraform.State - err = runProviderCommand(ctx, t, func() error { - refreshState, err = getState(ctx, t, wd) + err = runProviderCommand(ctx, t, wd, providers, func() error { + _, refreshState, err = getState(ctx, t, wd) if err != nil { return err } return nil - }, wd, providers) + }) if err != nil { t.Fatalf("Error getting state: %s", err) } @@ -63,19 +63,19 @@ func testStepNewRefreshState(ctx context.Context, t testing.T, wd *plugintest.Wo } // do a plan - err = runProviderCommand(ctx, t, func() error { + err = runProviderCommand(ctx, t, wd, providers, func() error { return wd.CreatePlan(ctx) - }, wd, providers) + }) if err != nil { return fmt.Errorf("Error running post-refresh plan: %w", err) } var plan *tfjson.Plan - err = runProviderCommand(ctx, t, func() error { + err = runProviderCommand(ctx, t, wd, providers, func() error { var err error plan, err = wd.SavedPlan(ctx) return err - }, wd, providers) + }) if err != nil { return fmt.Errorf("Error retrieving post-refresh plan: %w", err) } @@ -88,13 +88,13 @@ func testStepNewRefreshState(ctx context.Context, t testing.T, wd *plugintest.Wo } } - if !planIsEmpty(plan) && !step.ExpectNonEmptyPlan { + if !planIsEmpty(plan, wd.GetHelper().TerraformVersion()) && !step.ExpectNonEmptyPlan { var stdout string - err = runProviderCommand(ctx, t, func() error { + err = runProviderCommand(ctx, t, wd, providers, func() error { var err error stdout, err = wd.SavedPlanRawStdout(ctx) return err - }, wd, providers) + }) if err != nil { return fmt.Errorf("Error retrieving formatted plan output: %w", err) } diff --git a/helper/resource/testing_new_refresh_state_test.go b/helper/resource/testing_new_refresh_state_test.go index f82622a56..70b135a08 100644 --- a/helper/resource/testing_new_refresh_state_test.go +++ b/helper/resource/testing_new_refresh_state_test.go @@ -8,7 +8,13 @@ import ( "regexp" "testing" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/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/plancheck" + "github.com/hashicorp/terraform-plugin-testing/tfversion" ) func Test_RefreshPlanChecks_PostRefresh_Called(t *testing.T) { @@ -16,17 +22,46 @@ func Test_RefreshPlanChecks_PostRefresh_Called(t *testing.T) { spy1 := &planCheckSpy{} spy2 := &planCheckSpy{} - Test(t, TestCase{ - ExternalProviders: map[string]ExternalProvider{ - "random": { - Source: "registry.terraform.io/hashicorp/random", - }, + UnitTest(t, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), }, Steps: []TestStep{ { - Config: `resource "random_string" "one" { - length = 16 - }`, + Config: `resource "test_resource" "test" {}`, }, { RefreshState: true, @@ -59,17 +94,46 @@ func Test_RefreshPlanChecks_PostRefresh_Errors(t *testing.T) { spy3 := &planCheckSpy{ err: errors.New("spy3 check failed"), } - Test(t, TestCase{ - ExternalProviders: map[string]ExternalProvider{ - "random": { - Source: "registry.terraform.io/hashicorp/random", - }, + UnitTest(t, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), }, Steps: []TestStep{ { - Config: `resource "random_string" "one" { - length = 16 - }`, + Config: `resource "test_resource" "test" {}`, }, { RefreshState: true, @@ -85,3 +149,60 @@ func Test_RefreshPlanChecks_PostRefresh_Errors(t *testing.T) { }, }) } + +func Test_RefreshState_ExpectNonEmptyPlan(t *testing.T) { + t.Parallel() + + UnitTest(t, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_4_0), + }, + ExternalProviders: map[string]ExternalProvider{ + "terraform": {Source: "terraform.io/builtin/terraform"}, + }, + Steps: []TestStep{ + { + Config: `resource "terraform_data" "test" { + # Never recommended for real world configurations, but tests + # the intended behavior. + input = timestamp() + }`, + ExpectNonEmptyPlan: false, // intentional + ExpectError: regexp.MustCompile("After applying this test step, the non-refresh plan was not empty."), + }, + { + RefreshState: true, + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func Test_RefreshState_NonEmptyPlan_Error(t *testing.T) { + t.Parallel() + + UnitTest(t, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_4_0), + }, + ExternalProviders: map[string]ExternalProvider{ + "terraform": {Source: "terraform.io/builtin/terraform"}, + }, + Steps: []TestStep{ + { + Config: `resource "terraform_data" "test" { + # Never recommended for real world configurations, but tests + # the intended behavior. + input = timestamp() + }`, + ExpectNonEmptyPlan: false, // intentional + ExpectError: regexp.MustCompile("After applying this test step, the non-refresh plan was not empty."), + }, + { + RefreshState: true, + ExpectNonEmptyPlan: false, // intentional + ExpectError: regexp.MustCompile("After refreshing state during this test step, a followup plan was not empty."), + }, + }, + }) +} diff --git a/helper/resource/testing_new_test.go b/helper/resource/testing_new_test.go index 484f50ce9..22f726261 100644 --- a/helper/resource/testing_new_test.go +++ b/helper/resource/testing_new_test.go @@ -141,6 +141,21 @@ func TestShimState(t *testing.T) { "list_of_bool": { "sensitive": false, "value": [true, false, true] + }, + "tuple_one": { + "sensitive": false, + "value": [true, 1.23, "hello world"], + "type": ["tuple", ["bool", "number", "string"]] + }, + "tuple_two": { + "sensitive": false, + "value": ["hello world", true, 1.23], + "type": ["tuple", ["string", "bool", "number"]] + }, + "tuple_three": { + "sensitive": false, + "value": [1.23, "hello world", true], + "type": ["tuple", ["number", "string", "bool"]] } }, "root_module": {} @@ -198,6 +213,27 @@ func TestShimState(t *testing.T) { }, Sensitive: false, }, + "tuple_one": { + Type: "list", + Value: []interface{}{ + true, json.Number("1.23"), "hello world", + }, + Sensitive: false, + }, + "tuple_two": { + Type: "list", + Value: []interface{}{ + "hello world", true, json.Number("1.23"), + }, + Sensitive: false, + }, + "tuple_three": { + Type: "list", + Value: []interface{}{ + json.Number("1.23"), "hello world", true, + }, + Sensitive: false, + }, }, Resources: map[string]*terraform.ResourceState{}, Dependencies: []string{}, @@ -1089,8 +1125,6 @@ func TestShimState(t *testing.T) { } for i, tc := range testCases { - i, tc := i, tc - t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { t.Parallel() diff --git a/helper/resource/testing_sets.go b/helper/resource/testing_sets.go index a304bfc4e..851122204 100644 --- a/helper/resource/testing_sets.go +++ b/helper/resource/testing_sets.go @@ -57,6 +57,46 @@ const ( // If the values map is not granular enough, it is possible to match an element // you were not intending to in the set. Provide the most complete mapping of // attributes possible to be sure the unique element exists. +// +// An experimental interface exists to potentially replace the +// TestCheckTypeSetElemNestedAttrs functionality in the future and feedback +// would be appreciated. This example performs the same check as +// TestCheckTypeSetElemNestedAttrs with that experimental interface, by using +// [statecheck.ExpectKnownValue] in combination with [knownvalue.SetPartial]: +// +// package example_test +// +// import ( +// "testing" +// +// "github.com/hashicorp/terraform-plugin-testing/helper/resource" +// "github.com/hashicorp/terraform-plugin-testing/knownvalue" +// "github.com/hashicorp/terraform-plugin-testing/statecheck" +// "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +// ) +// +// func TestExpectKnownValue_CheckState_SetPartial(t *testing.T) { +// t.Parallel() +// +// resource.Test(t, resource.TestCase{ +// // Provider definition omitted. +// Steps: []resource.TestStep{ +// { +// // Example resource containing a computed set attribute named "computed_attribute" +// Config: `resource "test_resource" "one" {}`, +// ConfigStateChecks: []statecheck.StateCheck{ +// statecheck.ExpectKnownValue( +// "test_resource.one", +// tfjsonpath.New("computed_attribute"), +// knownvalue.SetPartial([]knownvalue.Check{ +// knownvalue.StringExact("value2"), +// }), +// ), +// }, +// }, +// }, +// }) +// } func TestCheckTypeSetElemNestedAttrs(name, attr string, values map[string]string) TestCheckFunc { return func(s *terraform.State) error { is, err := primaryInstanceState(s, name) @@ -129,6 +169,56 @@ func TestCheckTypeSetElemNestedAttrs(name, attr string, values map[string]string // If the values map is not granular enough, it is possible to match an element // you were not intending to in the set. Provide the most complete mapping of // attributes possible to be sure the unique element exists. +// +// If the values map is not granular enough, it is possible to match an element +// you were not intending to in the set. Provide the most complete mapping of +// attributes possible to be sure the unique element exists. +// +// An experimental interface exists to potentially replace the +// TestMatchTypeSetElemNestedAttrs functionality in the future and feedback +// would be appreciated. This example performs the same check as +// TestMatchTypeSetElemNestedAttrs with that experimental interface, by using +// [statecheck.ExpectKnownValue] in combination with [knownvalue.SetExact], +// with a nested [knownvalue.StringRegexp]: +// +// package example_test +// +// import ( +// "testing" +// +// "github.com/hashicorp/terraform-plugin-testing/helper/resource" +// "github.com/hashicorp/terraform-plugin-testing/knownvalue" +// "github.com/hashicorp/terraform-plugin-testing/statecheck" +// "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +// ) +// +// func TestExpectKnownValue_CheckState_SetNestedBlock_Custom(t *testing.T) { +// t.Parallel() +// +// resource.Test(t, resource.TestCase{ +// // Provider definition omitted. +// Steps: []resource.TestStep{ +// { +// // Example resource containing a set nested block name "block" which contains a computed string attribute named "computed_attribute" +// Config: `resource "test_resource" "one" {}`, +// ConfigStateChecks: []statecheck.StateCheck{ +// statecheck.ExpectKnownValue( +// "test_resource.one", +// tfjsonpath.New("block"), +// knownvalue.SetExact([]knownvalue.Check{ +// knownvalue.MapExact(map[string]knownvalue.Check{ +// "computed_attribute": knownvalue.StringRegexp(regexp.MustCompile("str")), +// }), +// knownvalue.MapExact(map[string]knownvalue.Check{ +// "computed_attribute": knownvalue.StringRegexp(regexp.MustCompile("rts")), +// }), +// }), +// ), +// }, +// }, +// }, +// }) +// } func TestMatchTypeSetElemNestedAttrs(name, attr string, values map[string]*regexp.Regexp) TestCheckFunc { return func(s *terraform.State) error { is, err := primaryInstanceState(s, name) @@ -200,6 +290,47 @@ func TestMatchTypeSetElemNestedAttrs(name, attr string, values map[string]*regex // - Boolean: "false" or "true". // - Float/Integer: Stringified number, such as "1.2" or "123". // - String: No conversion necessary. +// +// An experimental interface exists to potentially replace the +// TestCheckTypeSetElemAttr functionality in the future and feedback +// would be appreciated. This example performs the same check as +// TestCheckTypeSetElemAttr with that experimental interface, by using +// [statecheck.ExpectKnownValue] in combination with [knownvalue.SetExact]: +// +// package example_test +// +// import ( +// "testing" +// +// "github.com/hashicorp/terraform-plugin-testing/helper/resource" +// "github.com/hashicorp/terraform-plugin-testing/knownvalue" +// "github.com/hashicorp/terraform-plugin-testing/statecheck" +// "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +// ) +// +// func TestExpectKnownValue_CheckState_Set(t *testing.T) { +// t.Parallel() +// +// resource.Test(t, resource.TestCase{ +// // Provider definition omitted. +// Steps: []resource.TestStep{ +// { +// // Example resource containing a computed set attribute named "computed_attribute" +// Config: `resource "test_resource" "one" {}`, +// ConfigStateChecks: []statecheck.StateCheck{ +// statecheck.ExpectKnownValue( +// "test_resource.one", +// tfjsonpath.New("computed_attribute"), +// knownvalue.SetExact([]knownvalue.Check{ +// knownvalue.StringExact("value2"), +// knownvalue.StringExact("value1"), +// }), +// ), +// }, +// }, +// }, +// }) +// } func TestCheckTypeSetElemAttr(name, attr, value string) TestCheckFunc { return func(s *terraform.State) error { is, err := primaryInstanceState(s, name) diff --git a/helper/resource/testing_sets_test.go b/helper/resource/testing_sets_test.go index 0805a90b5..4d50e7ecc 100644 --- a/helper/resource/testing_sets_test.go +++ b/helper/resource/testing_sets_test.go @@ -402,7 +402,6 @@ func TestTestCheckTypeSetElemAttr(t *testing.T) { } for _, testCase := range testCases { - testCase := testCase t.Run(testCase.Description, func(t *testing.T) { t.Parallel() @@ -1010,7 +1009,6 @@ func TestTestCheckTypeSetElemAttrPair(t *testing.T) { } for _, testCase := range testCases { - testCase := testCase t.Run(testCase.Description, func(t *testing.T) { t.Parallel() @@ -1754,7 +1752,6 @@ func TestTestMatchTypeSetElemNestedAttrs(t *testing.T) { } for _, testCase := range testCases { - testCase := testCase t.Run(testCase.Description, func(t *testing.T) { t.Parallel() @@ -2593,7 +2590,6 @@ func TestTestCheckTypeSetElemNestedAttrs(t *testing.T) { } for _, testCase := range testCases { - testCase := testCase t.Run(testCase.Description, func(t *testing.T) { t.Parallel() diff --git a/helper/resource/testing_test.go b/helper/resource/testing_test.go index 221706bfb..cfd8afcff 100644 --- a/helper/resource/testing_test.go +++ b/helper/resource/testing_test.go @@ -13,10 +13,8 @@ import ( "strings" "testing" - "github.com/hashicorp/go-multierror" - testinginterface "github.com/mitchellh/go-testing-interface" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + testinginterface "github.com/mitchellh/go-testing-interface" "github.com/hashicorp/terraform-plugin-testing/terraform" ) @@ -36,7 +34,7 @@ func TestParallelTest(t *testing.T) { IsUnitTest: true, ProviderFactories: map[string]func() (*schema.Provider, error){ "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return nil, nil + return &schema.Provider{}, nil }, }, Steps: []TestStep{ @@ -54,29 +52,27 @@ func TestParallelTest(t *testing.T) { func TestComposeAggregateTestCheckFunc(t *testing.T) { t.Parallel() + err1 := errors.New("Error 1") check1 := func(s *terraform.State) error { - return errors.New("Error 1") + return err1 } + err2 := errors.New("Error 2") check2 := func(s *terraform.State) error { - return errors.New("Error 2") + return err2 } f := ComposeAggregateTestCheckFunc(check1, check2) err := f(nil) if err == nil { - t.Fatalf("Expected errors") + t.Fatal("expected error, got none") } - multi, ok := err.(*multierror.Error) - if !ok { - t.Fatalf("unexpected type %T for err", err) + if !errors.Is(err, err1) { + t.Errorf("expected %s, got: %s", err1, err) } - if !strings.Contains(multi.Errors[0].Error(), "Error 1") { - t.Fatalf("Expected Error 1, Got %s", multi.Errors[0]) - } - if !strings.Contains(multi.Errors[1].Error(), "Error 2") { - t.Fatalf("Expected Error 2, Got %s", multi.Errors[1]) + if !errors.Is(err, err2) { + t.Errorf("expected %s, got: %s", err2, err) } } @@ -181,7 +177,7 @@ func TestTest_Main(t *testing.T) { for _, tc := range cases { // reset sweepers sweeperFuncs = map[string]*Sweeper{} - tc := tc + t.Run(tc.Name, func(t *testing.T) { t.Parallel() @@ -393,7 +389,6 @@ func TestFilterSweepers(t *testing.T) { for _, tc := range cases { // reset sweepers sweeperFuncs = map[string]*Sweeper{} - tc := tc t.Run(tc.Name, func(t *testing.T) { t.Parallel() @@ -638,7 +633,6 @@ func TestFilterSweeperWithDependencies(t *testing.T) { for _, tc := range cases { // reset sweepers sweeperFuncs = map[string]*Sweeper{} - tc := tc t.Run(tc.Name, func(t *testing.T) { t.Parallel() @@ -817,7 +811,6 @@ func TestRunSweepers(t *testing.T) { for _, tc := range cases { // reset sweepers sweeperFuncs = map[string]*Sweeper{} - tc := tc t.Run(tc.Name, func(t *testing.T) { t.Parallel() @@ -867,7 +860,6 @@ func TestTestCheckResourceAttr(t *testing.T) { }{ "attribute not found": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -887,7 +879,6 @@ func TestTestCheckResourceAttr(t *testing.T) { }, "bool attribute match": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -908,7 +899,6 @@ func TestTestCheckResourceAttr(t *testing.T) { }, "bool attribute mismatch": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -930,7 +920,6 @@ func TestTestCheckResourceAttr(t *testing.T) { }, "float attribute match": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -951,7 +940,6 @@ func TestTestCheckResourceAttr(t *testing.T) { }, "float attribute mismatch": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -973,7 +961,6 @@ func TestTestCheckResourceAttr(t *testing.T) { }, "integer attribute match": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -994,7 +981,6 @@ func TestTestCheckResourceAttr(t *testing.T) { }, "integer attribute mismatch": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1016,7 +1002,6 @@ func TestTestCheckResourceAttr(t *testing.T) { }, "list attribute directly": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1039,7 +1024,6 @@ func TestTestCheckResourceAttr(t *testing.T) { }, "list attribute element count match": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1061,7 +1045,6 @@ func TestTestCheckResourceAttr(t *testing.T) { }, "list attribute element count mismatch": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1084,7 +1067,6 @@ func TestTestCheckResourceAttr(t *testing.T) { }, "list attribute element count match 0 when empty": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1106,7 +1088,6 @@ func TestTestCheckResourceAttr(t *testing.T) { // Special case with .# and value 0 "list attribute element count match 0 when missing": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1125,7 +1106,6 @@ func TestTestCheckResourceAttr(t *testing.T) { }, "list attribute element value match": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1147,7 +1127,6 @@ func TestTestCheckResourceAttr(t *testing.T) { }, "list attribute element value mismatch": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1170,7 +1149,6 @@ func TestTestCheckResourceAttr(t *testing.T) { }, "map attribute directly": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1193,7 +1171,6 @@ func TestTestCheckResourceAttr(t *testing.T) { }, "map attribute element count match": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1215,7 +1192,6 @@ func TestTestCheckResourceAttr(t *testing.T) { }, "map attribute element count mismatch": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1238,7 +1214,6 @@ func TestTestCheckResourceAttr(t *testing.T) { }, "map attribute element count match 0 when empty": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1260,7 +1235,6 @@ func TestTestCheckResourceAttr(t *testing.T) { // Special case with .% and value 0 "map attribute element count match 0 when missing": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1279,7 +1253,6 @@ func TestTestCheckResourceAttr(t *testing.T) { }, "map attribute element value match": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1301,7 +1274,6 @@ func TestTestCheckResourceAttr(t *testing.T) { }, "map attribute element value mismatch": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1324,7 +1296,6 @@ func TestTestCheckResourceAttr(t *testing.T) { }, "set attribute indexing error": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1347,7 +1318,6 @@ func TestTestCheckResourceAttr(t *testing.T) { }, "string attribute match": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1368,7 +1338,6 @@ func TestTestCheckResourceAttr(t *testing.T) { }, "string attribute mismatch": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1391,8 +1360,6 @@ func TestTestCheckResourceAttr(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -1426,7 +1393,6 @@ func TestTestCheckResourceAttrWith(t *testing.T) { }{ "attribute not found": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1446,7 +1412,6 @@ func TestTestCheckResourceAttrWith(t *testing.T) { }, "bool attribute match": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1467,7 +1432,6 @@ func TestTestCheckResourceAttrWith(t *testing.T) { }, "bool attribute mismatch": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1489,7 +1453,6 @@ func TestTestCheckResourceAttrWith(t *testing.T) { }, "list attribute directly": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1512,7 +1475,6 @@ func TestTestCheckResourceAttrWith(t *testing.T) { }, "list attribute element count match": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1534,7 +1496,6 @@ func TestTestCheckResourceAttrWith(t *testing.T) { }, "list attribute element count mismatch": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1557,7 +1518,6 @@ func TestTestCheckResourceAttrWith(t *testing.T) { }, "list attribute element count match 0 when empty": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1578,7 +1538,6 @@ func TestTestCheckResourceAttrWith(t *testing.T) { }, "list attribute element value match": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1600,7 +1559,6 @@ func TestTestCheckResourceAttrWith(t *testing.T) { }, "list attribute element value mismatch": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1623,7 +1581,6 @@ func TestTestCheckResourceAttrWith(t *testing.T) { }, "map attribute directly": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1646,7 +1603,6 @@ func TestTestCheckResourceAttrWith(t *testing.T) { }, "map attribute element count match": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1668,7 +1624,6 @@ func TestTestCheckResourceAttrWith(t *testing.T) { }, "map attribute element count mismatch": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1691,7 +1646,6 @@ func TestTestCheckResourceAttrWith(t *testing.T) { }, "map attribute element count match 0 when empty": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1712,7 +1666,6 @@ func TestTestCheckResourceAttrWith(t *testing.T) { }, "map attribute element value match": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1734,7 +1687,6 @@ func TestTestCheckResourceAttrWith(t *testing.T) { }, "map attribute element value mismatch": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1757,7 +1709,6 @@ func TestTestCheckResourceAttrWith(t *testing.T) { }, "set attribute indexing error": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1780,7 +1731,6 @@ func TestTestCheckResourceAttrWith(t *testing.T) { }, "string attribute match": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1801,7 +1751,6 @@ func TestTestCheckResourceAttrWith(t *testing.T) { }, "string attribute mismatch": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1824,8 +1773,6 @@ func TestTestCheckResourceAttrWith(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -1863,7 +1810,6 @@ func TestTestCheckNoResourceAttr(t *testing.T) { }{ "attribute not found": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1881,7 +1827,6 @@ func TestTestCheckNoResourceAttr(t *testing.T) { }, "attribute found": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1902,7 +1847,6 @@ func TestTestCheckNoResourceAttr(t *testing.T) { }, "list attribute directly": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1925,7 +1869,6 @@ func TestTestCheckNoResourceAttr(t *testing.T) { // Special case with .# and value 0 "list attribute element count match 0 when empty": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1945,7 +1888,6 @@ func TestTestCheckNoResourceAttr(t *testing.T) { }, "list attribute element count mismatch 0 when non-empty": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1967,7 +1909,6 @@ func TestTestCheckNoResourceAttr(t *testing.T) { }, "map attribute directly": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -1990,7 +1931,6 @@ func TestTestCheckNoResourceAttr(t *testing.T) { // Special case with .% and value 0 "map attribute element count match 0 when empty": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -2010,7 +1950,6 @@ func TestTestCheckNoResourceAttr(t *testing.T) { }, "map attribute element count mismatch 0 when non-empty": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -2032,7 +1971,6 @@ func TestTestCheckNoResourceAttr(t *testing.T) { }, "set attribute indexing error": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -2055,8 +1993,6 @@ func TestTestCheckNoResourceAttr(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -2350,8 +2286,6 @@ func TestTestCheckResourceAttrPair(t *testing.T) { } for name, test := range tests { - name, test := name, test - t.Run(name, func(t *testing.T) { t.Parallel() @@ -2385,7 +2319,6 @@ func TestTestCheckResourceAttrSet(t *testing.T) { }{ "attribute not found": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -2404,7 +2337,6 @@ func TestTestCheckResourceAttrSet(t *testing.T) { }, "attribute found": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -2424,7 +2356,6 @@ func TestTestCheckResourceAttrSet(t *testing.T) { }, "list attribute directly": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -2446,7 +2377,6 @@ func TestTestCheckResourceAttrSet(t *testing.T) { }, "map attribute directly": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -2468,7 +2398,6 @@ func TestTestCheckResourceAttrSet(t *testing.T) { }, "set attribute indexing error": { state: &terraform.State{ - IsBinaryDrivenTest: true, // Always true now Modules: []*terraform.ModuleState{ { Path: []string{"root"}, @@ -2491,8 +2420,6 @@ func TestTestCheckResourceAttrSet(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/helper/resource/teststep_providers.go b/helper/resource/teststep_providers.go index 9b759bde0..bd9f43b86 100644 --- a/helper/resource/teststep_providers.go +++ b/helper/resource/teststep_providers.go @@ -5,24 +5,17 @@ package resource import ( "context" + "encoding/json" "fmt" - "regexp" "strings" -) -var configProviderBlockRegex = regexp.MustCompile(`provider "?[a-zA-Z0-9_-]+"? {`) + "github.com/hashicorp/go-version" +) -// configHasProviderBlock returns true if the Config has declared a provider -// configuration block, e.g. provider "examplecloud" {...} -func (s TestStep) configHasProviderBlock(_ context.Context) bool { - return configProviderBlockRegex.MatchString(s.Config) -} - -// configHasTerraformBlock returns true if the Config has declared a terraform -// configuration block, e.g. terraform {...} -func (s TestStep) configHasTerraformBlock(_ context.Context) bool { - return strings.Contains(s.Config, "terraform {") -} +// tfBlockMinReqTFVersion is used to prevent errors arising from +// adding required providers to the terraform block when Terraform +// is any version prior to v1.0.0 +const tfBlockMinReqTFVersion = "1.0.0" // mergedConfig prepends any necessary terraform configuration blocks to the // TestStep Config. @@ -31,32 +24,49 @@ func (s TestStep) configHasTerraformBlock(_ context.Context) bool { // TestStep, the terraform configuration block should be included with the // step configuration to prevent errors with providers outside the // registry.terraform.io hostname or outside the hashicorp namespace. -func (s TestStep) mergedConfig(ctx context.Context, testCase TestCase) string { +// This is only necessary when using TestStep.Config. +// +// When TestStep.ConfigDirectory is used, the expectation is that the +// Terraform configuration files will specify a terraform configuration +// block and/or provider blocks as necessary. +func (s TestStep) mergedConfig(ctx context.Context, testCase TestCase, configHasTerraformBlock, configHasProviderBlock bool, tfVersion *version.Version) (string, error) { var config strings.Builder // Prevent issues with existing configurations containing the terraform // configuration block. - if s.configHasTerraformBlock(ctx) { + if configHasTerraformBlock { config.WriteString(s.Config) - return config.String() + return config.String(), nil } if testCase.hasProviders(ctx) { - config.WriteString(testCase.providerConfig(ctx, s.configHasProviderBlock(ctx))) + cfg, err := s.providerConfigTestCase(ctx, configHasProviderBlock, testCase, tfVersion) + + if err != nil { + return "", err + } + + config.WriteString(cfg) } else { - config.WriteString(s.providerConfig(ctx, s.configHasProviderBlock(ctx))) + cfg, err := s.providerConfig(ctx, configHasProviderBlock, tfVersion) + + if err != nil { + return "", err + } + + config.WriteString(cfg) } config.WriteString(s.Config) - return config.String() + return config.String(), nil } // providerConfig takes the list of providers in a TestStep and returns a // config with only empty provider blocks. This is useful for Import, where no // config is provided, but the providers must be defined. -func (s TestStep) providerConfig(_ context.Context, skipProviderBlock bool) string { +func (s TestStep) providerConfig(_ context.Context, skipProviderBlock bool, tfVersion *version.Version) (string, error) { var providerBlocks, requiredProviderBlocks strings.Builder for name, externalProvider := range s.ExternalProviders { @@ -81,6 +91,140 @@ func (s TestStep) providerConfig(_ context.Context, skipProviderBlock bool) stri requiredProviderBlocks.WriteString(" }\n") } + minReqVersion, err := version.NewVersion(tfBlockMinReqTFVersion) + + if err != nil { + return "", err + } + + for name := range s.ProviderFactories { + if tfVersion.LessThan(minReqVersion) { + break + } + + requiredProviderBlocks.WriteString(addTerraformBlockSource(name, s.Config)) + } + + for name := range s.ProtoV5ProviderFactories { + if tfVersion.LessThan(minReqVersion) { + break + } + + requiredProviderBlocks.WriteString(addTerraformBlockSource(name, s.Config)) + } + + for name := range s.ProtoV6ProviderFactories { + if tfVersion.LessThan(minReqVersion) { + break + } + + requiredProviderBlocks.WriteString(addTerraformBlockSource(name, s.Config)) + } + + if requiredProviderBlocks.Len() > 0 { + return fmt.Sprintf(` +terraform { + required_providers { +%[1]s + } +} + +%[2]s +`, strings.TrimSuffix(requiredProviderBlocks.String(), "\n"), providerBlocks.String()), nil + } + + return providerBlocks.String(), nil +} + +func (s TestStep) providerConfigTestCase(_ context.Context, skipProviderBlock bool, testCase TestCase, tfVersion *version.Version) (string, error) { + var providerBlocks, requiredProviderBlocks strings.Builder + + providerNames := make(map[string]struct{}, len(testCase.Providers)) + + for name := range testCase.Providers { + providerNames[name] = struct{}{} + } + + for name := range testCase.ProviderFactories { + delete(providerNames, name) + } + + // [BF] The Providers field handling predates the logic being moved to this + // method. It's not entirely clear to me at this time why this field + // is being used and not the others, but leaving it here just in case + // it does have a special purpose that wasn't being unit tested prior. + for name := range providerNames { + providerBlocks.WriteString(fmt.Sprintf("provider %q {}\n", name)) + + requiredProviderBlocks.WriteString(fmt.Sprintf(" %s = {\n", name)) + + requiredProviderBlocks.WriteString(" }\n") + } + + for name, externalProvider := range testCase.ExternalProviders { + if !skipProviderBlock { + providerBlocks.WriteString(fmt.Sprintf("provider %q {}\n", name)) + } + + if externalProvider.Source == "" && externalProvider.VersionConstraint == "" { + continue + } + + requiredProviderBlocks.WriteString(fmt.Sprintf(" %s = {\n", name)) + + if externalProvider.Source != "" { + requiredProviderBlocks.WriteString(fmt.Sprintf(" source = %q\n", externalProvider.Source)) + } + + if externalProvider.VersionConstraint != "" { + requiredProviderBlocks.WriteString(fmt.Sprintf(" version = %q\n", externalProvider.VersionConstraint)) + } + + requiredProviderBlocks.WriteString(" }\n") + } + + minReqVersion, err := version.NewVersion(tfBlockMinReqTFVersion) + + if err != nil { + return "", err + } + + for name := range testCase.ProviderFactories { + if tfVersion.LessThan(minReqVersion) { + break + } + + providerFactoryBlocks := addTerraformBlockSource(name, s.Config) + + if len(providerFactoryBlocks) > 0 { + requiredProviderBlocks.WriteString(providerFactoryBlocks) + } + } + + for name := range testCase.ProtoV5ProviderFactories { + if tfVersion.LessThan(minReqVersion) { + break + } + + protov5ProviderFactoryBlocks := addTerraformBlockSource(name, s.Config) + + if len(protov5ProviderFactoryBlocks) > 0 { + requiredProviderBlocks.WriteString(protov5ProviderFactoryBlocks) + } + } + + for name := range testCase.ProtoV6ProviderFactories { + if tfVersion.LessThan(minReqVersion) { + break + } + + protov6ProviderFactoryBlocks := addTerraformBlockSource(name, s.Config) + + if len(protov6ProviderFactoryBlocks) > 0 { + requiredProviderBlocks.WriteString(addTerraformBlockSource(name, s.Config)) + } + } + if requiredProviderBlocks.Len() > 0 { return fmt.Sprintf(` terraform { @@ -90,8 +234,25 @@ terraform { } %[2]s -`, strings.TrimSuffix(requiredProviderBlocks.String(), "\n"), providerBlocks.String()) +`, strings.TrimSuffix(requiredProviderBlocks.String(), "\n"), providerBlocks.String()), nil + } + + return providerBlocks.String(), nil +} + +func addTerraformBlockSource(name, config string) string { + var js json.RawMessage + + // Do not process JSON. + if err := json.Unmarshal([]byte(config), &js); err == nil { + return "" } + var providerBlocks strings.Builder + + providerBlocks.WriteString(fmt.Sprintf(" %s = {\n", name)) + providerBlocks.WriteString(fmt.Sprintf(" source = %q\n", getProviderAddr(name))) + providerBlocks.WriteString(" }\n") + return providerBlocks.String() } diff --git a/helper/resource/teststep_providers_test.go b/helper/resource/teststep_providers_test.go index 114395578..6ad3c2588 100644 --- a/helper/resource/teststep_providers_test.go +++ b/helper/resource/teststep_providers_test.go @@ -8,6 +8,7 @@ import ( "fmt" "os" "path/filepath" + "regexp" "strconv" "strings" "testing" @@ -15,132 +16,33 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-go/tfprotov6" - + "github.com/hashicorp/terraform-plugin-go/tftypes" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-testing/config" "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-plugin-testing/tfversion" ) -func TestStepConfigHasProviderBlock(t *testing.T) { - t.Parallel() - - testCases := map[string]struct { - testStep TestStep - expected bool - }{ - "no-config": { - testStep: TestStep{}, - expected: false, - }, - "provider-meta-attribute": { - testStep: TestStep{ - Config: ` -resource "test_test" "test" { - provider = test.test -} -`, - }, - expected: false, - }, - "provider-object-attribute": { - testStep: TestStep{ - Config: ` -resource "test_test" "test" { - test = { - provider = { - test = true - } - } -} -`, - }, - expected: false, - }, - "provider-string-attribute": { - testStep: TestStep{ - Config: ` -resource "test_test" "test" { - test = { - provider = "test" - } -} -`, - }, - expected: false, - }, - "provider-block-quoted-with-attributes": { - testStep: TestStep{ - Config: ` -provider "test" { - test = true -} - -resource "test_test" "test" {} -`, - }, - expected: true, - }, - "provider-block-unquoted-with-attributes": { - testStep: TestStep{ - Config: ` -provider test { - test = true -} - -resource "test_test" "test" {} -`, - }, - expected: true, - }, - "provider-block-quoted-without-attributes": { - testStep: TestStep{ - Config: ` -provider "test" {} - -resource "test_test" "test" {} -`, - }, - expected: true, - }, - "provider-block-unquoted-without-attributes": { - testStep: TestStep{ - Config: ` -provider test {} - -resource "test_test" "test" {} -`, - }, - expected: true, - }, - } - - for name, testCase := range testCases { - name, testCase := name, testCase - - t.Run(name, func(t *testing.T) { - t.Parallel() - - got := testCase.testStep.configHasProviderBlock(context.Background()) - - if testCase.expected != got { - t.Errorf("expected %t, got %t", testCase.expected, got) - } - }) - } -} - -func TestStepMergedConfig(t *testing.T) { +func TestStepMergedConfig_TF_0_15(t *testing.T) { t.Parallel() testCases := map[string]struct { - testCase TestCase - testStep TestStep - expected string + testCase TestCase + testStep TestStep + configHasTerraformBlock bool + configHasProviderBlock bool + expected string }{ "testcase-externalproviders-and-protov5providerfactories": { testCase: TestCase{ @@ -524,6 +426,7 @@ provider "test" {} resource "test_test" "test" {} `, }, + configHasProviderBlock: true, expected: ` terraform { required_providers { @@ -556,6 +459,7 @@ provider test {} resource "test_test" "test" {} `, }, + configHasProviderBlock: true, expected: ` terraform { required_providers { @@ -595,6 +499,7 @@ terraform { resource "test_test" "test" {} `, }, + configHasTerraformBlock: true, expected: ` terraform { required_providers { @@ -752,12 +657,14 @@ resource "test_test" "test" {} } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() - got := testCase.testStep.mergedConfig(context.Background(), testCase.testCase) + got, err := testCase.testStep.mergedConfig(context.Background(), testCase.testCase, testCase.configHasTerraformBlock, testCase.configHasProviderBlock, tfversion.Version0_15_0) + + if err != nil { + t.Errorf("cannot generate merged config: %s", err) + } if diff := cmp.Diff(strings.TrimSpace(got), strings.TrimSpace(testCase.expected)); diff != "" { t.Errorf("unexpected difference: %s", diff) @@ -766,16 +673,18 @@ resource "test_test" "test" {} } } -func TestStepProviderConfig(t *testing.T) { +func TestStepMergedConfig_TF_1_6(t *testing.T) { t.Parallel() testCases := map[string]struct { - testStep TestStep - skipProviderBlock bool - expected string + testCase TestCase + testStep TestStep + configHasTerraformBlock bool + configHasProviderBlock bool + expected string }{ - "externalproviders-and-protov5providerfactories": { - testStep: TestStep{ + "testcase-externalproviders-and-protov5providerfactories": { + testCase: TestCase{ ExternalProviders: map[string]ExternalProvider{ "externaltest": { Source: "registry.terraform.io/hashicorp/externaltest", @@ -786,6 +695,13 @@ func TestStepProviderConfig(t *testing.T) { "localtest": nil, }, }, + testStep: TestStep{ + Config: ` +resource "externaltest_test" "test" {} + +resource "localtest_test" "test" {} +`, + }, expected: ` terraform { required_providers { @@ -793,14 +709,22 @@ terraform { source = "registry.terraform.io/hashicorp/externaltest" version = "1.2.3" } + localtest = { + source = "registry.terraform.io/hashicorp/localtest" + } } } provider "externaltest" {} + + +resource "externaltest_test" "test" {} + +resource "localtest_test" "test" {} `, }, - "externalproviders-and-protov6providerfactories": { - testStep: TestStep{ + "testcase-externalproviders-and-protov6providerfactories": { + testCase: TestCase{ ExternalProviders: map[string]ExternalProvider{ "externaltest": { Source: "registry.terraform.io/hashicorp/externaltest", @@ -811,6 +735,13 @@ provider "externaltest" {} "localtest": nil, }, }, + testStep: TestStep{ + Config: ` +resource "externaltest_test" "test" {} + +resource "localtest_test" "test" {} +`, + }, expected: ` terraform { required_providers { @@ -818,14 +749,22 @@ terraform { source = "registry.terraform.io/hashicorp/externaltest" version = "1.2.3" } + localtest = { + source = "registry.terraform.io/hashicorp/localtest" + } } } provider "externaltest" {} + + +resource "externaltest_test" "test" {} + +resource "localtest_test" "test" {} `, }, - "externalproviders-and-providerfactories": { - testStep: TestStep{ + "testcase-externalproviders-and-providerfactories": { + testCase: TestCase{ ExternalProviders: map[string]ExternalProvider{ "externaltest": { Source: "registry.terraform.io/hashicorp/externaltest", @@ -836,6 +775,13 @@ provider "externaltest" {} "localtest": nil, }, }, + testStep: TestStep{ + Config: ` +resource "externaltest_test" "test" {} + +resource "localtest_test" "test" {} +`, + }, expected: ` terraform { required_providers { @@ -843,43 +789,39 @@ terraform { source = "registry.terraform.io/hashicorp/externaltest" version = "1.2.3" } + localtest = { + source = "registry.terraform.io/hashicorp/localtest" + } } } provider "externaltest" {} + + +resource "externaltest_test" "test" {} + +resource "localtest_test" "test" {} `, }, - "externalproviders-missing-source-and-versionconstraint": { - testStep: TestStep{ + "testcase-externalproviders-missing-source-and-versionconstraint": { + testCase: TestCase{ ExternalProviders: map[string]ExternalProvider{ "test": {}, }, }, - expected: `provider "test" {}`, - }, - "externalproviders-skip-provider-block": { testStep: TestStep{ - ExternalProviders: map[string]ExternalProvider{ - "test": { - Source: "registry.terraform.io/hashicorp/test", - VersionConstraint: "1.2.3", - }, - }, + Config: ` +resource "test_test" "test" {} +`, }, - skipProviderBlock: true, expected: ` -terraform { - required_providers { - test = { - source = "registry.terraform.io/hashicorp/test" - version = "1.2.3" - } - } -} +provider "test" {} + +resource "test_test" "test" {} `, }, - "externalproviders-source-and-versionconstraint": { - testStep: TestStep{ + "testcase-externalproviders-source-and-versionconstraint": { + testCase: TestCase{ ExternalProviders: map[string]ExternalProvider{ "test": { Source: "registry.terraform.io/hashicorp/test", @@ -887,6 +829,11 @@ terraform { }, }, }, + testStep: TestStep{ + Config: ` +resource "test_test" "test" {} +`, + }, expected: ` terraform { required_providers { @@ -898,16 +845,24 @@ terraform { } provider "test" {} + + +resource "test_test" "test" {} `, }, - "externalproviders-source": { - testStep: TestStep{ + "testcase-externalproviders-source": { + testCase: TestCase{ ExternalProviders: map[string]ExternalProvider{ "test": { Source: "registry.terraform.io/hashicorp/test", }, }, }, + testStep: TestStep{ + Config: ` +resource "test_test" "test" {} +`, + }, expected: ` terraform { required_providers { @@ -918,16 +873,24 @@ terraform { } provider "test" {} + + +resource "test_test" "test" {} `, }, - "externalproviders-versionconstraint": { - testStep: TestStep{ + "testcase-externalproviders-versionconstraint": { + testCase: TestCase{ ExternalProviders: map[string]ExternalProvider{ "test": { VersionConstraint: "1.2.3", }, }, }, + testStep: TestStep{ + Config: ` +resource "test_test" "test" {} +`, + }, expected: ` terraform { required_providers { @@ -938,635 +901,2990 @@ terraform { } provider "test" {} + + +resource "test_test" "test" {} `, }, - "protov5providerfactories": { - testStep: TestStep{ + "testcase-protov5providerfactories": { + testCase: TestCase{ ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ "test": nil, }, }, - expected: ``, - }, - "protov6providerfactories": { testStep: TestStep{ + Config: ` +resource "test_test" "test" {} +`, + }, + expected: ` +terraform { + required_providers { + test = { + source = "registry.terraform.io/hashicorp/test" + } + } +} + + + +resource "test_test" "test" {} +`, + }, + "testcase-protov6providerfactories": { + testCase: TestCase{ ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "test": nil, }, }, - expected: ``, - }, - "providerfactories": { testStep: TestStep{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": nil, - }, + Config: ` +resource "test_test" "test" {} +`, }, - expected: ``, - }, - } - - for name, testCase := range testCases { - name, testCase := name, testCase - - t.Run(name, func(t *testing.T) { - t.Parallel() - - got := testCase.testStep.providerConfig(context.Background(), testCase.skipProviderBlock) - - if diff := cmp.Diff(strings.TrimSpace(got), strings.TrimSpace(testCase.expected)); diff != "" { - t.Errorf("unexpected difference: %s", diff) - } - }) - } + expected: ` +terraform { + required_providers { + test = { + source = "registry.terraform.io/hashicorp/test" + } + } } -func TestTest_TestStep_ExternalProviders(t *testing.T) { - t.Parallel() - Test(t, TestCase{ - Steps: []TestStep{ + +resource "test_test" "test" {} +`, + }, + "testcase-providerfactories": { + testCase: TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": nil, + }, + }, + testStep: TestStep{ + Config: ` +resource "test_test" "test" {} +`, + }, + expected: ` +terraform { + required_providers { + test = { + source = "registry.terraform.io/hashicorp/test" + } + } +} + + + +resource "test_test" "test" {} +`, + }, + "teststep-externalproviders-and-protov5providerfactories": { + testCase: TestCase{}, + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "externaltest": { + Source: "registry.terraform.io/hashicorp/externaltest", + VersionConstraint: "1.2.3", + }, + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "localtest": nil, + }, + Config: ` +resource "externaltest_test" "test" {} + +resource "localtest_test" "test" {} +`, + }, + expected: ` +terraform { + required_providers { + externaltest = { + source = "registry.terraform.io/hashicorp/externaltest" + version = "1.2.3" + } + localtest = { + source = "registry.terraform.io/hashicorp/localtest" + } + } +} + +provider "externaltest" {} + + +resource "externaltest_test" "test" {} + +resource "localtest_test" "test" {} +`, + }, + "teststep-externalproviders-and-protov6providerfactories": { + testCase: TestCase{}, + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "externaltest": { + Source: "registry.terraform.io/hashicorp/externaltest", + VersionConstraint: "1.2.3", + }, + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "localtest": nil, + }, + Config: ` +resource "externaltest_test" "test" {} + +resource "localtest_test" "test" {} +`, + }, + expected: ` +terraform { + required_providers { + externaltest = { + source = "registry.terraform.io/hashicorp/externaltest" + version = "1.2.3" + } + localtest = { + source = "registry.terraform.io/hashicorp/localtest" + } + } +} + +provider "externaltest" {} + + +resource "externaltest_test" "test" {} + +resource "localtest_test" "test" {} +`, + }, + "teststep-externalproviders-and-providerfactories": { + testCase: TestCase{}, + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "externaltest": { + Source: "registry.terraform.io/hashicorp/externaltest", + VersionConstraint: "1.2.3", + }, + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "localtest": nil, + }, + Config: ` +resource "externaltest_test" "test" {} + +resource "localtest_test" "test" {} +`, + }, + expected: ` +terraform { + required_providers { + externaltest = { + source = "registry.terraform.io/hashicorp/externaltest" + version = "1.2.3" + } + localtest = { + source = "registry.terraform.io/hashicorp/localtest" + } + } +} + +provider "externaltest" {} + + +resource "externaltest_test" "test" {} + +resource "localtest_test" "test" {} +`, + }, + "teststep-externalproviders-config-with-provider-block-quoted": { + testCase: TestCase{}, + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "test": { + Source: "registry.terraform.io/hashicorp/test", + VersionConstraint: "1.2.3", + }, + }, + Config: ` +provider "test" {} + +resource "test_test" "test" {} +`, + }, + configHasProviderBlock: true, + expected: ` +terraform { + required_providers { + test = { + source = "registry.terraform.io/hashicorp/test" + version = "1.2.3" + } + } +} + + + +provider "test" {} + +resource "test_test" "test" {} +`, + }, + "teststep-externalproviders-config-with-provider-block-unquoted": { + testCase: TestCase{}, + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "test": { + Source: "registry.terraform.io/hashicorp/test", + VersionConstraint: "1.2.3", + }, + }, + Config: ` +provider test {} + +resource "test_test" "test" {} +`, + }, + configHasProviderBlock: true, + expected: ` +terraform { + required_providers { + test = { + source = "registry.terraform.io/hashicorp/test" + version = "1.2.3" + } + } +} + + + +provider test {} + +resource "test_test" "test" {} +`, + }, + "teststep-externalproviders-config-with-terraform-block": { + testCase: TestCase{}, + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "test": { + Source: "registry.terraform.io/hashicorp/test", + VersionConstraint: "1.2.3", + }, + }, + Config: ` +terraform { + required_providers { + test = { + source = "registry.terraform.io/hashicorp/test" + version = "1.2.3" + } + } +} + +resource "test_test" "test" {} +`, + }, + configHasTerraformBlock: true, + expected: ` +terraform { + required_providers { + test = { + source = "registry.terraform.io/hashicorp/test" + version = "1.2.3" + } + } +} + +resource "test_test" "test" {} +`, + }, + "teststep-externalproviders-missing-source-and-versionconstraint": { + testCase: TestCase{}, + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "test": {}, + }, + Config: ` +resource "test_test" "test" {} +`, + }, + expected: ` +provider "test" {} + +resource "test_test" "test" {} +`, + }, + "teststep-externalproviders-source-and-versionconstraint": { + testCase: TestCase{}, + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "test": { + Source: "registry.terraform.io/hashicorp/test", + VersionConstraint: "1.2.3", + }, + }, + Config: ` +resource "test_test" "test" {} +`, + }, + expected: ` +terraform { + required_providers { + test = { + source = "registry.terraform.io/hashicorp/test" + version = "1.2.3" + } + } +} + +provider "test" {} + + +resource "test_test" "test" {} +`, + }, + "teststep-externalproviders-source": { + testCase: TestCase{}, + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "test": { + Source: "registry.terraform.io/hashicorp/test", + }, + }, + Config: ` +resource "test_test" "test" {} +`, + }, + expected: ` +terraform { + required_providers { + test = { + source = "registry.terraform.io/hashicorp/test" + } + } +} + +provider "test" {} + + +resource "test_test" "test" {} +`, + }, + "teststep-externalproviders-versionconstraint": { + testCase: TestCase{}, + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "test": { + VersionConstraint: "1.2.3", + }, + }, + Config: ` +resource "test_test" "test" {} +`, + }, + expected: ` +terraform { + required_providers { + test = { + version = "1.2.3" + } + } +} + +provider "test" {} + + +resource "test_test" "test" {} +`, + }, + "teststep-protov5providerfactories": { + testCase: TestCase{}, + testStep: TestStep{ + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "test": nil, + }, + Config: ` +resource "test_test" "test" {} +`, + }, + expected: ` +terraform { + required_providers { + test = { + source = "registry.terraform.io/hashicorp/test" + } + } +} + + + +resource "test_test" "test" {} +`, + }, + "teststep-protov6providerfactories": { + testCase: TestCase{}, + testStep: TestStep{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": nil, + }, + Config: ` +resource "test_test" "test" {} +`, + }, + expected: ` +terraform { + required_providers { + test = { + source = "registry.terraform.io/hashicorp/test" + } + } +} + + + +resource "test_test" "test" {} +`, + }, + "teststep-providerfactories": { + testCase: TestCase{}, + testStep: TestStep{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": nil, + }, + Config: ` +resource "test_test" "test" {} +`, + }, + expected: ` +terraform { + required_providers { + test = { + source = "registry.terraform.io/hashicorp/test" + } + } +} + + + +resource "test_test" "test" {} +`, + }, + } + + v, err := version.NewVersion("1.6.0") + + if err != nil { + t.Errorf("error generating version: %s", err) + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.testStep.mergedConfig(context.Background(), testCase.testCase, testCase.configHasTerraformBlock, testCase.configHasProviderBlock, v) + + if err != nil { + t.Errorf("cannot generate merged config: %s", err) + } + + if diff := cmp.Diff(strings.TrimSpace(got), strings.TrimSpace(testCase.expected)); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStepProviderConfig_TF_0_15(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + testStep TestStep + skipProviderBlock bool + expected string + }{ + "externalproviders-and-protov5providerfactories": { + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "externaltest": { + Source: "registry.terraform.io/hashicorp/externaltest", + VersionConstraint: "1.2.3", + }, + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "localtest": nil, + }, + }, + expected: ` +terraform { + required_providers { + externaltest = { + source = "registry.terraform.io/hashicorp/externaltest" + version = "1.2.3" + } + } +} + +provider "externaltest" {} +`, + }, + "externalproviders-and-protov6providerfactories": { + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "externaltest": { + Source: "registry.terraform.io/hashicorp/externaltest", + VersionConstraint: "1.2.3", + }, + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "localtest": nil, + }, + }, + expected: ` +terraform { + required_providers { + externaltest = { + source = "registry.terraform.io/hashicorp/externaltest" + version = "1.2.3" + } + } +} + +provider "externaltest" {} +`, + }, + "externalproviders-and-providerfactories": { + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "externaltest": { + Source: "registry.terraform.io/hashicorp/externaltest", + VersionConstraint: "1.2.3", + }, + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "localtest": nil, + }, + }, + expected: ` +terraform { + required_providers { + externaltest = { + source = "registry.terraform.io/hashicorp/externaltest" + version = "1.2.3" + } + } +} + +provider "externaltest" {} +`, + }, + "externalproviders-missing-source-and-versionconstraint": { + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "test": {}, + }, + }, + expected: `provider "test" {}`, + }, + "externalproviders-skip-provider-block": { + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "test": { + Source: "registry.terraform.io/hashicorp/test", + VersionConstraint: "1.2.3", + }, + }, + }, + skipProviderBlock: true, + expected: ` +terraform { + required_providers { + test = { + source = "registry.terraform.io/hashicorp/test" + version = "1.2.3" + } + } +} +`, + }, + "externalproviders-source-and-versionconstraint": { + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "test": { + Source: "registry.terraform.io/hashicorp/test", + VersionConstraint: "1.2.3", + }, + }, + }, + expected: ` +terraform { + required_providers { + test = { + source = "registry.terraform.io/hashicorp/test" + version = "1.2.3" + } + } +} + +provider "test" {} +`, + }, + "externalproviders-source": { + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "test": { + Source: "registry.terraform.io/hashicorp/test", + }, + }, + }, + expected: ` +terraform { + required_providers { + test = { + source = "registry.terraform.io/hashicorp/test" + } + } +} + +provider "test" {} +`, + }, + "externalproviders-versionconstraint": { + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "test": { + VersionConstraint: "1.2.3", + }, + }, + }, + expected: ` +terraform { + required_providers { + test = { + version = "1.2.3" + } + } +} + +provider "test" {} +`, + }, + "protov5providerfactories": { + testStep: TestStep{ + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "test": nil, + }, + }, + expected: ``, + }, + "protov6providerfactories": { + testStep: TestStep{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": nil, + }, + }, + expected: ``, + }, + "providerfactories": { + testStep: TestStep{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": nil, + }, + }, + expected: ``, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.testStep.providerConfig(context.Background(), testCase.skipProviderBlock, tfversion.Version0_15_0) + + if err != nil { + t.Errorf("cannot generate provider config: %s", err) + } + + if diff := cmp.Diff(strings.TrimSpace(got), strings.TrimSpace(testCase.expected)); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStepProviderConfig_TF_1_6(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + testStep TestStep + skipProviderBlock bool + expected string + }{ + "externalproviders-and-protov5providerfactories": { + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "externaltest": { + Source: "registry.terraform.io/hashicorp/externaltest", + VersionConstraint: "1.2.3", + }, + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "localtest": nil, + }, + }, + expected: ` +terraform { + required_providers { + externaltest = { + source = "registry.terraform.io/hashicorp/externaltest" + version = "1.2.3" + } + localtest = { + source = "registry.terraform.io/hashicorp/localtest" + } + } +} + +provider "externaltest" {} +`, + }, + "externalproviders-and-protov6providerfactories": { + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "externaltest": { + Source: "registry.terraform.io/hashicorp/externaltest", + VersionConstraint: "1.2.3", + }, + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "localtest": nil, + }, + }, + expected: ` +terraform { + required_providers { + externaltest = { + source = "registry.terraform.io/hashicorp/externaltest" + version = "1.2.3" + } + localtest = { + source = "registry.terraform.io/hashicorp/localtest" + } + } +} + +provider "externaltest" {} +`, + }, + "externalproviders-and-providerfactories": { + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "externaltest": { + Source: "registry.terraform.io/hashicorp/externaltest", + VersionConstraint: "1.2.3", + }, + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "localtest": nil, + }, + }, + expected: ` +terraform { + required_providers { + externaltest = { + source = "registry.terraform.io/hashicorp/externaltest" + version = "1.2.3" + } + localtest = { + source = "registry.terraform.io/hashicorp/localtest" + } + } +} + +provider "externaltest" {} +`, + }, + "externalproviders-missing-source-and-versionconstraint": { + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "test": {}, + }, + }, + expected: `provider "test" {}`, + }, + "externalproviders-skip-provider-block": { + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "test": { + Source: "registry.terraform.io/hashicorp/test", + VersionConstraint: "1.2.3", + }, + }, + }, + skipProviderBlock: true, + expected: ` +terraform { + required_providers { + test = { + source = "registry.terraform.io/hashicorp/test" + version = "1.2.3" + } + } +} +`, + }, + "externalproviders-source-and-versionconstraint": { + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "test": { + Source: "registry.terraform.io/hashicorp/test", + VersionConstraint: "1.2.3", + }, + }, + }, + expected: ` +terraform { + required_providers { + test = { + source = "registry.terraform.io/hashicorp/test" + version = "1.2.3" + } + } +} + +provider "test" {} +`, + }, + "externalproviders-source": { + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "test": { + Source: "registry.terraform.io/hashicorp/test", + }, + }, + }, + expected: ` +terraform { + required_providers { + test = { + source = "registry.terraform.io/hashicorp/test" + } + } +} + +provider "test" {} +`, + }, + "externalproviders-versionconstraint": { + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "test": { + VersionConstraint: "1.2.3", + }, + }, + }, + expected: ` +terraform { + required_providers { + test = { + version = "1.2.3" + } + } +} + +provider "test" {} +`, + }, + "protov5providerfactories": { + testStep: TestStep{ + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "test": nil, + }, + }, + expected: ` +terraform { + required_providers { + test = { + source = "registry.terraform.io/hashicorp/test" + } + } +}`, + }, + "protov6providerfactories": { + testStep: TestStep{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": nil, + }, + }, + expected: ` +terraform { + required_providers { + test = { + source = "registry.terraform.io/hashicorp/test" + } + } +}`, + }, + "providerfactories": { + testStep: TestStep{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": nil, + }, + }, + expected: ` +terraform { + required_providers { + test = { + source = "registry.terraform.io/hashicorp/test" + } + } +}`, + }, + } + + v, err := version.NewVersion("1.6.0") + + if err != nil { + t.Errorf("error generating version: %s", err) + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.testStep.providerConfig(context.Background(), testCase.skipProviderBlock, v) + + if err != nil { + t.Errorf("cannot generate provider config: %s", err) + } + + if diff := cmp.Diff(strings.TrimSpace(got), strings.TrimSpace(testCase.expected)); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestTest_TestStep_ExternalProviders(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ + { + Config: "# not empty", + ExternalProviders: map[string]ExternalProvider{ + "null": { + Source: "registry.terraform.io/hashicorp/null", + }, + }, + }, + }, + }) +} + +func TestTest_TestStep_ExternalProviders_DifferentProviders(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ + { + Config: `resource "null_resource" "test" {}`, + ExternalProviders: map[string]ExternalProvider{ + "null": { + Source: "registry.terraform.io/hashicorp/null", + }, + }, + }, + { + Config: `resource "random_pet" "test" {}`, + ExternalProviders: map[string]ExternalProvider{ + "random": { + Source: "registry.terraform.io/hashicorp/random", + }, + }, + }, + }, + }) +} + +func TestTest_TestStep_ExternalProviders_DifferentVersions(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ + { + Config: `resource "null_resource" "test" {}`, + ExternalProviders: map[string]ExternalProvider{ + "null": { + Source: "registry.terraform.io/hashicorp/null", + VersionConstraint: "3.1.0", + }, + }, + }, + { + Config: `resource "null_resource" "test" {}`, + ExternalProviders: map[string]ExternalProvider{ + "null": { + Source: "registry.terraform.io/hashicorp/null", + VersionConstraint: "3.1.1", + }, + }, + }, + }, + }) +} + +func TestTest_TestStep_ExternalProviders_Error(t *testing.T) { + t.Parallel() + + plugintest.TestExpectTFatal(t, func() { + Test(&mockT{}, TestCase{ + Steps: []TestStep{ + { + Config: "# not empty", + ExternalProviders: map[string]ExternalProvider{ + "testnonexistent": { + Source: "registry.terraform.io/hashicorp/testnonexistent", + }, + }, + }, + }, + }) + }) +} + +func TestTest_TestStep_ExternalProviders_NonHashiCorpNamespace(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ExternalProvider.Source is protocol version 6 + }, + Steps: []TestStep{ + { + ExternalProviders: map[string]ExternalProvider{ + // This can be set to any provider outside the hashicorp namespace. + // bflad/scaffoldingtest happens to be a published version of + // terraform-provider-scaffolding-framework. + "scaffoldingtest": { + Source: "registry.terraform.io/bflad/scaffoldingtest", + VersionConstraint: "0.1.0", + }, + }, + Config: `resource "scaffoldingtest_example" "test" {}`, + }, + }, + }) +} + +func TestTest_TestStep_ExternalProvidersAndProviderFactories_NonHashiCorpNamespace(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ExternalProvider.Source is protocol version 6 + }, + Steps: []TestStep{ + { + ExternalProviders: map[string]ExternalProvider{ + // This can be set to any provider outside the hashicorp namespace. + // bflad/scaffoldingtest happens to be a published version of + // terraform-provider-scaffolding-framework. + "scaffoldingtest": { + Source: "registry.terraform.io/bflad/scaffoldingtest", + VersionConstraint: "0.1.0", + }, + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "null": func() (*schema.Provider, error) { //nolint:unparam // required signature + return &schema.Provider{ + ResourcesMap: map[string]*schema.Resource{ + "null_resource": { + CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + d.SetId("test") + return nil + }, + DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + Schema: map[string]*schema.Schema{ + "triggers": { + Elem: &schema.Schema{Type: schema.TypeString}, + ForceNew: true, + Optional: true, + Type: schema.TypeMap, + }, + }, + }, + }, + }, nil + }, + }, + Config: ` + resource "null_resource" "test" {} + resource "scaffoldingtest_example" "test" {} + `, + }, + }, + }) +} + +func TestTest_TestStep_ExternalProviders_To_ProtoV6ProviderFactories(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + }, + Steps: []TestStep{ + { + Config: `resource "null_resource" "test" {}`, + ExternalProviders: map[string]ExternalProvider{ + "null": { + Source: "registry.terraform.io/hashicorp/null", + VersionConstraint: "3.1.1", + }, + }, + }, + { + Config: `resource "null_resource" "test" {}`, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "null": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "null_resource": { + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + { + Name: "triggers", + Type: tftypes.Map{ElementType: tftypes.String}, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + }, + }, + }) +} + +func TestTest_TestStep_ExternalProviders_To_ProviderFactories(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ + { + Config: `resource "null_resource" "test" {}`, + ExternalProviders: map[string]ExternalProvider{ + "null": { + Source: "registry.terraform.io/hashicorp/null", + VersionConstraint: "3.1.1", + }, + }, + }, + { + Config: `resource "null_resource" "test" {}`, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "null": func() (*schema.Provider, error) { //nolint:unparam // required signature + return &schema.Provider{ + ResourcesMap: map[string]*schema.Resource{ + "null_resource": { + CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + d.SetId("test") + return nil + }, + DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + Schema: map[string]*schema.Schema{ + "triggers": { + Elem: &schema.Schema{Type: schema.TypeString}, + ForceNew: true, + Optional: true, + Type: schema.TypeMap, + }, + }, + }, + }, + }, nil + }, + }, + }, + }, + }) +} + +func TestTest_TestStep_ExternalProviders_To_ProviderFactories_StateUpgraders(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ + { + Config: `resource "null_resource" "test" {}`, + ExternalProviders: map[string]ExternalProvider{ + "null": { + Source: "registry.terraform.io/hashicorp/null", + VersionConstraint: "3.1.1", + }, + }, + }, + { + Check: TestCheckResourceAttr("null_resource.test", "id", "test-schema-version-1"), + Config: `resource "null_resource" "test" {}`, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "null": func() (*schema.Provider, error) { //nolint:unparam // required signature + return &schema.Provider{ + ResourcesMap: map[string]*schema.Resource{ + "null_resource": { + CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + d.SetId("test") + return nil + }, + DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + Schema: map[string]*schema.Schema{ + "triggers": { + Elem: &schema.Schema{Type: schema.TypeString}, + ForceNew: true, + Optional: true, + Type: schema.TypeMap, + }, + }, + SchemaVersion: 1, // null 3.1.3 is version 0 + StateUpgraders: []schema.StateUpgrader{ + { + Type: cty.Object(map[string]cty.Type{ + "id": cty.String, + "triggers": cty.Map(cty.String), + }), + Upgrade: func(ctx context.Context, rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { + // null 3.1.3 sets the id attribute to a stringified random integer. + // Double check that our resource wasn't created by this TestStep. + id, ok := rawState["id"].(string) + + if !ok || id == "test" { + return rawState, fmt.Errorf("unexpected rawState: %v", rawState) + } + + rawState["id"] = "test-schema-version-1" + + return rawState, nil + }, + Version: 0, + }, + }, + }, + }, + }, nil + }, + }, + }, + }, + }) +} + +func TestTest_TestStep_Taint(t *testing.T) { + t.Parallel() + + var idOne, idTwo string + + UnitTest(t, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + }, + Steps: []TestStep{ + { + Config: `resource "test_resource" "test" {}`, + Check: ComposeAggregateTestCheckFunc( + extractResourceAttr("test_resource.test", "id", &idOne), + ), + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-value1"), + }, + ), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + }, + { + Taint: []string{"test_resource.test"}, + Config: `resource "test_resource" "test" {}`, + Check: ComposeAggregateTestCheckFunc( + extractResourceAttr("test_resource.test", "id", &idTwo), + ), + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-value2"), + }, + ), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + }, + }, + }) + + if idOne == idTwo { + t.Errorf("taint is not causing destroy-create cycle, idOne == idTwo: %s == %s", idOne, idTwo) + } +} + +//nolint:unparam +func extractResourceAttr(resourceName string, attributeName string, attributeValue *string) TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + + if !ok { + return fmt.Errorf("resource name %s not found in state", resourceName) + } + + attrValue, ok := rs.Primary.Attributes[attributeName] + + if !ok { + return fmt.Errorf("attribute %s not found in resource %s state", attributeName, resourceName) + } + + *attributeValue = attrValue + + return nil + } +} + +func TestTest_TestStep_ProtoV5ProviderFactories(t *testing.T) { + t.Parallel() + + UnitTest(&mockT{}, TestCase{ + Steps: []TestStep{ + { + Config: "# not empty", + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "test": providerserver.NewProtov5ProviderServer(testprovider.Protov5Provider{}), + }, + }, + }, + }) +} + +func TestTest_TestStep_ProtoV5ProviderFactories_Error(t *testing.T) { + t.Parallel() + + plugintest.TestExpectTFatal(t, func() { + UnitTest(&mockT{}, TestCase{ + Steps: []TestStep{ + { + Config: "# not empty", + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "test": func() (tfprotov5.ProviderServer, error) { //nolint:unparam // required signature + return nil, fmt.Errorf("test") + }, + }, + }, + }, + }) + }) +} + +func TestTest_TestStep_ProtoV6ProviderFactories(t *testing.T) { + t.Parallel() + + UnitTest(&mockT{}, TestCase{ + Steps: []TestStep{ + { + Config: "# not empty", + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + }, + }, + }) +} + +func TestTest_TestStep_ProtoV6ProviderFactories_Error(t *testing.T) { + t.Parallel() + + plugintest.TestExpectTFatal(t, func() { + UnitTest(&mockT{}, TestCase{ + Steps: []TestStep{ + { + Config: "# not empty", + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature + return nil, fmt.Errorf("test") + }, + }, + }, + }, + }) + }) +} + +func TestTest_TestStep_ProtoV6ProviderFactories_To_ExternalProviders(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + }, + Steps: []TestStep{ + { + Config: `resource "null_resource" "test" {}`, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "null": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "null_resource": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "triggers": tftypes.Map{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test"), + "triggers": tftypes.NewValue(tftypes.Map{ElementType: tftypes.String}, nil), + }, + ), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + { + Name: "triggers", + Type: tftypes.Map{ElementType: tftypes.String}, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + }, + { + Config: `resource "null_resource" "test" {}`, + ExternalProviders: map[string]ExternalProvider{ + "null": { + Source: "registry.terraform.io/hashicorp/null", + }, + }, + }, + }, + }) +} + +func TestTest_TestStep_ProviderFactories(t *testing.T) { + t.Parallel() + + UnitTest(&mockT{}, TestCase{ + Steps: []TestStep{ + { + Config: "# not empty", + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return &schema.Provider{}, nil + }, + }, + }, + }, + }) +} + +func TestTest_TestStep_ProviderFactories_Error(t *testing.T) { + t.Parallel() + + plugintest.TestExpectTFatal(t, func() { + UnitTest(&mockT{}, TestCase{ + Steps: []TestStep{ + { + Config: "# not empty", + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return nil, fmt.Errorf("test") + }, + }, + }, + }, + }) + }) +} + +func TestTest_TestStep_ProviderFactories_To_ExternalProviders(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ + { + Config: `resource "null_resource" "test" {}`, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "null": func() (*schema.Provider, error) { //nolint:unparam // required signature + return &schema.Provider{ + ResourcesMap: map[string]*schema.Resource{ + "null_resource": { + CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + d.SetId("test") + return nil + }, + DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + Schema: map[string]*schema.Schema{ + "triggers": { + Elem: &schema.Schema{Type: schema.TypeString}, + ForceNew: true, + Optional: true, + Type: schema.TypeMap, + }, + }, + }, + }, + }, nil + }, + }, + }, + { + Config: `resource "null_resource" "test" {}`, + ExternalProviders: map[string]ExternalProvider{ + "null": { + Source: "registry.terraform.io/hashicorp/null", + }, + }, + }, + }, + }) +} + +func TestTest_TestStep_ProviderFactories_Import_Inline(t *testing.T) { + id := "none" + + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ + { + Config: `resource "random_password" "test" { length = 12 }`, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "random": func() (*schema.Provider, error) { //nolint:unparam // required signature + return &schema.Provider{ + ResourcesMap: map[string]*schema.Resource{ + "random_password": { + DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + Schema: map[string]*schema.Schema{ + "length": { + Required: true, + ForceNew: true, + Type: schema.TypeInt, + }, + "result": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + }, + + "id": { + Computed: true, + Type: schema.TypeString, + }, + }, + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + val := d.Id() + + d.SetId("none") + + err := d.Set("result", val) + if err != nil { + panic(err) + } + + err = d.Set("length", len(val)) + if err != nil { + panic(err) + } + + return []*schema.ResourceData{d}, nil + }, + }, + }, + }, + }, nil + }, + }, + ResourceName: "random_password.test", + ImportState: true, + ImportStateId: "Z=:cbrJE?Ltg", + ImportStatePersist: true, + ImportStateCheck: composeImportStateCheck( + testCheckResourceAttrInstanceState(&id, "result", "Z=:cbrJE?Ltg"), + testCheckResourceAttrInstanceState(&id, "length", "12"), + ), + }, + }, + }) +} + +func TestTest_TestStep_ProviderFactories_Import_Inline_WithPersistMatch(t *testing.T) { + var result1, result2 string + + t.Parallel() + + Test(t, TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "random": func() (*schema.Provider, error) { //nolint:unparam // required signature + return &schema.Provider{ + ResourcesMap: map[string]*schema.Resource{ + "random_password": { + DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + Schema: map[string]*schema.Schema{ + "length": { + Required: true, + ForceNew: true, + Type: schema.TypeInt, + }, + "result": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + }, + + "id": { + Computed: true, + Type: schema.TypeString, + }, + }, + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + val := d.Id() + + d.SetId("none") + + err := d.Set("result", val) + if err != nil { + panic(err) + } + + err = d.Set("length", len(val)) + if err != nil { + panic(err) + } + + return []*schema.ResourceData{d}, nil + }, + }, + }, + }, + }, nil + }, + }, + Steps: []TestStep{ + { + Config: `resource "random_password" "test" { length = 12 }`, + ResourceName: "random_password.test", + ImportState: true, + ImportStateId: "Z=:cbrJE?Ltg", + ImportStatePersist: true, + ImportStateCheck: composeImportStateCheck( + testExtractResourceAttrInstanceState("none", "result", &result1), + ), + }, + { + Config: `resource "random_password" "test" { length = 12 }`, + Check: ComposeTestCheckFunc( + testExtractResourceAttr("random_password.test", "result", &result2), + testCheckAttributeValuesEqual(&result1, &result2), + ), + }, + }, + }) +} + +func TestTest_TestStep_ProviderFactories_Import_Inline_WithoutPersist(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "random": func() (*schema.Provider, error) { //nolint:unparam // required signature + return &schema.Provider{ + ResourcesMap: map[string]*schema.Resource{ + "random_password": { + CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + d.SetId("none") + return nil + }, + DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + Schema: map[string]*schema.Schema{ + "length": { + Required: true, + ForceNew: true, + Type: schema.TypeInt, + }, + "result": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + }, + + "id": { + Computed: true, + Type: schema.TypeString, + }, + }, + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + val := d.Id() + + d.SetId("none") + + err := d.Set("result", val) + if err != nil { + panic(err) + } + + err = d.Set("length", len(val)) + if err != nil { + panic(err) + } + + return []*schema.ResourceData{d}, nil + }, + }, + }, + }, + }, nil + }, + }, + Steps: []TestStep{ + { + Config: `resource "random_password" "test" { length = 12 }`, + ResourceName: "random_password.test", + ImportState: true, + ImportStateId: "Z=:cbrJE?Ltg", + ImportStatePersist: false, + }, + { + Config: `resource "random_password" "test" { length = 12 }`, + Check: ComposeTestCheckFunc( + TestCheckNoResourceAttr("random_password.test", "result"), + ), + }, + }, + }) +} + +func TestTest_TestStep_ProviderFactories_Import_External(t *testing.T) { + id := "none" + + t.Parallel() + + Test(t, TestCase{ + ExternalProviders: map[string]ExternalProvider{ + "random": { + Source: "registry.terraform.io/hashicorp/random", + }, + }, + Steps: []TestStep{ + { + Config: `resource "random_password" "test" { length = 12 }`, + ResourceName: "random_password.test", + ImportState: true, + ImportStateId: "Z=:cbrJE?Ltg", + ImportStatePersist: true, + ImportStateCheck: composeImportStateCheck( + testCheckResourceAttrInstanceState(&id, "result", "Z=:cbrJE?Ltg"), + testCheckResourceAttrInstanceState(&id, "length", "12"), + ), + }, + }, + }) +} + +func TestTest_TestStep_ProviderFactories_Import_External_WithPersistMatch(t *testing.T) { + var result1, result2 string + + t.Parallel() + + Test(t, TestCase{ + ExternalProviders: map[string]ExternalProvider{ + "random": { + Source: "registry.terraform.io/hashicorp/random", + }, + }, + Steps: []TestStep{ + { + Config: `resource "random_password" "test" { length = 12 }`, + ResourceName: "random_password.test", + ImportState: true, + ImportStateId: "Z=:cbrJE?Ltg", + ImportStatePersist: true, + ImportStateCheck: composeImportStateCheck( + testExtractResourceAttrInstanceState("none", "result", &result1), + ), + }, + { + Config: `resource "random_password" "test" { length = 12 }`, + Check: ComposeTestCheckFunc( + testExtractResourceAttr("random_password.test", "result", &result2), + testCheckAttributeValuesEqual(&result1, &result2), + ), + }, + }, + }) +} + +//nolint:paralleltest // Can't use t.Parallel with t.Setenv +func TestTest_TestStep_ProviderFactories_Import_External_WithPersistMatch_WithPersistWorkingDir(t *testing.T) { + var result1, result2 string + + t.Setenv(plugintest.EnvTfAccPersistWorkingDir, "1") + workingDir := t.TempDir() + + testSteps := []TestStep{ + { + Config: `resource "random_password" "test" { length = 12 }`, + ResourceName: "random_password.test", + ImportState: true, + ImportStateId: "Z=:cbrJE?Ltg", + ImportStatePersist: true, + ImportStateCheck: composeImportStateCheck( + testExtractResourceAttrInstanceState("none", "result", &result1), + ), + }, + { + Config: `resource "random_password" "test" { length = 12 }`, + Check: ComposeTestCheckFunc( + testExtractResourceAttr("random_password.test", "result", &result2), + testCheckAttributeValuesEqual(&result1, &result2), + ), + }, + } + + Test(t, TestCase{ + ExternalProviders: map[string]ExternalProvider{ + "random": { + Source: "registry.terraform.io/hashicorp/random", + }, + }, + WorkingDir: workingDir, + Steps: testSteps, + }) + + for testStepIndex := range testSteps { + dir := filepath.Join(workingDir, fmt.Sprintf("step_%s", strconv.Itoa(testStepIndex+1))) + + dirEntries, err := os.ReadDir(dir) + if err != nil { + t.Errorf("cannot read dir: %s", dir) + } + + var workingDirName string + + // Relies upon convention of a directory being created that is prefixed "work". + for _, dirEntry := range dirEntries { + if strings.HasPrefix(dirEntry.Name(), "work") && dirEntry.IsDir() { + workingDirName = filepath.Join(dir, dirEntry.Name()) + break + } + } + + configPlanStateFiles := []string{ + "terraform_plugin_test.tf", + "terraform.tfstate", + "tfplan", + } + + for _, file := range configPlanStateFiles { + // Skip verifying plan for first test step as there is no plan file if the + // resource does not already exist. + if testStepIndex == 0 && file == "tfplan" { + break + } + _, err = os.Stat(filepath.Join(workingDirName, file)) + if err != nil { + t.Errorf("cannot stat %s in %s: %s", file, workingDirName, err) + } + } + } +} + +func TestTest_TestStep_ProviderFactories_Import_External_WithoutPersistNonMatch(t *testing.T) { + var result1, result2 string + + t.Parallel() + + Test(t, TestCase{ + ExternalProviders: map[string]ExternalProvider{ + "random": { + Source: "registry.terraform.io/hashicorp/random", + }, + }, + Steps: []TestStep{ + { + Config: `resource "random_password" "test" { length = 12 }`, + ResourceName: "random_password.test", + ImportState: true, + ImportStateId: "Z=:cbrJE?Ltg", + ImportStatePersist: false, + ImportStateCheck: composeImportStateCheck( + testExtractResourceAttrInstanceState("none", "result", &result1), + ), + }, + { + Config: `resource "random_password" "test" { length = 12 }`, + Check: ComposeTestCheckFunc( + testExtractResourceAttr("random_password.test", "result", &result2), + testCheckAttributeValuesDiffer(&result1, &result2), + ), + }, + }, + }) +} + +//nolint:paralleltest // Can't use t.Parallel with t.Setenv +func TestTest_TestStep_ProviderFactories_Import_External_WithoutPersistNonMatch_WithPersistWorkingDir(t *testing.T) { + var result1, result2 string + + t.Setenv(plugintest.EnvTfAccPersistWorkingDir, "1") + workingDir := t.TempDir() + + testSteps := []TestStep{ + { + Config: `resource "random_password" "test" { length = 12 }`, + ResourceName: "random_password.test", + ImportState: true, + ImportStateId: "Z=:cbrJE?Ltg", + ImportStatePersist: false, + ImportStateCheck: composeImportStateCheck( + testExtractResourceAttrInstanceState("none", "result", &result1), + ), + }, + { + Config: `resource "random_password" "test" { length = 12 }`, + Check: ComposeTestCheckFunc( + testExtractResourceAttr("random_password.test", "result", &result2), + testCheckAttributeValuesDiffer(&result1, &result2), + ), + }, + } + + Test(t, TestCase{ + ExternalProviders: map[string]ExternalProvider{ + "random": { + Source: "registry.terraform.io/hashicorp/random", + }, + }, + WorkingDir: workingDir, + Steps: testSteps, + }) + + for testStepIndex := range testSteps { + dir := filepath.Join(workingDir, fmt.Sprintf("step_%s", strconv.Itoa(testStepIndex+1))) + + dirEntries, err := os.ReadDir(dir) + if err != nil { + t.Errorf("cannot read dir: %s", dir) + } + + var workingDirName string + + // Relies upon convention of a directory being created that is prefixed "work". + for _, dirEntry := range dirEntries { + if strings.HasPrefix(dirEntry.Name(), "work") && dirEntry.IsDir() { + workingDirName = filepath.Join(dir, dirEntry.Name()) + break + } + } + + configPlanStateFiles := []string{ + "terraform_plugin_test.tf", + "terraform.tfstate", + "tfplan", + } + + for _, file := range configPlanStateFiles { + // Skip verifying state and plan for first test step as ImportStatePersist is + // false so the state is not persisted and there is no plan file if the + // resource does not already exist. + if testStepIndex == 0 && (file == "terraform.tfstate" || file == "tfplan") { + break + } + _, err = os.Stat(filepath.Join(workingDirName, file)) + if err != nil { + t.Errorf("cannot stat %s in %s: %s", file, workingDirName, err) + } + } + } +} + +func TestTest_TestStep_ProviderFactories_Refresh_Inline(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "random": func() (*schema.Provider, error) { //nolint:unparam // required signature + return &schema.Provider{ + ResourcesMap: map[string]*schema.Resource{ + "random_password": { + CreateContext: func(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { + d.SetId("id") + err := d.Set("min_special", 10) + if err != nil { + panic(err) + } + return nil + }, + DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + ReadContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + err := d.Set("min_special", 2) + if err != nil { + panic(err) + } + return nil + }, + Schema: map[string]*schema.Schema{ + "min_special": { + Computed: true, + Type: schema.TypeInt, + }, + + "id": { + Computed: true, + Type: schema.TypeString, + }, + }, + }, + }, + }, nil + }, + }, + Steps: []TestStep{ + { + Config: `resource "random_password" "test" { }`, + Check: TestCheckResourceAttr("random_password.test", "min_special", "10"), + }, { - Config: "# not empty", - ExternalProviders: map[string]ExternalProvider{ - "null": { - Source: "registry.terraform.io/hashicorp/null", + RefreshState: true, + Check: TestCheckResourceAttr("random_password.test", "min_special", "2"), + }, + { + Config: `resource "random_password" "test" { }`, + Check: TestCheckResourceAttr("random_password.test", "min_special", "2"), + }, + }, + }) +} + +//nolint:paralleltest // Can't use t.Parallel with t.Setenv +func TestTest_TestStep_ProviderFactories_CopyWorkingDir_EachTestStep(t *testing.T) { + t.Setenv(plugintest.EnvTfAccPersistWorkingDir, "1") + workingDir := t.TempDir() + + testSteps := []TestStep{ + { + Config: `resource "random_password" "test" { }`, + }, + { + Config: `resource "random_password" "test" { }`, + }, + } + + Test(t, TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "random": func() (*schema.Provider, error) { //nolint:unparam // required signature + return &schema.Provider{ + ResourcesMap: map[string]*schema.Resource{ + "random_password": { + CreateContext: func(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { + d.SetId("id") + return nil + }, + DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + ReadContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + Schema: map[string]*schema.Schema{ + "id": { + Computed: true, + Type: schema.TypeString, + }, + }, + }, + }, + }, nil + }, + }, + WorkingDir: workingDir, + Steps: testSteps, + }) + + for k := range testSteps { + dir := filepath.Join(workingDir, fmt.Sprintf("step_%s", strconv.Itoa(k+1))) + + _, err := os.ReadDir(dir) + if err != nil { + t.Fatalf("cannot read dir: %s", dir) + } + } +} + +func TestTest_TestStep_ProviderFactories_RefreshWithPlanModifier_Inline(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "random": func() (*schema.Provider, error) { //nolint:unparam // required signature + return &schema.Provider{ + ResourcesMap: map[string]*schema.Resource{ + "random_password": { + CustomizeDiff: customdiff.All( + func(ctx context.Context, d *schema.ResourceDiff, meta interface{}) error { + special, ok := d.Get("special").(bool) + if !ok { + return fmt.Errorf("unexpected type %T for 'special' key", d.Get("special")) + } + + if special == true { + err := d.SetNew("special", false) + if err != nil { + panic(err) + } + } + return nil + }, + ), + CreateContext: func(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { + d.SetId("id") + err := d.Set("special", false) + if err != nil { + panic(err) + } + return nil + }, + DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + ReadContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + t := getTimeForTest() + if t.After(time.Now().Add(time.Hour * 1)) { + err := d.Set("special", true) + if err != nil { + panic(err) + } + } + return nil + }, + Schema: map[string]*schema.Schema{ + "special": { + Computed: true, + Type: schema.TypeBool, + ForceNew: true, + }, + + "id": { + Computed: true, + Type: schema.TypeString, + }, + }, + }, + }, + }, nil + }, + }, + Steps: []TestStep{ + { + Config: `resource "random_password" "test" { }`, + Check: TestCheckResourceAttr("random_password.test", "special", "false"), + }, + { + PreConfig: setTimeForTest(time.Now().Add(time.Hour * 2)), + RefreshState: true, + ExpectNonEmptyPlan: true, + Check: TestCheckResourceAttr("random_password.test", "special", "true"), + }, + { + PreConfig: setTimeForTest(time.Now()), + Config: `resource "random_password" "test" { }`, + Check: TestCheckResourceAttr("random_password.test", "special", "false"), + }, + }, + }) +} + +func TestTest_TestStep_ProviderFactories_Import_Inline_With_Data_Source(t *testing.T) { + var id string + + t.Parallel() + + Test(t, TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "http": func() (*schema.Provider, error) { //nolint:unparam // required signature + return &schema.Provider{ + DataSourcesMap: map[string]*schema.Resource{ + "http": { + ReadContext: func(ctx context.Context, d *schema.ResourceData, i interface{}) (diags diag.Diagnostics) { + url, ok := d.Get("url").(string) + if !ok { + return diag.Errorf("unexpected type %T for 'url' key", d.Get("url")) + } + + responseHeaders := map[string]string{ + "headerOne": "one", + "headerTwo": "two", + "headerThree": "three", + "headerFour": "four", + } + if err := d.Set("response_headers", responseHeaders); err != nil { + return append(diags, diag.Errorf("Error setting HTTP response headers: %s", err)...) + } + + d.SetId(url) + + return diags + }, + Schema: map[string]*schema.Schema{ + "url": { + Type: schema.TypeString, + Required: true, + }, + "response_headers": { + Type: schema.TypeMap, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + }, + }, nil + }, + "random": func() (*schema.Provider, error) { //nolint:unparam // required signature + return &schema.Provider{ + ResourcesMap: map[string]*schema.Resource{ + "random_string": { + CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + d.SetId("none") + err := d.Set("length", 4) + if err != nil { + panic(err) + } + err = d.Set("result", "none") + if err != nil { + panic(err) + } + return nil + }, + DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + Schema: map[string]*schema.Schema{ + "length": { + Required: true, + ForceNew: true, + Type: schema.TypeInt, + }, + "result": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + }, + + "id": { + Computed: true, + Type: schema.TypeString, + }, + }, + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + val := d.Id() + + d.SetId(val) + + err := d.Set("result", val) + if err != nil { + panic(err) + } + + err = d.Set("length", len(val)) + if err != nil { + panic(err) + } + + return []*schema.ResourceData{d}, nil + }, + }, + }, }, - }, + }, nil + }, + }, + Steps: []TestStep{ + { + Config: `data "http" "example" { + url = "https://checkpoint-api.hashicorp.com/v1/check/terraform" + } + + resource "random_string" "example" { + length = length(data.http.example.response_headers) + }`, + Check: extractResourceAttr("random_string.example", "id", &id), + }, + { + Config: `data "http" "example" { + url = "https://checkpoint-api.hashicorp.com/v1/check/terraform" + } + + resource "random_string" "example" { + length = length(data.http.example.response_headers) + }`, + ResourceName: "random_string.example", + ImportState: true, + ImportStateCheck: composeImportStateCheck( + testCheckResourceAttrInstanceState(&id, "length", "4"), + ), + ImportStateVerify: true, }, }, }) } -func TestTest_TestStep_ExternalProviders_DifferentProviders(t *testing.T) { +func TestTest_TestStep_ProviderFactories_Import_External_With_Data_Source(t *testing.T) { + var id string + t.Parallel() Test(t, TestCase{ + ExternalProviders: map[string]ExternalProvider{ + "null": { + Source: "registry.terraform.io/hashicorp/null", + }, + "random": { + Source: "registry.terraform.io/hashicorp/random", + }, + }, Steps: []TestStep{ { - Config: `resource "null_resource" "test" {}`, - ExternalProviders: map[string]ExternalProvider{ - "null": { - Source: "registry.terraform.io/hashicorp/null", - }, - }, + Config: ` + data "null_data_source" "values" { + inputs = { + length = 12 + } + } + + resource "random_string" "example" { + length = data.null_data_source.values.outputs["length"] + } + `, + Check: extractResourceAttr("random_string.example", "id", &id), }, { - Config: `resource "random_pet" "test" {}`, - ExternalProviders: map[string]ExternalProvider{ - "random": { - Source: "registry.terraform.io/hashicorp/random", - }, - }, + ResourceName: "random_string.example", + ImportState: true, + ImportStateCheck: testCheckResourceAttrInstanceState(&id, "length", "12"), + ImportStateVerify: true, }, }, }) } -func TestTest_TestStep_ExternalProviders_DifferentVersions(t *testing.T) { +func TestTest_ConfigDirectory_StaticDirectory(t *testing.T) { t.Parallel() Test(t, TestCase{ Steps: []TestStep{ { - Config: `resource "null_resource" "test" {}`, - ExternalProviders: map[string]ExternalProvider{ - "null": { - Source: "registry.terraform.io/hashicorp/null", - VersionConstraint: "3.1.0", - }, - }, - }, - { - Config: `resource "null_resource" "test" {}`, - ExternalProviders: map[string]ExternalProvider{ - "null": { - Source: "registry.terraform.io/hashicorp/null", - VersionConstraint: "3.1.1", - }, - }, + ConfigDirectory: config.StaticDirectory(`testdata/fixtures/random_password_3.5.1`), + Check: TestCheckResourceAttrSet("random_password.test", "id"), }, }, }) } -func TestTest_TestStep_ExternalProviders_Error(t *testing.T) { +func TestTest_ConfigDirectory_StaticDirectory_Vars(t *testing.T) { t.Parallel() - plugintest.TestExpectTFatal(t, func() { - Test(&mockT{}, TestCase{ - Steps: []TestStep{ - { - Config: "# not empty", - ExternalProviders: map[string]ExternalProvider{ - "testnonexistent": { - Source: "registry.terraform.io/hashicorp/testnonexistent", - }, - }, + Test(t, TestCase{ + Steps: []TestStep{ + { + ConfigDirectory: config.StaticDirectory(`testdata/fixtures/random_password_3.5.1_vars`), + ConfigVariables: config.Variables{ + "length": config.IntegerVariable(8), + "numeric": config.BoolVariable(false), }, + Check: TestCheckResourceAttrSet("random_password.test", "id"), }, - }) + }, }) } -func TestTest_TestStep_ExternalProviders_NonHashiCorpNamespace(t *testing.T) { +func TestTest_ConfigDirectory_StaticDirectory_VarsMissing(t *testing.T) { t.Parallel() Test(t, TestCase{ Steps: []TestStep{ { - ExternalProviders: map[string]ExternalProvider{ - // This can be set to any provider outside the hashicorp namespace. - // bflad/scaffoldingtest happens to be a published version of - // terraform-provider-scaffolding-framework. - "scaffoldingtest": { - Source: "registry.terraform.io/bflad/scaffoldingtest", - VersionConstraint: "0.1.0", - }, - }, - Config: `resource "scaffoldingtest_example" "test" {}`, - }, + ConfigDirectory: config.StaticDirectory(`testdata/fixtures/random_password_3.5.1_vars`), + Check: TestCheckResourceAttrSet("random_password.test", "id"), + ExpectError: regexp.MustCompile(`.*Error: No value for required variable`)}, }, }) } -func TestTest_TestStep_ExternalProvidersAndProviderFactories_NonHashiCorpNamespace(t *testing.T) { +func TestTest_ConfigDirectory_TestNameDirectory(t *testing.T) { t.Parallel() Test(t, TestCase{ Steps: []TestStep{ { - ExternalProviders: map[string]ExternalProvider{ - // This can be set to any provider outside the hashicorp namespace. - // bflad/scaffoldingtest happens to be a published version of - // terraform-provider-scaffolding-framework. - "scaffoldingtest": { - Source: "registry.terraform.io/bflad/scaffoldingtest", - VersionConstraint: "0.1.0", - }, - }, - ProviderFactories: map[string]func() (*schema.Provider, error){ - "null": func() (*schema.Provider, error) { //nolint:unparam // required signature - return &schema.Provider{ - ResourcesMap: map[string]*schema.Resource{ - "null_resource": { - CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { - d.SetId("test") - return nil - }, - DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { - return nil - }, - ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { - return nil - }, - Schema: map[string]*schema.Schema{ - "triggers": { - Elem: &schema.Schema{Type: schema.TypeString}, - ForceNew: true, - Optional: true, - Type: schema.TypeMap, - }, - }, - }, - }, - }, nil - }, - }, - Config: ` - resource "null_resource" "test" {} - resource "scaffoldingtest_example" "test" {} - `, + ConfigDirectory: config.TestNameDirectory(), + Check: TestCheckResourceAttrSet("random_password.test", "id"), }, }, }) } -func TestTest_TestStep_ExternalProviders_To_ProviderFactories(t *testing.T) { +func TestTest_ConfigDirectory_TestNameDirectory_Vars(t *testing.T) { t.Parallel() Test(t, TestCase{ Steps: []TestStep{ { - Config: `resource "null_resource" "test" {}`, - ExternalProviders: map[string]ExternalProvider{ - "null": { - Source: "registry.terraform.io/hashicorp/null", - VersionConstraint: "3.1.1", - }, + ConfigDirectory: config.TestNameDirectory(), + ConfigVariables: config.Variables{ + "length": config.IntegerVariable(8), + "numeric": config.BoolVariable(false), }, + Check: TestCheckResourceAttrSet("random_password.test", "id"), }, + }, + }) +} + +func TestTest_ConfigDirectory_TestStepDirectory(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ { - Config: `resource "null_resource" "test" {}`, - ProviderFactories: map[string]func() (*schema.Provider, error){ - "null": func() (*schema.Provider, error) { //nolint:unparam // required signature - return &schema.Provider{ - ResourcesMap: map[string]*schema.Resource{ - "null_resource": { - CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { - d.SetId("test") - return nil - }, - DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { - return nil - }, - ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { - return nil - }, - Schema: map[string]*schema.Schema{ - "triggers": { - Elem: &schema.Schema{Type: schema.TypeString}, - ForceNew: true, - Optional: true, - Type: schema.TypeMap, - }, - }, - }, - }, - }, nil - }, - }, + ConfigDirectory: config.TestStepDirectory(), + Check: TestCheckResourceAttrSet("random_password.test", "id"), }, }, }) } -func TestTest_TestStep_ExternalProviders_To_ProviderFactories_StateUpgraders(t *testing.T) { +// TestTest_ConfigDirectory_TestStepDirectory_StepNotHardcoded uses a multistep test +// to prove that the test step number is not hardcoded and to show that the +// configuration files that are copied from the test step directory in test step 1 +// are removed prior to running test step 2. +func TestTest_ConfigDirectory_TestStepDirectory_StepNotHardcoded(t *testing.T) { t.Parallel() Test(t, TestCase{ Steps: []TestStep{ { - Config: `resource "null_resource" "test" {}`, - ExternalProviders: map[string]ExternalProvider{ - "null": { - Source: "registry.terraform.io/hashicorp/null", - VersionConstraint: "3.1.1", - }, - }, + ConfigDirectory: config.TestStepDirectory(), + ExpectError: regexp.MustCompile(`.*An argument named "numeric" is not expected here.`), }, { - Check: TestCheckResourceAttr("null_resource.test", "id", "test-schema-version-1"), - Config: `resource "null_resource" "test" {}`, - ProviderFactories: map[string]func() (*schema.Provider, error){ - "null": func() (*schema.Provider, error) { //nolint:unparam // required signature - return &schema.Provider{ - ResourcesMap: map[string]*schema.Resource{ - "null_resource": { - CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { - d.SetId("test") - return nil - }, - DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { - return nil - }, - ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { - return nil - }, - Schema: map[string]*schema.Schema{ - "triggers": { - Elem: &schema.Schema{Type: schema.TypeString}, - ForceNew: true, - Optional: true, - Type: schema.TypeMap, - }, - }, - SchemaVersion: 1, // null 3.1.3 is version 0 - StateUpgraders: []schema.StateUpgrader{ - { - Type: cty.Object(map[string]cty.Type{ - "id": cty.String, - "triggers": cty.Map(cty.String), - }), - Upgrade: func(ctx context.Context, rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { - // null 3.1.3 sets the id attribute to a stringified random integer. - // Double check that our resource wasn't created by this TestStep. - id, ok := rawState["id"].(string) - - if !ok || id == "test" { - return rawState, fmt.Errorf("unexpected rawState: %v", rawState) - } - - rawState["id"] = "test-schema-version-1" + ConfigDirectory: config.TestStepDirectory(), + Check: TestCheckResourceAttrPtr("random_password.test", "length", teststep.Pointer("9")), + }, + }, + }) +} - return rawState, nil - }, - Version: 0, - }, - }, - }, - }, - }, nil - }, +func TestTest_ConfigDirectory_TestStepDirectory_Vars(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ + { + ConfigDirectory: config.TestStepDirectory(), + ConfigVariables: config.Variables{ + "length": config.IntegerVariable(8), + "numeric": config.BoolVariable(false), }, + Check: TestCheckResourceAttrSet("random_password.test", "id"), }, }, }) } -func TestTest_TestStep_Taint(t *testing.T) { +func TestTest_ConfigDirectory_StaticDirectory_MultipleFiles(t *testing.T) { t.Parallel() - var idOne, idTwo string - Test(t, TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "random": func() (*schema.Provider, error) { //nolint:unparam // required signature - return &schema.Provider{ - ResourcesMap: map[string]*schema.Resource{ - "random_id": { - CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { - d.SetId(time.Now().String()) - return nil - }, - DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { - return nil - }, - ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { - return nil - }, - Schema: map[string]*schema.Schema{}, - }, - }, - }, nil + Steps: []TestStep{ + { + ConfigDirectory: config.StaticDirectory(`testdata/fixtures/random_password_3.5.1_multiple_files`), + Check: TestCheckResourceAttrSet("random_password.test", "id"), }, }, + }) +} + +func TestTest_ConfigDirectory_StaticDirectory_MultipleFiles_Vars(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ Steps: []TestStep{ { - Config: `resource "random_id" "test" {}`, - Check: ComposeAggregateTestCheckFunc( - extractResourceAttr("random_id.test", "id", &idOne), - ), + ConfigDirectory: config.StaticDirectory(`testdata/fixtures/random_password_3.5.1_multiple_files_vars`), + ConfigVariables: config.Variables{ + "length": config.IntegerVariable(8), + "numeric": config.BoolVariable(false), + }, + Check: TestCheckResourceAttrSet("random_password.test", "id"), }, + }, + }) +} + +func TestTest_ConfigDirectory_TestNameDirectory_MultipleFiles(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ { - Taint: []string{"random_id.test"}, - Config: `resource "random_id" "test" {}`, - Check: ComposeAggregateTestCheckFunc( - extractResourceAttr("random_id.test", "id", &idTwo), - ), + ConfigDirectory: config.TestNameDirectory(), + Check: TestCheckResourceAttrSet("random_password.test", "id"), }, }, }) - - if idOne == idTwo { - t.Errorf("taint is not causing destroy-create cycle, idOne == idTwo: %s == %s", idOne, idTwo) - } } -//nolint:unparam -func extractResourceAttr(resourceName string, attributeName string, attributeValue *string) TestCheckFunc { - return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[resourceName] +func TestTest_ConfigDirectory_TestNameDirectory_MultipleFiles_Vars(t *testing.T) { + t.Parallel() - if !ok { - return fmt.Errorf("resource name %s not found in state", resourceName) - } + Test(t, TestCase{ + Steps: []TestStep{ + { + ConfigDirectory: config.TestNameDirectory(), + ConfigVariables: config.Variables{ + "length": config.IntegerVariable(8), + "numeric": config.BoolVariable(false), + }, + Check: TestCheckResourceAttrSet("random_password.test", "id"), + }, + }, + }) +} - attrValue, ok := rs.Primary.Attributes[attributeName] +func TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles(t *testing.T) { + t.Parallel() - if !ok { - return fmt.Errorf("attribute %s not found in resource %s state", attributeName, resourceName) - } + Test(t, TestCase{ + Steps: []TestStep{ + { + ConfigDirectory: config.TestStepDirectory(), + Check: TestCheckResourceAttrSet("random_password.test", "id"), + }, + }, + }) +} - *attributeValue = attrValue +// TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_StepNotHardcoded uses a +// multistep test to prove that the test step number is not hardcoded, and to show +// that the configuration files that are copied from the test step directory in test +// step 1 are removed prior to running test step 2. +func TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_StepNotHardcoded(t *testing.T) { + t.Parallel() - return nil - } + Test(t, TestCase{ + Steps: []TestStep{ + { + ConfigDirectory: config.TestStepDirectory(), + ExpectError: regexp.MustCompile(`.*An argument named "numeric" is not expected here.`), + }, + { + ConfigDirectory: config.TestStepDirectory(), + Check: TestCheckResourceAttrPtr("random_password.test", "length", teststep.Pointer("9")), + }, + }, + }) } -func TestTest_TestStep_ProtoV5ProviderFactories(t *testing.T) { +func TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars(t *testing.T) { t.Parallel() - Test(&mockT{}, TestCase{ + Test(t, TestCase{ Steps: []TestStep{ { - Config: "# not empty", - ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ - "test": func() (tfprotov5.ProviderServer, error) { //nolint:unparam // required signature - return nil, nil - }, + ConfigDirectory: config.TestStepDirectory(), + ConfigVariables: config.Variables{ + "length": config.IntegerVariable(8), + "numeric": config.BoolVariable(false), }, + Check: TestCheckResourceAttrSet("random_password.test", "id"), }, }, }) } -func TestTest_TestStep_ProtoV5ProviderFactories_Error(t *testing.T) { +// TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars_StepNotHardcoded uses a +// multistep test to prove that the test step number is not hardcoded, and to show +// that the configuration files that are copied from the test step directory in test +// step 1 are removed prior to running test step 2. +func TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars_StepNotHardcoded(t *testing.T) { t.Parallel() - plugintest.TestExpectTFatal(t, func() { - Test(&mockT{}, TestCase{ - Steps: []TestStep{ - { - Config: "# not empty", - ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ - "test": func() (tfprotov5.ProviderServer, error) { //nolint:unparam // required signature - return nil, fmt.Errorf("test") - }, - }, + Test(t, TestCase{ + Steps: []TestStep{ + { + ConfigDirectory: config.TestStepDirectory(), + ConfigVariables: config.Variables{ + "length": config.IntegerVariable(8), + "numeric": config.BoolVariable(false), }, + ExpectError: regexp.MustCompile(`.*An argument named "numeric" is not expected here.`), }, - }) + { + ConfigDirectory: config.TestStepDirectory(), + ConfigVariables: config.Variables{ + "length": config.IntegerVariable(9), + "numeric": config.BoolVariable(false), + }, + Check: TestCheckResourceAttrPtr("random_password.test", "length", teststep.Pointer("9")), + }, + }, }) } -func TestTest_TestStep_ProtoV6ProviderFactories(t *testing.T) { +// TestTest_ConfigDirectory_StaticDirectory_AttributeDoesNotExist uses Terraform +// configuration specifying a "numeric" attribute that was introduced in v3.3.0 of the +// random provider password This test confirms that the TestCase ExternalProviders +// is being used when ConfigDirectory is set. +func TestTest_ConfigDirectory_StaticDirectory_AttributeDoesNotExist(t *testing.T) { t.Parallel() - Test(&mockT{}, TestCase{ + Test(t, TestCase{ Steps: []TestStep{ { - Config: "# not empty", - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature - return nil, nil - }, - }, + ConfigDirectory: config.StaticDirectory(`testdata/fixtures/random_password_3.2.0`), + ExpectError: regexp.MustCompile(`.*An argument named "numeric" is not expected here.`), }, }, }) } -func TestTest_TestStep_ProtoV6ProviderFactories_Error(t *testing.T) { +// TestTest_ConfigDirectory_StaticDirectory_AttributeDoesNotExist_Vars uses Terraform +// configuration specifying a "numeric" attribute that was introduced in v3.3.0 of the +// random provider password This test confirms that the TestCase ExternalProviders +// is being used when ConfigDirectory is set. +func TestTest_ConfigDirectory_StaticDirectory_AttributeDoesNotExist_Vars(t *testing.T) { t.Parallel() - plugintest.TestExpectTFatal(t, func() { - Test(&mockT{}, TestCase{ - Steps: []TestStep{ - { - Config: "# not empty", - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature - return nil, fmt.Errorf("test") - }, - }, + Test(t, TestCase{ + Steps: []TestStep{ + { + ConfigDirectory: config.StaticDirectory(`testdata/fixtures/random_password_3.2.0_vars`), + ConfigVariables: config.Variables{ + "length": config.IntegerVariable(8), + "numeric": config.BoolVariable(false), }, + ExpectError: regexp.MustCompile(`.*An argument named "numeric" is not expected here.`), }, - }) + }, }) } -func TestTest_TestStep_ProviderFactories(t *testing.T) { +// TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist uses Terraform +// configuration specifying a "numeric" attribute that was introduced in v3.3.0 of the +// random provider password This test confirms that the TestCase ExternalProviders +// is being used when ConfigDirectory is set. +func TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist(t *testing.T) { t.Parallel() - Test(&mockT{}, TestCase{ + Test(t, TestCase{ Steps: []TestStep{ { - Config: "# not empty", - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return nil, nil - }, - }, + ConfigDirectory: config.TestNameDirectory(), + ExpectError: regexp.MustCompile(`.*An argument named "numeric" is not expected here.`), }, }, }) } -func TestTest_TestStep_ProviderFactories_Error(t *testing.T) { +// TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_Vars uses Terraform +// configuration specifying a "numeric" attribute that was introduced in v3.3.0 of the +// random provider password This test confirms that the TestCase ExternalProviders +// is being used when ConfigDirectory is set. +func TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_Vars(t *testing.T) { t.Parallel() - plugintest.TestExpectTFatal(t, func() { - Test(&mockT{}, TestCase{ - Steps: []TestStep{ - { - Config: "# not empty", - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": func() (*schema.Provider, error) { //nolint:unparam // required signature - return nil, fmt.Errorf("test") - }, - }, + Test(t, TestCase{ + Steps: []TestStep{ + { + ConfigDirectory: config.TestNameDirectory(), + ConfigVariables: config.Variables{ + "length": config.IntegerVariable(8), + "numeric": config.BoolVariable(false), }, + ExpectError: regexp.MustCompile(`.*An argument named "numeric" is not expected here.`), }, - }) + }, }) } -func TestTest_TestStep_ProviderFactories_To_ExternalProviders(t *testing.T) { +// TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist uses Terraform +// configuration specifying a "numeric" attribute that was introduced in v3.3.0 of the +// random provider password This test confirms that the TestCase ExternalProviders +// is being used when ConfigDirectory is set. +func TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist(t *testing.T) { t.Parallel() Test(t, TestCase{ Steps: []TestStep{ { - Config: `resource "null_resource" "test" {}`, - ProviderFactories: map[string]func() (*schema.Provider, error){ - "null": func() (*schema.Provider, error) { //nolint:unparam // required signature - return &schema.Provider{ - ResourcesMap: map[string]*schema.Resource{ - "null_resource": { - CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { - d.SetId("test") - return nil - }, - DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { - return nil - }, - ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { - return nil - }, - Schema: map[string]*schema.Schema{ - "triggers": { - Elem: &schema.Schema{Type: schema.TypeString}, - ForceNew: true, - Optional: true, - Type: schema.TypeMap, - }, - }, - }, - }, - }, nil - }, - }, + ConfigDirectory: config.TestStepDirectory(), + ExpectError: regexp.MustCompile(`.*An argument named "numeric" is not expected here.`), }, + }, + }) +} + +// TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_Vars uses Terraform +// configuration specifying a "numeric" attribute that was introduced in v3.3.0 of the +// random provider password This test confirms that the TestCase ExternalProviders +// is being used when ConfigDirectory is set. +func TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_Vars(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ { - Config: `resource "null_resource" "test" {}`, - ExternalProviders: map[string]ExternalProvider{ - "null": { - Source: "registry.terraform.io/hashicorp/null", - }, + ConfigDirectory: config.TestStepDirectory(), + ConfigVariables: config.Variables{ + "length": config.IntegerVariable(8), + "numeric": config.BoolVariable(false), }, + ExpectError: regexp.MustCompile(`.*An argument named "numeric" is not expected here.`), }, }, }) } -func TestTest_TestStep_ProviderFactories_Import_Inline(t *testing.T) { - id := "none" +// TestTest_ConfigDirectory_StaticDirectory_AttributeDoesNotExist_MultipleFiles uses Terraform +// configuration specifying a "numeric" attribute that was introduced in v3.3.0 of the +// random provider password This test confirms that the TestCase ExternalProviders +// is being used when ConfigDirectory is set. +func TestTest_ConfigDirectory_StaticDirectory_AttributeDoesNotExist_MultipleFiles(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ + { + ConfigDirectory: config.StaticDirectory(`testdata/fixtures/random_password_3.2.0_multiple_files`), + ExpectError: regexp.MustCompile(`.*An argument named "numeric" is not expected here.`), + }, + }, + }) +} +// TestTest_ConfigDirectory_StaticDirectory_AttributeDoesNotExist_MultipleFiles_Vars uses Terraform +// configuration specifying a "numeric" attribute that was introduced in v3.3.0 of the +// random provider password This test confirms that the TestCase ExternalProviders +// is being used when ConfigDirectory is set. +func TestTest_ConfigDirectory_StaticDirectory_AttributeDoesNotExist_MultipleFiles_Vars(t *testing.T) { t.Parallel() Test(t, TestCase{ Steps: []TestStep{ { - Config: `resource "random_password" "test" { length = 12 }`, - ProviderFactories: map[string]func() (*schema.Provider, error){ - "random": func() (*schema.Provider, error) { //nolint:unparam // required signature - return &schema.Provider{ - ResourcesMap: map[string]*schema.Resource{ - "random_password": { - DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { - return nil - }, - ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { - return nil - }, - Schema: map[string]*schema.Schema{ - "length": { - Required: true, - ForceNew: true, - Type: schema.TypeInt, - }, - "result": { - Type: schema.TypeString, - Computed: true, - Sensitive: true, - }, + ConfigDirectory: config.StaticDirectory(`testdata/fixtures/random_password_3.2.0_multiple_files_vars`), + ConfigVariables: config.Variables{ + "length": config.IntegerVariable(8), + "numeric": config.BoolVariable(false), + }, + ExpectError: regexp.MustCompile(`.*An argument named "numeric" is not expected here.`), + }, + }, + }) +} - "id": { - Computed: true, - Type: schema.TypeString, - }, - }, - Importer: &schema.ResourceImporter{ - StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { - val := d.Id() +// TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_MultipleFiles uses Terraform +// configuration specifying a "numeric" attribute that was introduced in v3.3.0 of the +// random provider password This test confirms that the TestCase ExternalProviders +// is being used when ConfigDirectory is set. +func TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_MultipleFiles(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ + { + ConfigDirectory: config.TestNameDirectory(), + ExpectError: regexp.MustCompile(`.*An argument named "numeric" is not expected here.`), + }, + }, + }) +} - d.SetId("none") +// TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_MultipleFiles_Vars uses Terraform +// configuration specifying a "numeric" attribute that was introduced in v3.3.0 of the +// random provider password This test confirms that the TestCase ExternalProviders +// is being used when ConfigDirectory is set. +func TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_MultipleFiles_Vars(t *testing.T) { + t.Parallel() - err := d.Set("result", val) - if err != nil { - panic(err) - } + Test(t, TestCase{ + Steps: []TestStep{ + { + ConfigDirectory: config.TestNameDirectory(), + ConfigVariables: config.Variables{ + "length": config.IntegerVariable(8), + "numeric": config.BoolVariable(false), + }, + ExpectError: regexp.MustCompile(`.*An argument named "numeric" is not expected here.`), + }, + }, + }) +} - err = d.Set("length", len(val)) - if err != nil { - panic(err) - } +// TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_MultipleFiles uses Terraform +// configuration specifying a "numeric" attribute that was introduced in v3.3.0 of the +// random provider password This test confirms that the TestCase ExternalProviders +// is being used when ConfigDirectory is set. +func TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_MultipleFiles(t *testing.T) { + t.Parallel() - return []*schema.ResourceData{d}, nil - }, - }, - }, - }, - }, nil - }, - }, - ResourceName: "random_password.test", - ImportState: true, - ImportStateId: "Z=:cbrJE?Ltg", - ImportStatePersist: true, - ImportStateCheck: composeImportStateCheck( - testCheckResourceAttrInstanceState(&id, "result", "Z=:cbrJE?Ltg"), - testCheckResourceAttrInstanceState(&id, "length", "12"), - ), + Test(t, TestCase{ + Steps: []TestStep{ + { + ConfigDirectory: config.TestStepDirectory(), + ExpectError: regexp.MustCompile(`.*An argument named "numeric" is not expected here.`), }, }, }) } -func TestTest_TestStep_ProviderFactories_Import_Inline_WithPersistMatch(t *testing.T) { - var result1, result2 string +// TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_MultipleFiles_Vars uses Terraform +// configuration specifying a "numeric" attribute that was introduced in v3.3.0 of the +// random provider password This test confirms that the TestCase ExternalProviders +// is being used when ConfigDirectory is set. +func TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_MultipleFiles_Vars(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ + { + ConfigDirectory: config.TestStepDirectory(), + ConfigVariables: config.Variables{ + "length": config.IntegerVariable(8), + "numeric": config.BoolVariable(false), + }, + ExpectError: regexp.MustCompile(`.*An argument named "numeric" is not expected here.`), + }, + }, + }) +} +func TestTest_TestStep_ProviderFactories_ConfigDirectory_StaticDirectory(t *testing.T) { t.Parallel() Test(t, TestCase{ @@ -1574,49 +3892,18 @@ func TestTest_TestStep_ProviderFactories_Import_Inline_WithPersistMatch(t *testi "random": func() (*schema.Provider, error) { //nolint:unparam // required signature return &schema.Provider{ ResourcesMap: map[string]*schema.Resource{ - "random_password": { + "random_id": { + CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + d.SetId(time.Now().String()) + return nil + }, DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { return nil }, ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { return nil }, - Schema: map[string]*schema.Schema{ - "length": { - Required: true, - ForceNew: true, - Type: schema.TypeInt, - }, - "result": { - Type: schema.TypeString, - Computed: true, - Sensitive: true, - }, - - "id": { - Computed: true, - Type: schema.TypeString, - }, - }, - Importer: &schema.ResourceImporter{ - StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { - val := d.Id() - - d.SetId("none") - - err := d.Set("result", val) - if err != nil { - panic(err) - } - - err = d.Set("length", len(val)) - if err != nil { - panic(err) - } - - return []*schema.ResourceData{d}, nil - }, - }, + Schema: map[string]*schema.Schema{}, }, }, }, nil @@ -1624,27 +3911,14 @@ func TestTest_TestStep_ProviderFactories_Import_Inline_WithPersistMatch(t *testi }, Steps: []TestStep{ { - Config: `resource "random_password" "test" { length = 12 }`, - ResourceName: "random_password.test", - ImportState: true, - ImportStateId: "Z=:cbrJE?Ltg", - ImportStatePersist: true, - ImportStateCheck: composeImportStateCheck( - testExtractResourceAttrInstanceState("none", "result", &result1), - ), - }, - { - Config: `resource "random_password" "test" { length = 12 }`, - Check: ComposeTestCheckFunc( - testExtractResourceAttr("random_password.test", "result", &result2), - testCheckAttributeValuesEqual(&result1, &result2), - ), + ConfigDirectory: config.StaticDirectory(`testdata/fixtures/random_id`), + Check: TestCheckResourceAttrSet("random_id.test", "id"), }, }, }) } -func TestTest_TestStep_ProviderFactories_Import_Inline_WithoutPersist(t *testing.T) { +func TestTest_TestStep_ProviderFactories_ConfigDirectory_TestNameDirectory(t *testing.T) { t.Parallel() Test(t, TestCase{ @@ -1652,9 +3926,9 @@ func TestTest_TestStep_ProviderFactories_Import_Inline_WithoutPersist(t *testing "random": func() (*schema.Provider, error) { //nolint:unparam // required signature return &schema.Provider{ ResourcesMap: map[string]*schema.Resource{ - "random_password": { + "random_id": { CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { - d.SetId("none") + d.SetId(time.Now().String()) return nil }, DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { @@ -1663,42 +3937,7 @@ func TestTest_TestStep_ProviderFactories_Import_Inline_WithoutPersist(t *testing ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { return nil }, - Schema: map[string]*schema.Schema{ - "length": { - Required: true, - ForceNew: true, - Type: schema.TypeInt, - }, - "result": { - Type: schema.TypeString, - Computed: true, - Sensitive: true, - }, - - "id": { - Computed: true, - Type: schema.TypeString, - }, - }, - Importer: &schema.ResourceImporter{ - StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { - val := d.Id() - - d.SetId("none") - - err := d.Set("result", val) - if err != nil { - panic(err) - } - - err = d.Set("length", len(val)) - if err != nil { - panic(err) - } - - return []*schema.ResourceData{d}, nil - }, - }, + Schema: map[string]*schema.Schema{}, }, }, }, nil @@ -1706,382 +3945,299 @@ func TestTest_TestStep_ProviderFactories_Import_Inline_WithoutPersist(t *testing }, Steps: []TestStep{ { - Config: `resource "random_password" "test" { length = 12 }`, - ResourceName: "random_password.test", - ImportState: true, - ImportStateId: "Z=:cbrJE?Ltg", - ImportStatePersist: false, - }, - { - Config: `resource "random_password" "test" { length = 12 }`, - Check: ComposeTestCheckFunc( - TestCheckNoResourceAttr("random_password.test", "result"), - ), + ConfigDirectory: config.TestNameDirectory(), + Check: TestCheckResourceAttrSet("random_id.test", "id"), }, }, }) } -func TestTest_TestStep_ProviderFactories_Import_External(t *testing.T) { - id := "none" - +func TestTest_TestStep_ProviderFactories_ConfigDirectory_TestStepDirectory(t *testing.T) { t.Parallel() Test(t, TestCase{ - ExternalProviders: map[string]ExternalProvider{ - "random": { - Source: "registry.terraform.io/hashicorp/random", + ProviderFactories: map[string]func() (*schema.Provider, error){ + "random": func() (*schema.Provider, error) { //nolint:unparam // required signature + return &schema.Provider{ + ResourcesMap: map[string]*schema.Resource{ + "random_id": { + CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + d.SetId(time.Now().String()) + return nil + }, + DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + Schema: map[string]*schema.Schema{}, + }, + }, + }, nil }, }, Steps: []TestStep{ { - Config: `resource "random_password" "test" { length = 12 }`, - ResourceName: "random_password.test", - ImportState: true, - ImportStateId: "Z=:cbrJE?Ltg", - ImportStatePersist: true, - ImportStateCheck: composeImportStateCheck( - testCheckResourceAttrInstanceState(&id, "result", "Z=:cbrJE?Ltg"), - testCheckResourceAttrInstanceState(&id, "length", "12"), - ), + ConfigDirectory: config.TestStepDirectory(), + Check: TestCheckResourceAttrSet("random_id.test", "id"), }, }, }) } -func TestTest_TestStep_ProviderFactories_Import_External_WithPersistMatch(t *testing.T) { - var result1, result2 string - +func TestTest_ConfigFile_StaticFile(t *testing.T) { t.Parallel() Test(t, TestCase{ - ExternalProviders: map[string]ExternalProvider{ - "random": { - Source: "registry.terraform.io/hashicorp/random", - }, - }, - Steps: []TestStep{ - { - Config: `resource "random_password" "test" { length = 12 }`, - ResourceName: "random_password.test", - ImportState: true, - ImportStateId: "Z=:cbrJE?Ltg", - ImportStatePersist: true, - ImportStateCheck: composeImportStateCheck( - testExtractResourceAttrInstanceState("none", "result", &result1), - ), - }, - { - Config: `resource "random_password" "test" { length = 12 }`, - Check: ComposeTestCheckFunc( - testExtractResourceAttr("random_password.test", "result", &result2), - testCheckAttributeValuesEqual(&result1, &result2), - ), - }, - }, - }) -} - -//nolint:paralleltest // Can't use t.Parallel with t.Setenv -func TestTest_TestStep_ProviderFactories_Import_External_WithPersistMatch_WithPersistWorkingDir(t *testing.T) { - var result1, result2 string - - t.Setenv(plugintest.EnvTfAccPersistWorkingDir, "1") - workingDir := t.TempDir() - - testSteps := []TestStep{ - { - Config: `resource "random_password" "test" { length = 12 }`, - ResourceName: "random_password.test", - ImportState: true, - ImportStateId: "Z=:cbrJE?Ltg", - ImportStatePersist: true, - ImportStateCheck: composeImportStateCheck( - testExtractResourceAttrInstanceState("none", "result", &result1), - ), - }, - { - Config: `resource "random_password" "test" { length = 12 }`, - Check: ComposeTestCheckFunc( - testExtractResourceAttr("random_password.test", "result", &result2), - testCheckAttributeValuesEqual(&result1, &result2), - ), - }, - } - - Test(t, TestCase{ - ExternalProviders: map[string]ExternalProvider{ - "random": { - Source: "registry.terraform.io/hashicorp/random", + Steps: []TestStep{ + { + ConfigFile: config.StaticFile(`testdata/fixtures/random_password_3.5.1/random.tf`), + Check: TestCheckResourceAttrSet("random_password.test", "id"), }, }, - WorkingDir: workingDir, - Steps: testSteps, }) +} - for testStepIndex := range testSteps { - dir := filepath.Join(workingDir, fmt.Sprintf("step_%s", strconv.Itoa(testStepIndex+1))) - - dirEntries, err := os.ReadDir(dir) - if err != nil { - t.Errorf("cannot read dir: %s", dir) - } - - var workingDirName string +func TestTest_ConfigFile_StaticFile_Vars(t *testing.T) { + t.Parallel() - // Relies upon convention of a directory being created that is prefixed "work". - for _, dirEntry := range dirEntries { - if strings.HasPrefix(dirEntry.Name(), "work") && dirEntry.IsDir() { - workingDirName = filepath.Join(dir, dirEntry.Name()) - break - } - } + Test(t, TestCase{ + Steps: []TestStep{ + { + ConfigFile: config.StaticFile(`testdata/fixtures/random_password_3.5.1_vars_single_file/random.tf`), + ConfigVariables: config.Variables{ + "length": config.IntegerVariable(8), + "numeric": config.BoolVariable(false), + }, + Check: TestCheckResourceAttrSet("random_password.test", "id"), + }, + }, + }) +} - configPlanStateFiles := []string{ - "terraform_plugin_test.tf", - "terraform.tfstate", - "tfplan", - } +func TestTest_ConfigFile_StaticFile_VarsMissing(t *testing.T) { + t.Parallel() - for _, file := range configPlanStateFiles { - // Skip verifying plan for first test step as there is no plan file if the - // resource does not already exist. - if testStepIndex == 0 && file == "tfplan" { - break - } - _, err = os.Stat(filepath.Join(workingDirName, file)) - if err != nil { - t.Errorf("cannot stat %s in %s: %s", file, workingDirName, err) - } - } - } + Test(t, TestCase{ + Steps: []TestStep{ + { + ConfigFile: config.StaticFile(`testdata/fixtures/random_password_3.5.1_vars_single_file/random.tf`), + Check: TestCheckResourceAttrSet("random_password.test", "id"), + ExpectError: regexp.MustCompile(`.*Error: No value for required variable`)}, + }, + }) } -func TestTest_TestStep_ProviderFactories_Import_External_WithoutPersistNonMatch(t *testing.T) { - var result1, result2 string - +func TestTest_ConfigFile_TestNameFile(t *testing.T) { t.Parallel() Test(t, TestCase{ - ExternalProviders: map[string]ExternalProvider{ - "random": { - Source: "registry.terraform.io/hashicorp/random", - }, - }, Steps: []TestStep{ { - Config: `resource "random_password" "test" { length = 12 }`, - ResourceName: "random_password.test", - ImportState: true, - ImportStateId: "Z=:cbrJE?Ltg", - ImportStatePersist: false, - ImportStateCheck: composeImportStateCheck( - testExtractResourceAttrInstanceState("none", "result", &result1), - ), - }, - { - Config: `resource "random_password" "test" { length = 12 }`, - Check: ComposeTestCheckFunc( - testExtractResourceAttr("random_password.test", "result", &result2), - testCheckAttributeValuesDiffer(&result1, &result2), - ), + ConfigFile: config.TestNameFile("random.tf"), + Check: TestCheckResourceAttrSet("random_password.test", "id"), }, }, }) } -//nolint:paralleltest // Can't use t.Parallel with t.Setenv -func TestTest_TestStep_ProviderFactories_Import_External_WithoutPersistNonMatch_WithPersistWorkingDir(t *testing.T) { - var result1, result2 string - - t.Setenv(plugintest.EnvTfAccPersistWorkingDir, "1") - workingDir := t.TempDir() - - testSteps := []TestStep{ - { - Config: `resource "random_password" "test" { length = 12 }`, - ResourceName: "random_password.test", - ImportState: true, - ImportStateId: "Z=:cbrJE?Ltg", - ImportStatePersist: false, - ImportStateCheck: composeImportStateCheck( - testExtractResourceAttrInstanceState("none", "result", &result1), - ), - }, - { - Config: `resource "random_password" "test" { length = 12 }`, - Check: ComposeTestCheckFunc( - testExtractResourceAttr("random_password.test", "result", &result2), - testCheckAttributeValuesDiffer(&result1, &result2), - ), - }, - } +func TestTest_ConfigFile_TestNameFile_Vars(t *testing.T) { + t.Parallel() Test(t, TestCase{ - ExternalProviders: map[string]ExternalProvider{ - "random": { - Source: "registry.terraform.io/hashicorp/random", + Steps: []TestStep{ + { + ConfigFile: config.TestNameFile("random.tf"), + ConfigVariables: config.Variables{ + "length": config.IntegerVariable(8), + "numeric": config.BoolVariable(false), + }, + Check: TestCheckResourceAttrSet("random_password.test", "id"), }, }, - WorkingDir: workingDir, - Steps: testSteps, }) +} - for testStepIndex := range testSteps { - dir := filepath.Join(workingDir, fmt.Sprintf("step_%s", strconv.Itoa(testStepIndex+1))) +func TestTest_ConfigFile_TestStepFile(t *testing.T) { + t.Parallel() - dirEntries, err := os.ReadDir(dir) - if err != nil { - t.Errorf("cannot read dir: %s", dir) - } + Test(t, TestCase{ + Steps: []TestStep{ + { + ConfigFile: config.TestStepFile("random.tf"), + Check: TestCheckResourceAttrSet("random_password.test", "id"), + }, + }, + }) +} - var workingDirName string +func TestTest_ConfigFile_TestStepFile_Vars(t *testing.T) { + t.Parallel() - // Relies upon convention of a directory being created that is prefixed "work". - for _, dirEntry := range dirEntries { - if strings.HasPrefix(dirEntry.Name(), "work") && dirEntry.IsDir() { - workingDirName = filepath.Join(dir, dirEntry.Name()) - break - } - } + Test(t, TestCase{ + Steps: []TestStep{ + { + ConfigFile: config.TestStepFile("random.tf"), + ConfigVariables: config.Variables{ + "length": config.IntegerVariable(8), + "numeric": config.BoolVariable(false), + }, + Check: TestCheckResourceAttrSet("random_password.test", "id"), + }, + }, + }) +} - configPlanStateFiles := []string{ - "terraform_plugin_test.tf", - "terraform.tfstate", - "tfplan", - } +// TestTest_ConfigFile_StaticFile_AttributeDoesNotExist uses Terraform +// configuration specifying a "numeric" attribute that was introduced in v3.3.0 of the +// random provider password This test confirms that the TestCase ExternalProviders +// is being used when ConfigDirectory is set. +func TestTest_ConfigFile_StaticFile_AttributeDoesNotExist(t *testing.T) { + t.Parallel() - for _, file := range configPlanStateFiles { - // Skip verifying state and plan for first test step as ImportStatePersist is - // false so the state is not persisted and there is no plan file if the - // resource does not already exist. - if testStepIndex == 0 && (file == "terraform.tfstate" || file == "tfplan") { - break - } - _, err = os.Stat(filepath.Join(workingDirName, file)) - if err != nil { - t.Errorf("cannot stat %s in %s: %s", file, workingDirName, err) - } - } - } + Test(t, TestCase{ + Steps: []TestStep{ + { + ConfigFile: config.StaticFile(`testdata/fixtures/random_password_3.2.0/random.tf`), + ExpectError: regexp.MustCompile(`.*An argument named "numeric" is not expected here.`), + }, + }, + }) } -func TestTest_TestStep_ProviderFactories_Refresh_Inline(t *testing.T) { +// TestTest_ConfigFile_StaticFile_AttributeDoesNotExist_Vars uses Terraform +// configuration specifying a "numeric" attribute that was introduced in v3.3.0 of the +// random provider password This test confirms that the TestCase ExternalProviders +// is being used when ConfigDirectory is set. +func TestTest_ConfigFile_StaticFile_AttributeDoesNotExist_Vars(t *testing.T) { t.Parallel() Test(t, TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "random": func() (*schema.Provider, error) { //nolint:unparam // required signature - return &schema.Provider{ - ResourcesMap: map[string]*schema.Resource{ - "random_password": { - CreateContext: func(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { - d.SetId("id") - err := d.Set("min_special", 10) - if err != nil { - panic(err) - } - return nil - }, - DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { - return nil - }, - ReadContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { - err := d.Set("min_special", 2) - if err != nil { - panic(err) - } - return nil - }, - Schema: map[string]*schema.Schema{ - "min_special": { - Computed: true, - Type: schema.TypeInt, - }, - - "id": { - Computed: true, - Type: schema.TypeString, - }, - }, - }, - }, - }, nil + Steps: []TestStep{ + { + ConfigFile: config.StaticFile(`testdata/fixtures/random_password_3.2.0_vars_single_file/random.tf`), + ConfigVariables: config.Variables{ + "length": config.IntegerVariable(8), + "numeric": config.BoolVariable(false), + }, + ExpectError: regexp.MustCompile(`.*An argument named "numeric" is not expected here.`), }, }, + }) +} + +// TestTest_ConfigFile_TestNameFile_AttributeDoesNotExist uses Terraform +// configuration specifying a "numeric" attribute that was introduced in v3.3.0 of the +// random provider password This test confirms that the TestCase ExternalProviders +// is being used when ConfigDirectory is set. +func TestTest_ConfigFile_TestNameFile_AttributeDoesNotExist(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ Steps: []TestStep{ { - Config: `resource "random_password" "test" { }`, - Check: TestCheckResourceAttr("random_password.test", "min_special", "10"), + ConfigFile: config.TestNameFile("random.tf"), + ExpectError: regexp.MustCompile(`.*An argument named "numeric" is not expected here.`), }, + }, + }) +} + +// TestTest_ConfigFile_TestNameFile_AttributeDoesNotExist_Vars uses Terraform +// configuration specifying a "numeric" attribute that was introduced in v3.3.0 of the +// random provider password This test confirms that the TestCase ExternalProviders +// is being used when ConfigDirectory is set. +func TestTest_ConfigFile_TestNameFile_AttributeDoesNotExist_Vars(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ { - RefreshState: true, - Check: TestCheckResourceAttr("random_password.test", "min_special", "2"), + ConfigFile: config.TestNameFile("random.tf"), + ConfigVariables: config.Variables{ + "length": config.IntegerVariable(8), + "numeric": config.BoolVariable(false), + }, + ExpectError: regexp.MustCompile(`.*An argument named "numeric" is not expected here.`), }, + }, + }) +} + +// TestTest_ConfigFile_TestStepFile_AttributeDoesNotExist uses Terraform +// configuration specifying a "numeric" attribute that was introduced in v3.3.0 of the +// random provider password This test confirms that the TestCase ExternalProviders +// is being used when ConfigDirectory is set. +func TestTest_ConfigFile_TestStepFile_AttributeDoesNotExist(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ { - Config: `resource "random_password" "test" { }`, - Check: TestCheckResourceAttr("random_password.test", "min_special", "2"), + ConfigFile: config.TestStepFile("random.tf"), + ExpectError: regexp.MustCompile(`.*An argument named "numeric" is not expected here.`), }, }, }) } -//nolint:paralleltest // Can't use t.Parallel with t.Setenv -func TestTest_TestStep_ProviderFactories_CopyWorkingDir_EachTestStep(t *testing.T) { - t.Setenv(plugintest.EnvTfAccPersistWorkingDir, "1") - workingDir := t.TempDir() +// TestTest_ConfigFile_TestStepFile_AttributeDoesNotExist_Vars uses Terraform +// configuration specifying a "numeric" attribute that was introduced in v3.3.0 of the +// random provider password This test confirms that the TestCase ExternalProviders +// is being used when ConfigDirectory is set. +func TestTest_ConfigFile_TestStepFile_AttributeDoesNotExist_Vars(t *testing.T) { + t.Parallel() - testSteps := []TestStep{ - { - Config: `resource "random_password" "test" { }`, - }, - { - Config: `resource "random_password" "test" { }`, + Test(t, TestCase{ + Steps: []TestStep{ + { + ConfigFile: config.TestStepFile("random.tf"), + ConfigVariables: config.Variables{ + "length": config.IntegerVariable(8), + "numeric": config.BoolVariable(false), + }, + ExpectError: regexp.MustCompile(`.*An argument named "numeric" is not expected here.`), + }, }, - } + }) +} + +func TestTest_TestStep_ProviderFactories_ConfigFile_StaticFile(t *testing.T) { + t.Parallel() Test(t, TestCase{ ProviderFactories: map[string]func() (*schema.Provider, error){ "random": func() (*schema.Provider, error) { //nolint:unparam // required signature return &schema.Provider{ ResourcesMap: map[string]*schema.Resource{ - "random_password": { - CreateContext: func(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { - d.SetId("id") + "random_id": { + CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + d.SetId(time.Now().String()) return nil }, DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { return nil }, - ReadContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { return nil }, - Schema: map[string]*schema.Schema{ - "id": { - Computed: true, - Type: schema.TypeString, - }, - }, + Schema: map[string]*schema.Schema{}, }, }, }, nil }, }, - WorkingDir: workingDir, - Steps: testSteps, + Steps: []TestStep{ + { + ConfigFile: config.StaticFile(`testdata/fixtures/random_id/random.tf`), + Check: TestCheckResourceAttrSet("random_id.test", "id"), + }, + }, }) - - for k := range testSteps { - dir := filepath.Join(workingDir, fmt.Sprintf("step_%s", strconv.Itoa(k+1))) - - _, err := os.ReadDir(dir) - if err != nil { - t.Fatalf("cannot read dir: %s", dir) - } - } } -func TestTest_TestStep_ProviderFactories_RefreshWithPlanModifier_Inline(t *testing.T) { +func TestTest_TestStep_ProviderFactories_ConfigFile_TestNameFile(t *testing.T) { t.Parallel() Test(t, TestCase{ @@ -2089,56 +4245,18 @@ func TestTest_TestStep_ProviderFactories_RefreshWithPlanModifier_Inline(t *testi "random": func() (*schema.Provider, error) { //nolint:unparam // required signature return &schema.Provider{ ResourcesMap: map[string]*schema.Resource{ - "random_password": { - CustomizeDiff: customdiff.All( - func(ctx context.Context, d *schema.ResourceDiff, meta interface{}) error { - special, ok := d.Get("special").(bool) - if !ok { - return fmt.Errorf("unexpected type %T for 'special' key", d.Get("special")) - } - - if special == true { - err := d.SetNew("special", false) - if err != nil { - panic(err) - } - } - return nil - }, - ), - CreateContext: func(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { - d.SetId("id") - err := d.Set("special", false) - if err != nil { - panic(err) - } + "random_id": { + CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + d.SetId(time.Now().String()) return nil }, DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { return nil }, - ReadContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { - t := getTimeForTest() - if t.After(time.Now().Add(time.Hour * 1)) { - err := d.Set("special", true) - if err != nil { - panic(err) - } - } + ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { return nil }, - Schema: map[string]*schema.Schema{ - "special": { - Computed: true, - Type: schema.TypeBool, - ForceNew: true, - }, - - "id": { - Computed: true, - Type: schema.TypeString, - }, - }, + Schema: map[string]*schema.Schema{}, }, }, }, nil @@ -2146,86 +4264,24 @@ func TestTest_TestStep_ProviderFactories_RefreshWithPlanModifier_Inline(t *testi }, Steps: []TestStep{ { - Config: `resource "random_password" "test" { }`, - Check: TestCheckResourceAttr("random_password.test", "special", "false"), - }, - { - PreConfig: setTimeForTest(time.Now().Add(time.Hour * 2)), - RefreshState: true, - ExpectNonEmptyPlan: true, - Check: TestCheckResourceAttr("random_password.test", "special", "true"), - }, - { - PreConfig: setTimeForTest(time.Now()), - Config: `resource "random_password" "test" { }`, - Check: TestCheckResourceAttr("random_password.test", "special", "false"), + ConfigFile: config.TestNameFile("random.tf"), + Check: TestCheckResourceAttrSet("random_id.test", "id"), }, }, }) } -func TestTest_TestStep_ProviderFactories_Import_Inline_With_Data_Source(t *testing.T) { - var id string - +func TestTest_TestStep_ProviderFactories_ConfigFile_TestStepFile(t *testing.T) { t.Parallel() Test(t, TestCase{ ProviderFactories: map[string]func() (*schema.Provider, error){ - "http": func() (*schema.Provider, error) { //nolint:unparam // required signature - return &schema.Provider{ - DataSourcesMap: map[string]*schema.Resource{ - "http": { - ReadContext: func(ctx context.Context, d *schema.ResourceData, i interface{}) (diags diag.Diagnostics) { - url, ok := d.Get("url").(string) - if !ok { - return diag.Errorf("unexpected type %T for 'url' key", d.Get("url")) - } - - responseHeaders := map[string]string{ - "headerOne": "one", - "headerTwo": "two", - "headerThree": "three", - "headerFour": "four", - } - if err := d.Set("response_headers", responseHeaders); err != nil { - return append(diags, diag.Errorf("Error setting HTTP response headers: %s", err)...) - } - - d.SetId(url) - - return diags - }, - Schema: map[string]*schema.Schema{ - "url": { - Type: schema.TypeString, - Required: true, - }, - "response_headers": { - Type: schema.TypeMap, - Computed: true, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - }, - }, - }, - }, - }, nil - }, "random": func() (*schema.Provider, error) { //nolint:unparam // required signature return &schema.Provider{ ResourcesMap: map[string]*schema.Resource{ - "random_string": { + "random_id": { CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { - d.SetId("none") - err := d.Set("length", 4) - if err != nil { - panic(err) - } - err = d.Set("result", "none") - if err != nil { - panic(err) - } + d.SetId(time.Now().String()) return nil }, DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { @@ -2234,42 +4290,7 @@ func TestTest_TestStep_ProviderFactories_Import_Inline_With_Data_Source(t *testi ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { return nil }, - Schema: map[string]*schema.Schema{ - "length": { - Required: true, - ForceNew: true, - Type: schema.TypeInt, - }, - "result": { - Type: schema.TypeString, - Computed: true, - Sensitive: true, - }, - - "id": { - Computed: true, - Type: schema.TypeString, - }, - }, - Importer: &schema.ResourceImporter{ - StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { - val := d.Id() - - d.SetId(val) - - err := d.Set("result", val) - if err != nil { - panic(err) - } - - err = d.Set("length", len(val)) - if err != nil { - panic(err) - } - - return []*schema.ResourceData{d}, nil - }, - }, + Schema: map[string]*schema.Schema{}, }, }, }, nil @@ -2277,73 +4298,8 @@ func TestTest_TestStep_ProviderFactories_Import_Inline_With_Data_Source(t *testi }, Steps: []TestStep{ { - Config: `data "http" "example" { - url = "https://checkpoint-api.hashicorp.com/v1/check/terraform" - } - - resource "random_string" "example" { - length = length(data.http.example.response_headers) - }`, - Check: extractResourceAttr("random_string.example", "id", &id), - }, - { - Config: `data "http" "example" { - url = "https://checkpoint-api.hashicorp.com/v1/check/terraform" - } - - resource "random_string" "example" { - length = length(data.http.example.response_headers) - }`, - ResourceName: "random_string.example", - ImportState: true, - ImportStateCheck: composeImportStateCheck( - testCheckResourceAttrInstanceState(&id, "length", "4"), - ), - ImportStateVerify: true, - }, - }, - }) -} - -func TestTest_TestStep_ProviderFactories_Import_External_With_Data_Source(t *testing.T) { - var id string - - t.Parallel() - - Test(t, TestCase{ - ExternalProviders: map[string]ExternalProvider{ - "http": { - Source: "registry.terraform.io/hashicorp/http", - }, - "random": { - Source: "registry.terraform.io/hashicorp/random", - }, - }, - Steps: []TestStep{ - { - Config: `data "http" "example" { - url = "https://checkpoint-api.hashicorp.com/v1/check/terraform" - } - - resource "random_string" "example" { - length = length(data.http.example.response_headers) - }`, - Check: extractResourceAttr("random_string.example", "id", &id), - }, - { - Config: `data "http" "example" { - url = "https://checkpoint-api.hashicorp.com/v1/check/terraform" - } - - resource "random_string" "example" { - length = length(data.http.example.response_headers) - }`, - ResourceName: "random_string.example", - ImportState: true, - ImportStateCheck: composeImportStateCheck( - testCheckResourceAttrInstanceState(&id, "length", "12"), - ), - ImportStateVerify: true, + ConfigFile: config.TestStepFile("random.tf"), + Check: TestCheckResourceAttrSet("random_id.test", "id"), }, }, }) diff --git a/helper/resource/teststep_test.go b/helper/resource/teststep_test.go new file mode 100644 index 000000000..feffb7524 --- /dev/null +++ b/helper/resource/teststep_test.go @@ -0,0 +1,202 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package resource + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" + "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/tfversion" +) + +// Reference: https://github.com/hashicorp/terraform-plugin-testing/issues/84 +func TestTestStep_ImportStateVerifyIdentifierAttribute(t *testing.T) { + t.Parallel() + + UnitTest(t, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + }, + Steps: []TestStep{ + { + Check: ComposeAggregateTestCheckFunc( + TestCheckNoResourceAttr("test_resource.test", "id"), + TestCheckResourceAttr("test_resource.test", "not_id", "test"), + ), + Config: `resource "test_resource" "test" {}`, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "not_id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "not_id": tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "not_id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + }, + { + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "not_id", + ResourceName: "test_resource.test", + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + ImportStateResponse: &resource.ImportStateResponse{ + State: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "not_id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "not_id": tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "not_id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + }, + }, + }) +} + +// Reference: https://github.com/hashicorp/terraform-plugin-testing/issues/84 +func TestTestStep_ImportStateVerifyIdentifierAttribute_Error(t *testing.T) { + t.Parallel() + + plugintest.TestExpectTFatal(t, func() { + Test(&mockT{}, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + }, + Steps: []TestStep{ + { + Check: ComposeAggregateTestCheckFunc( + TestCheckNoResourceAttr("test_resource.test", "id"), + TestCheckResourceAttr("test_resource.test", "not_id", "test"), + ), + Config: `resource "test_resource" "test" {}`, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "not_id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "not_id": tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "not_id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + }, + { + // Intentionally not setting ImportStateVerifyIdentifierAttribute + ImportState: true, + ImportStateVerify: true, + ResourceName: "test_resource.test", + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + ImportStateResponse: &resource.ImportStateResponse{ + State: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "not_id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "not_id": tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "not_id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + }, + }, + }) + }) +} diff --git a/helper/resource/teststep_validate.go b/helper/resource/teststep_validate.go index b1da44ad4..8590ca466 100644 --- a/helper/resource/teststep_validate.go +++ b/helper/resource/teststep_validate.go @@ -7,41 +7,88 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-testing/config" "github.com/hashicorp/terraform-plugin-testing/internal/logging" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" ) // testStepValidateRequest contains data for the (TestStep).validate() method. type testStepValidateRequest struct { + // StepConfiguration contains the TestStep configuration derived from + // TestStep.Config, TestStep.ConfigDirectory, or TestStep.ConfigFile. + StepConfiguration teststep.Config + // StepNumber is the index of the TestStep in the TestCase.Steps. StepNumber int + // TestCaseHasExternalProviders is enabled if the TestCase has + // ExternalProviders. + TestCaseHasExternalProviders bool + // TestCaseHasProviders is enabled if the TestCase has set any of // ExternalProviders, ProtoV5ProviderFactories, ProtoV6ProviderFactories, // or ProviderFactories. TestCaseHasProviders bool + + // TestName is the name of the test. + TestName string +} + +// hasExternalProviders returns true if the TestStep has +// ExternalProviders set. +func (s TestStep) hasExternalProviders() bool { + return len(s.ExternalProviders) > 0 } // hasProviders returns true if the TestStep has set any of the // ExternalProviders, ProtoV5ProviderFactories, ProtoV6ProviderFactories, or -// ProviderFactories fields. -func (s TestStep) hasProviders(_ context.Context) bool { +// ProviderFactories fields. It will also return true if ConfigDirectory or +// Config contain terraform configuration which specify a provider block. +func (s TestStep) hasProviders(ctx context.Context, stepIndex int, testName string) (bool, error) { if len(s.ExternalProviders) > 0 { - return true + return true, nil } if len(s.ProtoV5ProviderFactories) > 0 { - return true + return true, nil } if len(s.ProtoV6ProviderFactories) > 0 { - return true + return true, nil } if len(s.ProviderFactories) > 0 { - return true + return true, nil + } + + configRequest := teststep.PrepareConfigurationRequest{ + Directory: s.ConfigDirectory, + File: s.ConfigFile, + TestStepConfigRequest: config.TestStepConfigRequest{ + StepNumber: stepIndex + 1, + TestName: testName, + }, + }.Exec() + + cfg := teststep.Configuration(configRequest) + + var cfgHasProviders bool + + if cfg != nil { + var err error + + cfgHasProviders, err = cfg.HasProviderBlock(ctx) + + if err != nil { + return false, err + } } - return false + if cfgHasProviders { + return true, nil + } + + return false, nil } // validate ensures the TestStep is valid based on the following criteria: @@ -67,14 +114,14 @@ func (s TestStep) validate(ctx context.Context, req testStepValidateRequest) err logging.HelperResourceTrace(ctx, "Validating TestStep") - if s.Config == "" && !s.ImportState && !s.RefreshState { - err := fmt.Errorf("TestStep missing Config or ImportState or RefreshState") + if req.StepConfiguration == nil && !s.ImportState && !s.RefreshState { + err := fmt.Errorf("TestStep missing Config or ConfigDirectory or ConfigFile or ImportState or RefreshState") logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) return err } - if s.Config != "" && s.RefreshState { - err := fmt.Errorf("TestStep cannot have Config and RefreshState") + if req.StepConfiguration != nil && s.RefreshState { + err := fmt.Errorf("TestStep cannot have Config or ConfigDirectory or ConfigFile and RefreshState") logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) return err } @@ -105,7 +152,25 @@ func (s TestStep) validate(ctx context.Context, req testStepValidateRequest) err } } - hasProviders := s.hasProviders(ctx) + if req.TestCaseHasExternalProviders && req.StepConfiguration != nil && req.StepConfiguration.HasConfigurationFiles() { + err := fmt.Errorf("Providers must only be specified within the terraform configuration files when using TestStep.Config") + logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) + return err + } + + if s.hasExternalProviders() && req.StepConfiguration != nil && req.StepConfiguration.HasConfigurationFiles() { + err := fmt.Errorf("Providers must only be specified within the terraform configuration files when using TestStep.Config") + logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) + return err + } + + // We need a 0-based step index for consistency + hasProviders, err := s.hasProviders(ctx, req.StepNumber-1, req.TestName) + + if err != nil { + logging.HelperResourceError(ctx, "TestStep error checking for providers", map[string]interface{}{logging.KeyError: err}) + return err + } if req.TestCaseHasProviders && hasProviders { err := fmt.Errorf("Providers must only be specified either at the TestCase or TestStep level") @@ -113,8 +178,19 @@ func (s TestStep) validate(ctx context.Context, req testStepValidateRequest) err return err } - if !req.TestCaseHasProviders && !hasProviders { - err := fmt.Errorf("Providers must be specified at the TestCase level or in all TestStep") + var cfgHasProviderBlock bool + + if req.StepConfiguration != nil { + cfgHasProviderBlock, err = req.StepConfiguration.HasProviderBlock(ctx) + + if err != nil { + logging.HelperResourceError(ctx, "TestStep error checking for if configuration has provider block", map[string]interface{}{logging.KeyError: err}) + return err + } + } + + if !req.TestCaseHasProviders && !hasProviders && !cfgHasProviderBlock { + err := fmt.Errorf("Providers must be specified at the TestCase level, or in all TestStep, or in TestStep.ConfigDirectory or TestStep.ConfigFile") logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) return err } @@ -128,8 +204,8 @@ func (s TestStep) validate(ctx context.Context, req testStepValidateRequest) err } if len(s.ConfigPlanChecks.PreApply) > 0 { - if s.Config == "" { - err := fmt.Errorf("TestStep ConfigPlanChecks.PreApply must only be specified with Config") + if req.StepConfiguration == nil { + err := fmt.Errorf("TestStep ConfigPlanChecks.PreApply must only be specified with Config, ConfigDirectory or ConfigFile") logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) return err } @@ -141,14 +217,14 @@ func (s TestStep) validate(ctx context.Context, req testStepValidateRequest) err } } - if len(s.ConfigPlanChecks.PostApplyPreRefresh) > 0 && s.Config == "" { - err := fmt.Errorf("TestStep ConfigPlanChecks.PostApplyPreRefresh must only be specified with Config") + if len(s.ConfigPlanChecks.PostApplyPreRefresh) > 0 && req.StepConfiguration == nil { + err := fmt.Errorf("TestStep ConfigPlanChecks.PostApplyPreRefresh must only be specified with Config, ConfigDirectory or ConfigFile") logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) return err } - if len(s.ConfigPlanChecks.PostApplyPostRefresh) > 0 && s.Config == "" { - err := fmt.Errorf("TestStep ConfigPlanChecks.PostApplyPostRefresh must only be specified with Config") + if len(s.ConfigPlanChecks.PostApplyPostRefresh) > 0 && req.StepConfiguration == nil { + err := fmt.Errorf("TestStep ConfigPlanChecks.PostApplyPostRefresh must only be specified with Config, ConfigDirectory or ConfigFile") logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) return err } @@ -159,5 +235,11 @@ func (s TestStep) validate(ctx context.Context, req testStepValidateRequest) err return err } + if len(s.ConfigStateChecks) > 0 && req.StepConfiguration == nil { + err := fmt.Errorf("TestStep ConfigStateChecks must only be specified with Config, ConfigDirectory or ConfigFile") + logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) + return err + } + return nil } diff --git a/helper/resource/teststep_validate_test.go b/helper/resource/teststep_validate_test.go index cf910c576..e040726d5 100644 --- a/helper/resource/teststep_validate_test.go +++ b/helper/resource/teststep_validate_test.go @@ -12,11 +12,49 @@ import ( "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) +func TestTestStepHasExternalProviders(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + testStep TestStep + expected bool + }{ + "none": { + testStep: TestStep{}, + expected: false, + }, + "externalproviders": { + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "test": {}, // does not need to be real + }, + }, + expected: true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := test.testStep.hasExternalProviders() + + if got != test.expected { + t.Errorf("expected %t, got %t", test.expected, got) + } + }) + } +} + func TestTestStepHasProviders(t *testing.T) { t.Parallel() @@ -62,18 +100,24 @@ func TestTestStepHasProviders(t *testing.T) { }, } - for name, test := range tests { - name, test := name, test + var stepIndex int + for name, test := range tests { t.Run(name, func(t *testing.T) { t.Parallel() - got := test.testStep.hasProviders(context.Background()) + got, err := test.testStep.hasProviders(context.Background(), stepIndex, "TestTestStepHasProviders") + + if err != nil { + t.Errorf("unexpected error: %s", err) + } if got != test.expected { t.Errorf("expected %t, got %t", test.expected, got) } }) + + stepIndex++ } } @@ -82,20 +126,37 @@ func TestTestStepValidate(t *testing.T) { tests := map[string]struct { testStep TestStep + testStepConfig string + testStepConfigDirectory string + testStepConfigFile string testStepValidateRequest testStepValidateRequest expectedError error }{ "config-and-importstate-and-refreshstate-missing": { testStep: TestStep{}, testStepValidateRequest: testStepValidateRequest{}, - expectedError: fmt.Errorf("TestStep missing Config or ImportState or RefreshState"), + expectedError: fmt.Errorf("TestStep missing Config or ConfigDirectory or ConfigFile or ImportState or RefreshState"), }, "config-and-refreshstate-both-set": { testStep: TestStep{ - Config: "# not empty", RefreshState: true, }, - expectedError: fmt.Errorf("TestStep cannot have Config and RefreshState"), + testStepConfig: "# not empty", + expectedError: fmt.Errorf("TestStep cannot have Config or ConfigDirectory or ConfigFile and RefreshState"), + }, + "config-directory-and-refreshstate-both-set": { + testStep: TestStep{ + RefreshState: true, + }, + testStepConfigDirectory: "# not empty", + expectedError: fmt.Errorf("TestStep cannot have Config or ConfigDirectory or ConfigFile and RefreshState"), + }, + "config-file-and-refreshstate-both-set": { + testStep: TestStep{ + RefreshState: true, + }, + testStepConfigFile: "# not empty", + expectedError: fmt.Errorf("TestStep cannot have Config or ConfigDirectory or ConfigFile and RefreshState"), }, "refreshstate-first-step": { testStep: TestStep{ @@ -124,7 +185,6 @@ func TestTestStepValidate(t *testing.T) { }, "externalproviders-overlapping-providerfactories": { testStep: TestStep{ - Config: "# not empty", ExternalProviders: map[string]ExternalProvider{ "test": {}, // does not need to be real }, @@ -132,16 +192,79 @@ func TestTestStepValidate(t *testing.T) { "test": nil, // does not need to be real }, }, + testStepConfig: "# not empty", + testStepValidateRequest: testStepValidateRequest{}, + expectedError: fmt.Errorf("TestStep provider \"test\" set in both ExternalProviders and ProviderFactories"), + }, + "externalproviders-overlapping-providerfactories-config-directory": { + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "test": {}, // does not need to be real + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": nil, // does not need to be real + }, + }, + testStepConfigDirectory: "# not empty", testStepValidateRequest: testStepValidateRequest{}, expectedError: fmt.Errorf("TestStep provider \"test\" set in both ExternalProviders and ProviderFactories"), }, + "externalproviders-overlapping-providerfactories-config-file": { + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "test": {}, // does not need to be real + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": nil, // does not need to be real + }, + }, + testStepConfigFile: "# not empty", + testStepValidateRequest: testStepValidateRequest{}, + expectedError: fmt.Errorf("TestStep provider \"test\" set in both ExternalProviders and ProviderFactories"), + }, + "externalproviders-testcase-config-directory": { + testStep: TestStep{}, + testStepConfigDirectory: "# not empty", + testStepValidateRequest: testStepValidateRequest{ + TestCaseHasExternalProviders: true, + }, + expectedError: fmt.Errorf("Providers must only be specified within the terraform configuration files when using TestStep.Config"), + }, + "externalproviders-testcase-config-file": { + testStep: TestStep{}, + testStepConfigFile: "# not empty", + testStepValidateRequest: testStepValidateRequest{ + TestCaseHasExternalProviders: true, + }, + expectedError: fmt.Errorf("Providers must only be specified within the terraform configuration files when using TestStep.Config"), + }, + "externalproviders-teststep-config-directory": { + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "test": {}, // does not need to be real + }, + }, + testStepConfigDirectory: "# not empty", + testStepValidateRequest: testStepValidateRequest{}, + expectedError: fmt.Errorf("Providers must only be specified within the terraform configuration files when using TestStep.Config"), + }, + "externalproviders-teststep-config-file": { + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "test": {}, // does not need to be real + }, + }, + testStepConfigFile: "# not empty", + testStepValidateRequest: testStepValidateRequest{}, + expectedError: fmt.Errorf("Providers must only be specified within the terraform configuration files when using TestStep.Config"), + }, "externalproviders-testcase-providers": { testStep: TestStep{ - Config: "# not empty", ExternalProviders: map[string]ExternalProvider{ "test": {}, // does not need to be real }, }, + testStepConfig: "# not empty", testStepValidateRequest: testStepValidateRequest{ TestCaseHasProviders: true, }, @@ -156,13 +279,50 @@ func TestTestStepValidate(t *testing.T) { }, expectedError: fmt.Errorf("TestStep ImportState must be specified with ImportStateId, ImportStateIdFunc, or ResourceName"), }, + // This test has been added to verify that providers can be defined + // both within the TestStep.Config and at the TestCase level. + // The regression was reported in + // https://github.com/hashicorp/terraform-plugin-testing/issues/176 + "config-providers-testcase-providers": { + testStep: TestStep{ + Config: "provider abc {", + }, + testStepConfig: "# not empty", + testStepValidateRequest: testStepValidateRequest{ + TestCaseHasProviders: true, + }, + }, "protov5providerfactories-testcase-providers": { testStep: TestStep{ - Config: "# not empty", ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ "test": nil, // does not need to be real }, }, + testStepConfig: "# not empty", + testStepValidateRequest: testStepValidateRequest{ + TestCaseHasProviders: true, + }, + expectedError: fmt.Errorf("Providers must only be specified either at the TestCase or TestStep level"), + }, + "protov5providerfactories-testcase-providers-config-directory": { + testStep: TestStep{ + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "test": nil, // does not need to be real + }, + }, + testStepConfigDirectory: "# not empty", + testStepValidateRequest: testStepValidateRequest{ + TestCaseHasProviders: true, + }, + expectedError: fmt.Errorf("Providers must only be specified either at the TestCase or TestStep level"), + }, + "protov5providerfactories-testcase-providers-config-file": { + testStep: TestStep{ + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "test": nil, // does not need to be real + }, + }, + testStepConfigFile: "# not empty", testStepValidateRequest: testStepValidateRequest{ TestCaseHasProviders: true, }, @@ -170,11 +330,35 @@ func TestTestStepValidate(t *testing.T) { }, "protov6providerfactories-testcase-providers": { testStep: TestStep{ - Config: "# not empty", ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "test": nil, // does not need to be real }, }, + testStepConfig: "# not empty", + testStepValidateRequest: testStepValidateRequest{ + TestCaseHasProviders: true, + }, + expectedError: fmt.Errorf("Providers must only be specified either at the TestCase or TestStep level"), + }, + "protov6providerfactories-testcase-providers-config-directory": { + testStep: TestStep{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": nil, // does not need to be real + }, + }, + testStepConfigDirectory: "# not empty", + testStepValidateRequest: testStepValidateRequest{ + TestCaseHasProviders: true, + }, + expectedError: fmt.Errorf("Providers must only be specified either at the TestCase or TestStep level"), + }, + "protov6providerfactories-testcase-providers-config-file": { + testStep: TestStep{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": nil, // does not need to be real + }, + }, + testStepConfigFile: "# not empty", testStepValidateRequest: testStepValidateRequest{ TestCaseHasProviders: true, }, @@ -182,11 +366,35 @@ func TestTestStepValidate(t *testing.T) { }, "providerfactories-testcase-providers": { testStep: TestStep{ - Config: "# not empty", ProviderFactories: map[string]func() (*schema.Provider, error){ "test": nil, // does not need to be real }, }, + testStepConfig: "# not empty", + testStepValidateRequest: testStepValidateRequest{ + TestCaseHasProviders: true, + }, + expectedError: fmt.Errorf("Providers must only be specified either at the TestCase or TestStep level"), + }, + "providerfactories-testcase-providers-config-directory": { + testStep: TestStep{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": nil, // does not need to be real + }, + }, + testStepConfigDirectory: "# not empty", + testStepValidateRequest: testStepValidateRequest{ + TestCaseHasProviders: true, + }, + expectedError: fmt.Errorf("Providers must only be specified either at the TestCase or TestStep level"), + }, + "providerfactories-testcase-providers-config-file": { + testStep: TestStep{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": nil, // does not need to be real + }, + }, + testStepConfigFile: "# not empty", testStepValidateRequest: testStepValidateRequest{ TestCaseHasProviders: true, }, @@ -207,9 +415,31 @@ func TestTestStepValidate(t *testing.T) { ConfigPlanChecks: ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{&planCheckSpy{}}, }, - Config: "# not empty", PlanOnly: true, }, + testStepConfig: "# not empty", + testStepValidateRequest: testStepValidateRequest{TestCaseHasProviders: true}, + expectedError: errors.New("TestStep ConfigPlanChecks.PreApply cannot be run with PlanOnly"), + }, + "configplanchecks-preapply-not-planonly-config-directory": { + testStep: TestStep{ + ConfigPlanChecks: ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{&planCheckSpy{}}, + }, + PlanOnly: true, + }, + testStepConfigDirectory: "testdata/fixtures/random_id", + testStepValidateRequest: testStepValidateRequest{TestCaseHasProviders: true}, + expectedError: errors.New("TestStep ConfigPlanChecks.PreApply cannot be run with PlanOnly"), + }, + "configplanchecks-preapply-not-planonly-config-file": { + testStep: TestStep{ + ConfigPlanChecks: ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{&planCheckSpy{}}, + }, + PlanOnly: true, + }, + testStepConfigFile: "testdata/fixtures/random_id/random.tf", testStepValidateRequest: testStepValidateRequest{TestCaseHasProviders: true}, expectedError: errors.New("TestStep ConfigPlanChecks.PreApply cannot be run with PlanOnly"), }, @@ -233,25 +463,65 @@ func TestTestStepValidate(t *testing.T) { testStepValidateRequest: testStepValidateRequest{TestCaseHasProviders: true}, expectedError: errors.New("TestStep ConfigPlanChecks.PostApplyPostRefresh must only be specified with Config"), }, + "configstatechecks-not-config-mode": { + testStep: TestStep{ + ConfigStateChecks: []statecheck.StateCheck{ + &stateCheckSpy{}, + }, + RefreshState: true, + }, + testStepValidateRequest: testStepValidateRequest{TestCaseHasProviders: true}, + expectedError: errors.New("TestStep ConfigStateChecks must only be specified with Config"), + }, "refreshplanchecks-postrefresh-not-refresh-mode": { testStep: TestStep{ RefreshPlanChecks: RefreshPlanChecks{ PostRefresh: []plancheck.PlanCheck{&planCheckSpy{}}, }, - Config: "# not empty", }, + testStepConfig: "# not empty", + testStepValidateRequest: testStepValidateRequest{TestCaseHasProviders: true}, + expectedError: errors.New("TestStep RefreshPlanChecks.PostRefresh must only be specified with RefreshState"), + }, + "refreshplanchecks-postrefresh-not-refresh-mode-config-directory": { + testStep: TestStep{ + RefreshPlanChecks: RefreshPlanChecks{ + PostRefresh: []plancheck.PlanCheck{&planCheckSpy{}}, + }, + }, + testStepConfigDirectory: "testdata/fixtures/random_id", + testStepValidateRequest: testStepValidateRequest{TestCaseHasProviders: true}, + expectedError: errors.New("TestStep RefreshPlanChecks.PostRefresh must only be specified with RefreshState"), + }, + "refreshplanchecks-postrefresh-not-refresh-mode-config-file": { + testStep: TestStep{ + RefreshPlanChecks: RefreshPlanChecks{ + PostRefresh: []plancheck.PlanCheck{&planCheckSpy{}}, + }, + }, + testStepConfigFile: "testdata/fixtures/random_id/random.tf", testStepValidateRequest: testStepValidateRequest{TestCaseHasProviders: true}, expectedError: errors.New("TestStep RefreshPlanChecks.PostRefresh must only be specified with RefreshState"), }, } for name, test := range tests { - name, test := name, test - t.Run(name, func(t *testing.T) { t.Parallel() - err := test.testStep.validate(context.Background(), test.testStepValidateRequest) + configRequest := teststep.PrepareConfigurationRequest{ + Directory: func(config.TestStepConfigRequest) string { return test.testStepConfigDirectory }, + File: func(config.TestStepConfigRequest) string { return test.testStepConfigFile }, + Raw: test.testStepConfig, + TestStepConfigRequest: config.TestStepConfigRequest{}, + }.Exec() + + testStepConfig := teststep.Configuration(configRequest) + + testStepValidateRequest := test.testStepValidateRequest + testStepValidateRequest.StepConfiguration = testStepConfig + + err := test.testStep.validate(context.Background(), testStepValidateRequest) if err != nil { if test.expectedError == nil { diff --git a/helper/resource/tfversion_checks_test.go b/helper/resource/tfversion_checks_test.go index 0bc2238f4..6410bc732 100644 --- a/helper/resource/tfversion_checks_test.go +++ b/helper/resource/tfversion_checks_test.go @@ -48,8 +48,6 @@ func TestRunTFVersionChecks(t *testing.T) { } for name, test := range tests { - name, test := name, test - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/configs/configschema/coerce_value_test.go b/internal/configs/configschema/coerce_value_test.go index 9363e7075..30ea60e04 100644 --- a/internal/configs/configschema/coerce_value_test.go +++ b/internal/configs/configschema/coerce_value_test.go @@ -546,8 +546,6 @@ func TestCoerceValue(t *testing.T) { } for name, test := range tests { - name, test := name, test - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/configs/configschema/empty_value_test.go b/internal/configs/configschema/empty_value_test.go index 6b7d758da..cd7642568 100644 --- a/internal/configs/configschema/empty_value_test.go +++ b/internal/configs/configschema/empty_value_test.go @@ -163,7 +163,6 @@ func TestBlockEmptyValue(t *testing.T) { } for _, test := range tests { - test := test t.Run(fmt.Sprintf("%#v", test.Schema), func(t *testing.T) { t.Parallel() diff --git a/internal/configs/configschema/implied_type_test.go b/internal/configs/configschema/implied_type_test.go index 5da233a91..1faf25252 100644 --- a/internal/configs/configschema/implied_type_test.go +++ b/internal/configs/configschema/implied_type_test.go @@ -119,8 +119,6 @@ func TestBlockImpliedType(t *testing.T) { } for name, test := range tests { - name, test := name, test - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/configs/hcl2shim/flatmap_test.go b/internal/configs/hcl2shim/flatmap_test.go index 410887fa2..3eb5fead2 100644 --- a/internal/configs/hcl2shim/flatmap_test.go +++ b/internal/configs/hcl2shim/flatmap_test.go @@ -243,7 +243,6 @@ func TestFlatmapValueFromHCL2(t *testing.T) { } for _, test := range tests { - test := test t.Run(test.Value.GoString(), func(t *testing.T) { t.Parallel() @@ -312,7 +311,6 @@ func TestFlatmapValueFromHCL2FromFlatmap(t *testing.T) { } for _, test := range tests { - test := test t.Run(test.Name, func(t *testing.T) { t.Parallel() @@ -740,8 +738,6 @@ func TestHCL2ValueFromFlatmap(t *testing.T) { } for i, test := range tests { - i, test := i, test - t.Run(fmt.Sprintf("%d %#v as %#v", i, test.Flatmap, test.Type), func(t *testing.T) { t.Parallel() diff --git a/internal/configs/hcl2shim/paths_test.go b/internal/configs/hcl2shim/paths_test.go index 82ce4b68f..1b5c38373 100644 --- a/internal/configs/hcl2shim/paths_test.go +++ b/internal/configs/hcl2shim/paths_test.go @@ -211,7 +211,6 @@ func TestPathFromFlatmap(t *testing.T) { } for _, test := range tests { - test := test t.Run(fmt.Sprintf("%s as %#v", test.Flatmap, test.Type), func(t *testing.T) { t.Parallel() @@ -364,7 +363,6 @@ func TestRequiresReplace(t *testing.T) { }, }, } { - tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() @@ -413,8 +411,6 @@ func TestFlatmapKeyFromPath(t *testing.T) { attr: "attr.key.obj_attr.0.force_new", }, } { - i, tc := i, tc - t.Run(strconv.Itoa(i), func(t *testing.T) { t.Parallel() diff --git a/internal/configs/hcl2shim/values_equiv_test.go b/internal/configs/hcl2shim/values_equiv_test.go index bfbb41ec1..35b40fe45 100644 --- a/internal/configs/hcl2shim/values_equiv_test.go +++ b/internal/configs/hcl2shim/values_equiv_test.go @@ -419,7 +419,6 @@ func TestValuesSDKEquivalent(t *testing.T) { } for _, test := range tests { - test := test t.Run(fmt.Sprintf("%#v ≈ %#v", test.A, test.B), func(t *testing.T) { t.Parallel() diff --git a/internal/configs/hcl2shim/values_test.go b/internal/configs/hcl2shim/values_test.go index e063bbed9..f14d76882 100644 --- a/internal/configs/hcl2shim/values_test.go +++ b/internal/configs/hcl2shim/values_test.go @@ -241,7 +241,6 @@ func TestConfigValueFromHCL2Block(t *testing.T) { } for _, test := range tests { - test := test t.Run(fmt.Sprintf("%#v", test.Input), func(t *testing.T) { t.Parallel() @@ -334,7 +333,6 @@ func TestConfigValueFromHCL2(t *testing.T) { } for _, test := range tests { - test := test t.Run(fmt.Sprintf("%#v", test.Input), func(t *testing.T) { t.Parallel() @@ -423,7 +421,6 @@ func TestHCL2ValueFromConfigValue(t *testing.T) { } for _, test := range tests { - test := test t.Run(fmt.Sprintf("%#v", test.Input), func(t *testing.T) { t.Parallel() diff --git a/internal/errorshim/error_join_shim.go b/internal/errorshim/error_join_shim.go deleted file mode 100644 index b7371af82..000000000 --- a/internal/errorshim/error_join_shim.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -// TODO: Once Go 1.20 is the minimum supported version delete this package, replace all usages with `errors` package -// - https://github.com/hashicorp/terraform-plugin-testing/issues/99 -package errorshim - -// Copied from -> https://cs.opensource.google/go/go/+/refs/tags/go1.20.2:src/errors/join.go -func Join(errs ...error) error { - n := 0 - for _, err := range errs { - if err != nil { - n++ - } - } - if n == 0 { - return nil - } - e := &joinError{ - errs: make([]error, 0, n), - } - for _, err := range errs { - if err != nil { - e.errs = append(e.errs, err) - } - } - return e -} - -type joinError struct { - errs []error -} - -func (e *joinError) Error() string { - var b []byte - for i, err := range e.errs { - if i > 0 { - b = append(b, '\n') - } - b = append(b, err.Error()...) - } - return string(b) -} - -func (e *joinError) Unwrap() []error { - return e.errs -} diff --git a/internal/logging/context.go b/internal/logging/context.go index 0fe8002aa..5a3108451 100644 --- a/internal/logging/context.go +++ b/internal/logging/context.go @@ -11,22 +11,6 @@ import ( testing "github.com/mitchellh/go-testing-interface" ) -// InitContext creates SDK logger contexts when the provider is running in -// "production" (not under acceptance testing). The incoming context will -// already have the root SDK logger and root provider logger setup from -// terraform-plugin-go tf5server RPC handlers. -func InitContext(ctx context.Context) context.Context { - ctx = tfsdklog.NewSubsystem(ctx, SubsystemHelperSchema, - // All calls are through the HelperSchema* helper functions - tfsdklog.WithAdditionalLocationOffset(1), - tfsdklog.WithLevelFromEnv(EnvTfLogSdkHelperSchema), - // Propagate tf_req_id, tf_rpc, etc. fields - tfsdklog.WithRootFields(), - ) - - return ctx -} - // InitTestContext registers the terraform-plugin-log/tfsdklog test sink, // configures the standard library log package, and creates SDK logger // contexts. The incoming context is expected to be devoid of logging setup. diff --git a/internal/logging/context_test.go b/internal/logging/context_test.go index 1d1e85e3d..833b7b544 100644 --- a/internal/logging/context_test.go +++ b/internal/logging/context_test.go @@ -15,43 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-testing/internal/logging" ) -func TestInitContext(t *testing.T) { - t.Parallel() - - var output bytes.Buffer - - ctx := tfsdklogtest.RootLogger(context.Background(), &output) - - // Simulate root logger fields that would have been associated by - // terraform-plugin-go prior to the InitContext() call. - ctx = tfsdklog.SetField(ctx, "tf_rpc", "GetProviderSchema") - ctx = tfsdklog.SetField(ctx, "tf_req_id", "123-testing-123") - - ctx = logging.InitContext(ctx) - - logging.HelperSchemaTrace(ctx, "test message") - - entries, err := tfsdklogtest.MultilineJSONDecode(&output) - - if err != nil { - t.Fatalf("unable to read multiple line JSON: %s", err) - } - - expectedEntries := []map[string]interface{}{ - { - "@level": "trace", - "@message": "test message", - "@module": "sdk.helper_schema", - "tf_rpc": "GetProviderSchema", - "tf_req_id": "123-testing-123", - }, - } - - if diff := cmp.Diff(entries, expectedEntries); diff != "" { - t.Errorf("unexpected difference: %s", diff) - } -} - func TestTestNameContext(t *testing.T) { t.Parallel() diff --git a/internal/logging/environment_variables.go b/internal/logging/environment_variables.go index 2ffc73eee..846cf67dd 100644 --- a/internal/logging/environment_variables.go +++ b/internal/logging/environment_variables.go @@ -19,9 +19,4 @@ const ( // level of SDK helper/resource loggers. Infers root SDK logging level, if // unset. EnvTfLogSdkHelperResource = "TF_LOG_SDK_HELPER_RESOURCE" - - // EnvTfLogSdkHelperSchema is an environment variable that sets the logging - // level of SDK helper/schema loggers. Infers root SDK logging level, if - // unset. - EnvTfLogSdkHelperSchema = "TF_LOG_SDK_HELPER_SCHEMA" ) diff --git a/internal/logging/helper_schema.go b/internal/logging/helper_schema.go deleted file mode 100644 index 0ecf6bf2e..000000000 --- a/internal/logging/helper_schema.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package logging - -import ( - "context" - - "github.com/hashicorp/terraform-plugin-log/tfsdklog" -) - -const ( - // SubsystemHelperSchema is the tfsdklog subsystem name for helper/schema. - SubsystemHelperSchema = "helper_schema" -) - -// HelperSchemaDebug emits a helper/schema subsystem log at DEBUG level. -func HelperSchemaDebug(ctx context.Context, msg string, additionalFields ...map[string]interface{}) { - tfsdklog.SubsystemDebug(ctx, SubsystemHelperSchema, msg, additionalFields...) -} - -// HelperSchemaError emits a helper/schema subsystem log at ERROR level. -func HelperSchemaError(ctx context.Context, msg string, additionalFields ...map[string]interface{}) { - tfsdklog.SubsystemError(ctx, SubsystemHelperSchema, msg, additionalFields...) -} - -// HelperSchemaTrace emits a helper/schema subsystem log at TRACE level. -func HelperSchemaTrace(ctx context.Context, msg string, additionalFields ...map[string]interface{}) { - tfsdklog.SubsystemTrace(ctx, SubsystemHelperSchema, msg, additionalFields...) -} - -// HelperSchemaWarn emits a helper/schema subsystem log at WARN level. -func HelperSchemaWarn(ctx context.Context, msg string, additionalFields ...map[string]interface{}) { - tfsdklog.SubsystemWarn(ctx, SubsystemHelperSchema, msg, additionalFields...) -} diff --git a/internal/logging/helper_schema_test.go b/internal/logging/helper_schema_test.go deleted file mode 100644 index fea9fc7e3..000000000 --- a/internal/logging/helper_schema_test.go +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package logging_test - -import ( - "bytes" - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-log/tfsdklogtest" - - "github.com/hashicorp/terraform-plugin-testing/internal/logging" -) - -func TestHelperSchemaDebug(t *testing.T) { - t.Parallel() - - var output bytes.Buffer - - ctx := tfsdklogtest.RootLogger(context.Background(), &output) - ctx = logging.InitContext(ctx) - - logging.HelperSchemaDebug(ctx, "test message") - - entries, err := tfsdklogtest.MultilineJSONDecode(&output) - - if err != nil { - t.Fatalf("unable to read multiple line JSON: %s", err) - } - - expectedEntries := []map[string]interface{}{ - { - "@level": "debug", - "@message": "test message", - "@module": "sdk.helper_schema", - }, - } - - if diff := cmp.Diff(entries, expectedEntries); diff != "" { - t.Errorf("unexpected difference: %s", diff) - } -} - -func TestHelperSchemaError(t *testing.T) { - t.Parallel() - - var output bytes.Buffer - - ctx := tfsdklogtest.RootLogger(context.Background(), &output) - ctx = logging.InitContext(ctx) - - logging.HelperSchemaError(ctx, "test message") - - entries, err := tfsdklogtest.MultilineJSONDecode(&output) - - if err != nil { - t.Fatalf("unable to read multiple line JSON: %s", err) - } - - expectedEntries := []map[string]interface{}{ - { - "@level": "error", - "@message": "test message", - "@module": "sdk.helper_schema", - }, - } - - if diff := cmp.Diff(entries, expectedEntries); diff != "" { - t.Errorf("unexpected difference: %s", diff) - } -} - -func TestHelperSchemaTrace(t *testing.T) { - t.Parallel() - - var output bytes.Buffer - - ctx := tfsdklogtest.RootLogger(context.Background(), &output) - ctx = logging.InitContext(ctx) - - logging.HelperSchemaTrace(ctx, "test message") - - entries, err := tfsdklogtest.MultilineJSONDecode(&output) - - if err != nil { - t.Fatalf("unable to read multiple line JSON: %s", err) - } - - expectedEntries := []map[string]interface{}{ - { - "@level": "trace", - "@message": "test message", - "@module": "sdk.helper_schema", - }, - } - - if diff := cmp.Diff(entries, expectedEntries); diff != "" { - t.Errorf("unexpected difference: %s", diff) - } -} - -func TestHelperSchemaWarn(t *testing.T) { - t.Parallel() - - var output bytes.Buffer - - ctx := tfsdklogtest.RootLogger(context.Background(), &output) - ctx = logging.InitContext(ctx) - - logging.HelperSchemaWarn(ctx, "test message") - - entries, err := tfsdklogtest.MultilineJSONDecode(&output) - - if err != nil { - t.Fatalf("unable to read multiple line JSON: %s", err) - } - - expectedEntries := []map[string]interface{}{ - { - "@level": "warn", - "@message": "test message", - "@module": "sdk.helper_schema", - }, - } - - if diff := cmp.Diff(entries, expectedEntries); diff != "" { - t.Errorf("unexpected difference: %s", diff) - } -} diff --git a/internal/plugintest/util.go b/internal/plugintest/util.go index acccb3bcf..be187a01b 100644 --- a/internal/plugintest/util.go +++ b/internal/plugintest/util.go @@ -6,6 +6,7 @@ package plugintest import ( "fmt" "io" + "io/fs" "os" "path" "path/filepath" @@ -125,7 +126,13 @@ func CopyDir(src, dest, baseDirName string) error { continue } - if dirEntry.IsDir() { + fi, err := dirEntry.Info() + + if err != nil { + return fmt.Errorf("unable to get dir entry info: %w", err) + } + + if dirEntry.IsDir() || fi.Mode()&fs.ModeSymlink == fs.ModeSymlink { if err = CopyDir(srcFilepath, destFilepath, baseDirName); err != nil { return fmt.Errorf("unable to copy directory: %w", err) } diff --git a/internal/plugintest/working_dir.go b/internal/plugintest/working_dir.go index 9ee2046f2..6494125f9 100644 --- a/internal/plugintest/working_dir.go +++ b/internal/plugintest/working_dir.go @@ -5,21 +5,22 @@ package plugintest import ( "context" - "encoding/json" "fmt" - "os" - "path/filepath" - "github.com/hashicorp/terraform-exec/tfexec" tfjson "github.com/hashicorp/terraform-json" + "io" + "os" + "path/filepath" + "github.com/hashicorp/terraform-plugin-testing/config" "github.com/hashicorp/terraform-plugin-testing/internal/logging" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" ) const ( - ConfigFileName = "terraform_plugin_test.tf" - ConfigFileNameJSON = ConfigFileName + ".json" - PlanFileName = "tfplan" + ConfigFileName = "terraform_plugin_test.tf" + PlanFileName = "tfplan" + QueryFileName = "terraform_plugin_test.tfquery.hcl" ) // WorkingDir represents a distinct working directory that can be used for @@ -36,6 +37,10 @@ type WorkingDir struct { // was stored; empty until SetConfig is called. configFilename string + // queryFilename is the full filename where the latest query configuration + // was stored; empty until SetQuery is called. + queryFilename string + // tf is the instance of tfexec.Terraform used for running Terraform commands tf *tfexec.Terraform @@ -82,29 +87,145 @@ func (wd *WorkingDir) GetHelper() *Helper { // This must be called at least once before any call to Init, Plan, Apply, or // Destroy to establish the configuration. Any previously-set configuration is // discarded and any saved plan is cleared. -func (wd *WorkingDir) SetConfig(ctx context.Context, cfg string) error { +func (wd *WorkingDir) SetConfig(ctx context.Context, cfg teststep.Config, vars config.Variables) error { + // Remove old config and variables files first + d, err := os.Open(wd.baseDir) + + if err != nil { + return err + } + + defer d.Close() + + fi, err := d.Readdir(-1) + + if err != nil { + return err + } + + for _, file := range fi { + if file.Mode().IsRegular() { + if filepath.Ext(file.Name()) == ".tf" || filepath.Ext(file.Name()) == ".json" || filepath.Ext(file.Name()) == ".tfquery.hcl" { + err = os.Remove(filepath.Join(d.Name(), file.Name())) + + if err != nil && !os.IsNotExist(err) { + return err + } + } + } + } + logging.HelperResourceTrace(ctx, "Setting Terraform configuration", map[string]any{logging.KeyTestTerraformConfiguration: cfg}) outFilename := filepath.Join(wd.baseDir, ConfigFileName) - rmFilename := filepath.Join(wd.baseDir, ConfigFileNameJSON) - bCfg := []byte(cfg) - if json.Valid(bCfg) { - outFilename, rmFilename = rmFilename, outFilename + + // This file has to be written otherwise wd.Init() will return an error. + err = os.WriteFile(outFilename, nil, 0700) + + if err != nil { + return err + } + + // wd.configFilename must be set otherwise wd.Init() will return an error. + wd.configFilename = outFilename + + // Write configuration + if cfg != nil { + err = cfg.Write(ctx, wd.baseDir) + + if err != nil { + return err + } + } + + //Write configuration variables + err = vars.Write(wd.baseDir) + + if err != nil { + return err } - if err := os.Remove(rmFilename); err != nil && !os.IsNotExist(err) { - return fmt.Errorf("unable to remove %q: %w", rmFilename, err) + + // Changing configuration invalidates any saved plan. + err = wd.ClearPlan(ctx) + + if err != nil { + return err } - err := os.WriteFile(outFilename, bCfg, 0700) + + return nil +} + +// SetQuery sets a new query configuration for the working directory. +// +// This must be called at least once before any call to Init or Query Destroy +// to establish the query configuration. Any previously-set configuration is +// discarded and any saved plan is cleared. +func (wd *WorkingDir) SetQuery(ctx context.Context, cfg teststep.Config, vars config.Variables) error { + // Remove old config and variables files first + d, err := os.Open(wd.baseDir) + if err != nil { return err } + + defer d.Close() + + fi, err := d.Readdir(-1) + + if err != nil { + return err + } + + for _, file := range fi { + if file.Mode().IsRegular() { + if filepath.Ext(file.Name()) == ".warioform" || filepath.Ext(file.Name()) == ".json" || filepath.Ext(file.Name()) == ".tfquery.hcl" { + err = os.Remove(filepath.Join(d.Name(), file.Name())) + + if err != nil && !os.IsNotExist(err) { + return err + } + } + } + } + + logging.HelperResourceTrace(ctx, "Setting Terraform query configuration", map[string]any{logging.KeyTestTerraformConfiguration: cfg}) + + outFilename := filepath.Join(wd.baseDir, QueryFileName) + + // This file has to be written otherwise wd.Init() will return an error. + err = os.WriteFile(outFilename, nil, 0700) + + if err != nil { + return err + } + + // wd.configFilename must be set otherwise wd.Init() will return an error. + wd.queryFilename = outFilename wd.configFilename = outFilename + // Write configuration + if cfg != nil { + err = cfg.WriteQuery(ctx, wd.baseDir) + + if err != nil { + return err + } + } + + //Write configuration variables + err = vars.Write(wd.baseDir) + + if err != nil { + return err + } + // Changing configuration invalidates any saved plan. err = wd.ClearPlan(ctx) + if err != nil { return err } + return nil } @@ -131,6 +252,40 @@ func (wd *WorkingDir) ClearState(ctx context.Context) error { return nil } +func (wd *WorkingDir) CopyState(ctx context.Context, src string) error { + srcState, err := os.Open(src) + if err != nil { + return fmt.Errorf("failed to open statefile for read: %w", err) + } + + defer srcState.Close() + + dstState, err := os.Create(filepath.Join(wd.baseDir, "terraform.tfstate")) + if err != nil { + return fmt.Errorf("failed to open statefile for write: %w", err) + } + + defer dstState.Close() + + buf := make([]byte, 1024) + for { + n, err := srcState.Read(buf) + if err != nil { + if err == io.EOF { + break + } + return fmt.Errorf("failed to read from statefile: %w", err) + } + + _, err = dstState.Write(buf[:n]) + if err != nil { + return fmt.Errorf("failed to write to statefile: %w", err) + } + } + + return nil +} + // ClearPlan deletes any saved plan present in the working directory. func (wd *WorkingDir) ClearPlan(ctx context.Context) error { logging.HelperResourceTrace(ctx, "Clearing Terraform plan") @@ -180,10 +335,13 @@ func (wd *WorkingDir) planFilename() string { // CreatePlan runs "terraform plan" to create a saved plan file, which if successful // will then be used for the next call to Apply. -func (wd *WorkingDir) CreatePlan(ctx context.Context) error { +func (wd *WorkingDir) CreatePlan(ctx context.Context, opts ...tfexec.PlanOption) error { logging.HelperResourceTrace(ctx, "Calling Terraform CLI plan command") - hasChanges, err := wd.tf.Plan(context.Background(), tfexec.Reattach(wd.reattachInfo), tfexec.Refresh(false), tfexec.Out(PlanFileName)) + opts = append(opts, tfexec.Reattach(wd.reattachInfo)) + opts = append(opts, tfexec.Out(PlanFileName)) + + hasChanges, err := wd.tf.Plan(context.Background(), opts...) logging.HelperResourceTrace(ctx, "Called Terraform CLI plan command") @@ -208,42 +366,13 @@ func (wd *WorkingDir) CreatePlan(ctx context.Context) error { return nil } -// CreateDestroyPlan runs "terraform plan -destroy" to create a saved plan -// file, which if successful will then be used for the next call to Apply. -func (wd *WorkingDir) CreateDestroyPlan(ctx context.Context) error { - logging.HelperResourceTrace(ctx, "Calling Terraform CLI plan -destroy command") - - hasChanges, err := wd.tf.Plan(context.Background(), tfexec.Reattach(wd.reattachInfo), tfexec.Refresh(false), tfexec.Out(PlanFileName), tfexec.Destroy(true)) - - logging.HelperResourceTrace(ctx, "Called Terraform CLI plan -destroy command") - - if err != nil { - return err - } - - if !hasChanges { - logging.HelperResourceTrace(ctx, "Created destroy plan with no changes") - - return nil - } - - stdout, err := wd.SavedPlanRawStdout(ctx) - - if err != nil { - return fmt.Errorf("error retrieving formatted plan output: %w", err) - } - - logging.HelperResourceTrace(ctx, "Created destroy plan with changes", map[string]any{logging.KeyTestTerraformPlan: stdout}) - - return nil -} - // Apply runs "terraform apply". If CreatePlan has previously completed // successfully and the saved plan has not been cleared in the meantime then // this will apply the saved plan. Otherwise, it will implicitly create a new // plan and apply it. -func (wd *WorkingDir) Apply(ctx context.Context) error { +func (wd *WorkingDir) Apply(ctx context.Context, opts ...tfexec.ApplyOption) error { args := []tfexec.ApplyOption{tfexec.Reattach(wd.reattachInfo), tfexec.Refresh(false)} + args = append(args, opts...) if wd.HasSavedPlan() { args = append(args, tfexec.DirOrPlan(PlanFileName)) } @@ -279,6 +408,17 @@ func (wd *WorkingDir) HasSavedPlan() bool { return err == nil } +// RemoveResource removes a resource from state. +func (wd *WorkingDir) RemoveResource(ctx context.Context, address string) error { + logging.HelperResourceTrace(ctx, "Calling Terraform CLI state rm command") + + err := wd.tf.StateRm(context.Background(), address) + + logging.HelperResourceTrace(ctx, "Called Terraform CLI state rm command") + + return err +} + // SavedPlan returns an object describing the current saved plan file, if any. // // If no plan is saved or if the plan file cannot be read, SavedPlan returns @@ -290,7 +430,7 @@ func (wd *WorkingDir) SavedPlan(ctx context.Context) (*tfjson.Plan, error) { logging.HelperResourceTrace(ctx, "Calling Terraform CLI show command for JSON plan") - plan, err := wd.tf.ShowPlanFile(context.Background(), wd.planFilename(), tfexec.Reattach(wd.reattachInfo)) + plan, err := wd.tf.ShowPlanFile(context.Background(), wd.planFilename(), tfexec.Reattach(wd.reattachInfo), tfexec.JSONNumber(true)) logging.HelperResourceTrace(ctx, "Calling Terraform CLI show command for JSON plan") @@ -333,6 +473,10 @@ func (wd *WorkingDir) State(ctx context.Context) (*tfjson.State, error) { return state, err } +func (wd *WorkingDir) StateFilePath() string { + return filepath.Join(wd.baseDir, "terraform.tfstate") +} + // Import runs terraform import func (wd *WorkingDir) Import(ctx context.Context, resource, id string) error { logging.HelperResourceTrace(ctx, "Calling Terraform CLI import command") @@ -378,3 +522,43 @@ func (wd *WorkingDir) Schemas(ctx context.Context) (*tfjson.ProviderSchemas, err return providerSchemas, err } + +func (wd *WorkingDir) Query(ctx context.Context) ([]tfjson.LogMsg, error) { + var messages []tfjson.LogMsg + var diags []tfjson.LogMsg + + logging.HelperResourceTrace(ctx, "Calling Terraform CLI providers query command") + + args := []tfexec.QueryOption{tfexec.Reattach(wd.reattachInfo)} + + logs, err := wd.tf.QueryJSON(context.Background(), args...) + + if err != nil { + return nil, fmt.Errorf("running terraform query command: %w", err) + } + + for msg := range logs { + if msg.Msg == nil { + continue + } + + if msg.Err != nil { + return nil, fmt.Errorf("retrieving message: %w", msg.Err) + } + + if msg.Msg.Level() == tfjson.Error { + // TODO reimplement missing .tf config error + diags = append(diags, msg.Msg) + continue + } + messages = append(messages, msg.Msg) + } + + if len(diags) > 0 { + return nil, fmt.Errorf("running terraform query command returned diagnostics: %+v", diags) + } + + logging.HelperResourceTrace(ctx, "Called Terraform CLI providers query command") + + return messages, nil +} diff --git a/internal/testing/doc.go b/internal/testing/doc.go new file mode 100644 index 000000000..763953104 --- /dev/null +++ b/internal/testing/doc.go @@ -0,0 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package testing contains functionality and helpers for unit testing within +// this Go module. +package testing diff --git a/internal/testing/testprovider/datasource.go b/internal/testing/testprovider/datasource.go new file mode 100644 index 000000000..66b29e26b --- /dev/null +++ b/internal/testing/testprovider/datasource.go @@ -0,0 +1,38 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/datasource" +) + +var _ datasource.DataSource = DataSource{} + +type DataSource struct { + ReadResponse *datasource.ReadResponse + SchemaResponse *datasource.SchemaResponse + ValidateConfigResponse *datasource.ValidateConfigResponse +} + +func (d DataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + if d.ReadResponse != nil { + resp.Diagnostics = d.ReadResponse.Diagnostics + resp.State = d.ReadResponse.State + } +} + +func (d DataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + if d.SchemaResponse != nil { + resp.Diagnostics = d.SchemaResponse.Diagnostics + resp.Schema = d.SchemaResponse.Schema + } +} + +func (d DataSource) ValidateConfig(ctx context.Context, req datasource.ValidateConfigRequest, resp *datasource.ValidateConfigResponse) { + if d.ValidateConfigResponse != nil { + resp.Diagnostics = d.ValidateConfigResponse.Diagnostics + } +} diff --git a/internal/testing/testprovider/doc.go b/internal/testing/testprovider/doc.go new file mode 100644 index 000000000..e997e521c --- /dev/null +++ b/internal/testing/testprovider/doc.go @@ -0,0 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package testprovider is a declarative provider for implementing unit testing +// within this Go module. +package testprovider diff --git a/internal/testing/testprovider/list_resource.go b/internal/testing/testprovider/list_resource.go new file mode 100644 index 000000000..630723b87 --- /dev/null +++ b/internal/testing/testprovider/list_resource.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/list" +) + +var _ list.ListResource = ListResource{} + +type ListResource struct { + IncludeResource bool + SchemaResponse *list.SchemaResponse + ListResultsStream *list.ListResultsStream + ValidateListConfigResponse *list.ValidateListConfigResponse +} + +func (r ListResource) Schema(ctx context.Context, req list.SchemaRequest, resp *list.SchemaResponse) { + if r.SchemaResponse != nil { + resp.Diagnostics = r.SchemaResponse.Diagnostics + resp.Schema = r.SchemaResponse.Schema + } +} +func (r ListResource) List(ctx context.Context, req list.ListRequest, stream *list.ListResultsStream) { + req.IncludeResource = r.IncludeResource + stream.Results = r.ListResultsStream.Results +} diff --git a/internal/testing/testprovider/provider.go b/internal/testing/testprovider/provider.go new file mode 100644 index 000000000..f30883bcc --- /dev/null +++ b/internal/testing/testprovider/provider.go @@ -0,0 +1,87 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/datasource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/list" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/provider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" +) + +var _ provider.Provider = Provider{} + +// Provider is a declarative provider implementation for unit testing in this +// Go module. +type Provider struct { + ConfigureResponse *provider.ConfigureResponse + DataSources map[string]DataSource + ListResources map[string]ListResource + Resources map[string]Resource + SchemaResponse *provider.SchemaResponse + StopResponse *provider.StopResponse + ValidateConfigResponse *provider.ValidateConfigResponse +} + +func (p Provider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { + if p.ConfigureResponse != nil { + resp.Diagnostics = p.ConfigureResponse.Diagnostics + } +} + +func (p Provider) DataSourcesMap() map[string]datasource.DataSource { + datasources := make(map[string]datasource.DataSource, len(p.DataSources)) + + for typeName, d := range p.DataSources { + datasources[typeName] = d + } + + return datasources +} + +func (p Provider) ListResourcesMap() map[string]list.ListResource { + listResources := make(map[string]list.ListResource, len(p.ListResources)) + + for typeName, d := range p.ListResources { + listResources[typeName] = d + } + + return listResources +} + +func (p Provider) ResourcesMap() map[string]resource.Resource { + resources := make(map[string]resource.Resource, len(p.Resources)) + + for typeName, d := range p.Resources { + resources[typeName] = d + } + + return resources +} + +func (p Provider) Stop(ctx context.Context, req provider.StopRequest, resp *provider.StopResponse) { + if p.StopResponse != nil { + resp.Error = p.StopResponse.Error + } +} + +func (p Provider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) { + if p.SchemaResponse != nil { + resp.Diagnostics = p.SchemaResponse.Diagnostics + resp.Schema = p.SchemaResponse.Schema + } + + resp.Schema = &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{}, + } +} + +func (p Provider) ValidateConfig(ctx context.Context, req provider.ValidateConfigRequest, resp *provider.ValidateConfigResponse) { + if p.ValidateConfigResponse != nil { + resp.Diagnostics = p.ValidateConfigResponse.Diagnostics + } +} diff --git a/internal/testing/testprovider/provider_protov5.go b/internal/testing/testprovider/provider_protov5.go new file mode 100644 index 000000000..65b4ee5ef --- /dev/null +++ b/internal/testing/testprovider/provider_protov5.go @@ -0,0 +1,51 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/datasource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/provider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" +) + +var _ provider.Protov5Provider = Protov5Provider{} + +// Protov5Provider is a declarative provider implementation for unit testing in this +// Go module. The provider is unimplemented except for the Schema method. +type Protov5Provider struct { + ConfigureResponse *provider.Protov5ConfigureResponse + DataSources map[string]DataSource + Resources map[string]Resource + SchemaResponse *provider.Protov5SchemaResponse + StopResponse *provider.Protov5StopResponse + ValidateConfigResponse *provider.Protov5ValidateConfigResponse +} + +func (p Protov5Provider) Configure(ctx context.Context, req provider.Protov5ConfigureRequest, resp *provider.Protov5ConfigureResponse) { + +} + +func (p Protov5Provider) DataSourcesMap() map[string]datasource.DataSource { + return nil +} + +func (p Protov5Provider) ResourcesMap() map[string]resource.Resource { + return nil +} + +func (p Protov5Provider) Stop(ctx context.Context, req provider.Protov5StopRequest, resp *provider.Protov5StopResponse) { +} + +func (p Protov5Provider) Schema(ctx context.Context, req provider.Protov5SchemaRequest, resp *provider.Protov5SchemaResponse) { + resp.Schema = &tfprotov5.Schema{ + Block: &tfprotov5.SchemaBlock{}, + } +} + +func (p Protov5Provider) ValidateConfig(ctx context.Context, req provider.Protov5ValidateConfigRequest, resp *provider.Protov5ValidateConfigResponse) { +} diff --git a/internal/testing/testprovider/resource.go b/internal/testing/testprovider/resource.go new file mode 100644 index 000000000..8969f5ca9 --- /dev/null +++ b/internal/testing/testprovider/resource.go @@ -0,0 +1,100 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" +) + +var _ resource.Resource = Resource{} + +type Resource struct { + CreateResponse *resource.CreateResponse + DeleteResponse *resource.DeleteResponse + ImportStateResponse *resource.ImportStateResponse + + // Planning happens multiple ways during a single TestStep, so statically + // defining only the response is very problematic. + PlanChangeFunc func(context.Context, resource.PlanChangeRequest, *resource.PlanChangeResponse) + + ReadResponse *resource.ReadResponse + IdentitySchemaResponse *resource.IdentitySchemaResponse + SchemaResponse *resource.SchemaResponse + UpdateResponse *resource.UpdateResponse + UpgradeStateResponse *resource.UpgradeStateResponse + ValidateConfigResponse *resource.ValidateConfigResponse +} + +func (r Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + if r.CreateResponse != nil { + resp.Diagnostics = r.CreateResponse.Diagnostics + resp.NewState = r.CreateResponse.NewState + resp.NewIdentity = r.CreateResponse.NewIdentity + } +} + +func (r Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + if r.DeleteResponse != nil { + resp.Diagnostics = r.DeleteResponse.Diagnostics + } +} + +func (r Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + if r.ImportStateResponse != nil { + resp.Diagnostics = r.ImportStateResponse.Diagnostics + resp.State = r.ImportStateResponse.State + resp.Identity = r.ImportStateResponse.Identity + } +} + +func (r Resource) PlanChange(ctx context.Context, req resource.PlanChangeRequest, resp *resource.PlanChangeResponse) { + if r.PlanChangeFunc != nil { + r.PlanChangeFunc(ctx, req, resp) + } +} + +func (r Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + if r.ReadResponse != nil { + resp.Diagnostics = r.ReadResponse.Diagnostics + resp.NewState = r.ReadResponse.NewState + resp.NewIdentity = r.ReadResponse.NewIdentity + } +} + +func (r Resource) IdentitySchema(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + if r.IdentitySchemaResponse != nil { + resp.Diagnostics = r.IdentitySchemaResponse.Diagnostics + resp.Schema = r.IdentitySchemaResponse.Schema + } +} + +func (r Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + if r.SchemaResponse != nil { + resp.Diagnostics = r.SchemaResponse.Diagnostics + resp.Schema = r.SchemaResponse.Schema + } +} + +func (r Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + if r.UpdateResponse != nil { + resp.Diagnostics = r.UpdateResponse.Diagnostics + resp.NewState = r.UpdateResponse.NewState + resp.NewIdentity = r.UpdateResponse.NewIdentity + } +} + +func (r Resource) UpgradeState(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) { + if r.UpgradeStateResponse != nil { + resp.Diagnostics = r.UpgradeStateResponse.Diagnostics + resp.UpgradedState = r.UpgradeStateResponse.UpgradedState + } +} + +func (r Resource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + if r.ValidateConfigResponse != nil { + resp.Diagnostics = r.ValidateConfigResponse.Diagnostics + } +} diff --git a/internal/testing/testsdk/datasource/datasource.go b/internal/testing/testsdk/datasource/datasource.go new file mode 100644 index 000000000..12200fb0c --- /dev/null +++ b/internal/testing/testsdk/datasource/datasource.go @@ -0,0 +1,41 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package datasource + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +type DataSource interface { + Read(context.Context, ReadRequest, *ReadResponse) + Schema(context.Context, SchemaRequest, *SchemaResponse) + ValidateConfig(context.Context, ValidateConfigRequest, *ValidateConfigResponse) +} + +type ReadRequest struct { + Config tftypes.Value +} + +type ReadResponse struct { + Diagnostics []*tfprotov6.Diagnostic + State tftypes.Value +} + +type SchemaRequest struct{} + +type SchemaResponse struct { + Diagnostics []*tfprotov6.Diagnostic + Schema *tfprotov6.Schema +} + +type ValidateConfigRequest struct { + Config tftypes.Value +} + +type ValidateConfigResponse struct { + Diagnostics []*tfprotov6.Diagnostic +} diff --git a/internal/testing/testsdk/datasource/doc.go b/internal/testing/testsdk/datasource/doc.go new file mode 100644 index 000000000..6499dded5 --- /dev/null +++ b/internal/testing/testsdk/datasource/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package datasource provides testsdk handling of the data resource concept. +package datasource diff --git a/internal/testing/testsdk/doc.go b/internal/testing/testsdk/doc.go new file mode 100644 index 000000000..2ff15aaf5 --- /dev/null +++ b/internal/testing/testsdk/doc.go @@ -0,0 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package testsdk provides a lightweight terraform-plugin-go SDK for +// implementing unit testing within this Go module. +package testsdk diff --git a/internal/testing/testsdk/list/list_resource.go b/internal/testing/testsdk/list/list_resource.go new file mode 100644 index 000000000..1660d3426 --- /dev/null +++ b/internal/testing/testsdk/list/list_resource.go @@ -0,0 +1,56 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package list + +import ( + "context" + "iter" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +type ListResource interface { + Schema(context.Context, SchemaRequest, *SchemaResponse) + List(context.Context, ListRequest, *ListResultsStream) +} + +type ListRequest struct { + TypeName string + // Config is the configuration the user supplied for listing resource + // instances. + Config tftypes.Value + + // IncludeResource indicates whether the provider should populate the + // [ListResult.Resource] field. + IncludeResource bool + + // Limit specifies the maximum number of results that Terraform is + // expecting. + Limit int64 + + ResourceSchema *tfprotov6.Schema + ResourceIdentitySchema *tfprotov6.ResourceIdentitySchema +} + +type ListResultsStream struct { + Results iter.Seq[ListResult] +} + +type ListResult struct { + DisplayName string + Identity *tftypes.Value + Resource *tftypes.Value + Diagnostics []*tfprotov6.Diagnostic +} + +type ValidateListConfigResponse struct { +} + +type SchemaRequest struct{} + +type SchemaResponse struct { + Diagnostics []*tfprotov6.Diagnostic + Schema *tfprotov6.Schema +} diff --git a/internal/testing/testsdk/provider/doc.go b/internal/testing/testsdk/provider/doc.go new file mode 100644 index 000000000..d0457580a --- /dev/null +++ b/internal/testing/testsdk/provider/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package provider provides testsdk handling of the provider concept. +package provider diff --git a/internal/testing/testsdk/provider/provider.go b/internal/testing/testsdk/provider/provider.go new file mode 100644 index 000000000..4fcc6b4d3 --- /dev/null +++ b/internal/testing/testsdk/provider/provider.go @@ -0,0 +1,53 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/datasource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/list" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" +) + +type Provider interface { + Configure(context.Context, ConfigureRequest, *ConfigureResponse) + DataSourcesMap() map[string]datasource.DataSource + ListResourcesMap() map[string]list.ListResource + ResourcesMap() map[string]resource.Resource + Schema(context.Context, SchemaRequest, *SchemaResponse) + Stop(context.Context, StopRequest, *StopResponse) + ValidateConfig(context.Context, ValidateConfigRequest, *ValidateConfigResponse) +} + +type ConfigureRequest struct { + Config tftypes.Value +} + +type ConfigureResponse struct { + Diagnostics []*tfprotov6.Diagnostic +} + +type SchemaRequest struct{} + +type SchemaResponse struct { + Diagnostics []*tfprotov6.Diagnostic + Schema *tfprotov6.Schema +} + +type StopRequest struct{} + +type StopResponse struct { + Error error +} + +type ValidateConfigRequest struct { + Config tftypes.Value +} + +type ValidateConfigResponse struct { + Diagnostics []*tfprotov6.Diagnostic +} diff --git a/internal/testing/testsdk/provider/provider_protov5.go b/internal/testing/testsdk/provider/provider_protov5.go new file mode 100644 index 000000000..27f4f1659 --- /dev/null +++ b/internal/testing/testsdk/provider/provider_protov5.go @@ -0,0 +1,53 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/datasource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" +) + +type Protov5Provider interface { + Configure(context.Context, Protov5ConfigureRequest, *Protov5ConfigureResponse) + DataSourcesMap() map[string]datasource.DataSource + ResourcesMap() map[string]resource.Resource + Schema(context.Context, Protov5SchemaRequest, *Protov5SchemaResponse) + Stop(context.Context, Protov5StopRequest, *Protov5StopResponse) + ValidateConfig(context.Context, Protov5ValidateConfigRequest, *Protov5ValidateConfigResponse) +} + +type Protov5ConfigureRequest struct { + Config tftypes.Value +} + +type Protov5ConfigureResponse struct { + Diagnostics []*tfprotov6.Diagnostic +} + +type Protov5SchemaRequest struct{} + +type Protov5SchemaResponse struct { + Diagnostics []*tfprotov5.Diagnostic + Schema *tfprotov5.Schema +} + +type Protov5StopRequest struct{} + +type Protov5StopResponse struct { + Error error +} + +type Protov5ValidateConfigRequest struct { + Config tftypes.Value +} + +type Protov5ValidateConfigResponse struct { + Diagnostics []*tfprotov5.Diagnostic +} diff --git a/internal/testing/testsdk/providerserver/datasources.go b/internal/testing/testsdk/providerserver/datasources.go new file mode 100644 index 000000000..ea1c74068 --- /dev/null +++ b/internal/testing/testsdk/providerserver/datasources.go @@ -0,0 +1,24 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package providerserver + +import ( + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/datasource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/provider" +) + +func ProviderDataSource(p provider.Provider, typeName string) (datasource.DataSource, *tfprotov6.Diagnostic) { + d, ok := p.DataSourcesMap()[typeName] + + if !ok { + return nil, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Missing Data Source Type", + Detail: "The provider does not define the data source type: " + typeName, + } + } + + return d, nil +} diff --git a/internal/testing/testsdk/providerserver/doc.go b/internal/testing/testsdk/providerserver/doc.go new file mode 100644 index 000000000..edf1d2cdb --- /dev/null +++ b/internal/testing/testsdk/providerserver/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package providerserver provides testsdk handling of serving a provider. +package providerserver diff --git a/internal/testing/testsdk/providerserver/list_resources.go b/internal/testing/testsdk/providerserver/list_resources.go new file mode 100644 index 000000000..cd31ef1e0 --- /dev/null +++ b/internal/testing/testsdk/providerserver/list_resources.go @@ -0,0 +1,24 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package providerserver + +import ( + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/list" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/provider" +) + +func ProviderListResource(p provider.Provider, typeName string) (list.ListResource, *tfprotov6.Diagnostic) { + r, ok := p.ListResourcesMap()[typeName] + + if !ok { + return nil, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Missing List Resource Type", + Detail: "The provider does not define the list resource type: " + typeName, + } + } + + return r, nil +} diff --git a/internal/testing/testsdk/providerserver/providerserver.go b/internal/testing/testsdk/providerserver/providerserver.go new file mode 100644 index 000000000..0f14d0dac --- /dev/null +++ b/internal/testing/testsdk/providerserver/providerserver.go @@ -0,0 +1,1178 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package providerserver + +import ( + "context" + "errors" + "fmt" + "iter" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/datasource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/list" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/provider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" +) + +var _ tfprotov6.ProviderServer = ProviderServer{} + +// var _ tfprotov6.ProviderServerWithListResource = ProviderServer{} + +// NewProviderServer returns a lightweight protocol version 6 provider server +// for consumption with ProtoV6ProviderFactories. +func NewProviderServer(p provider.Provider) func() (tfprotov6.ProviderServer, error) { + return NewProviderServerWithError(p, nil) +} + +// NewProviderServerWithError returns a lightweight protocol version 6 provider +// server and an associated error for consumption with ProtoV6ProviderFactories. +func NewProviderServerWithError(p provider.Provider, err error) func() (tfprotov6.ProviderServer, error) { + providerServer := ProviderServer{ + Provider: p, + } + + return func() (tfprotov6.ProviderServer, error) { + return providerServer, err + } +} + +// ProviderServer is a lightweight protocol version 6 provider server which +// is assumed to be well-behaved, e.g. does not return gRPC errors. +// +// This implementation intends to reduce the heaviest parts of +// terraform-plugin-go based provider development: +// +// - Converting *tfprotov6.DynamicValue to tftypes.Value using schema +// - Splitting ApplyResourceChange into Create/Update/Delete calls +// - Set PlanResourceChange null config values of Computed attributes to unknown +// - Roundtrip UpgradeResourceState with equal schema version +// +// By default, the following data is copied automatically: +// +// - ApplyResourceChange (create): req.Config -> resp.NewState +// - ApplyResourceChange (create): req.PlannedIdentity -> resp.NewIdentity +// - ApplyResourceChange (delete): req.PlannedState -> resp.NewState +// - ApplyResourceChange (update): req.PlannedState -> resp.NewState +// - ApplyResourceChange (update): req.PlannedIdentity -> resp.NewIdentity +// - PlanResourceChange: req.ProposedNewState -> resp.PlannedState +// - PlanResourceChange: req.PriorIdentity -> resp.PlannedIdentity +// - ImportResourceState: req.Identity -> resp.ImportedResources[0].Identity +// - ReadDataSource: req.Config -> resp.State +// - ReadResource: req.CurrentState -> resp.NewState +// - ReadResource: req.CurrentIdentity -> resp.NewIdentity +type ProviderServer struct { + Provider provider.Provider +} + +func (s ProviderServer) MoveResourceState(ctx context.Context, req *tfprotov6.MoveResourceStateRequest) (*tfprotov6.MoveResourceStateResponse, error) { + return &tfprotov6.MoveResourceStateResponse{}, nil +} + +func (s ProviderServer) GetMetadata(ctx context.Context, request *tfprotov6.GetMetadataRequest) (*tfprotov6.GetMetadataResponse, error) { + resp := &tfprotov6.GetMetadataResponse{ + // Functions and ephemeral resources not supported in this test SDK + Functions: []tfprotov6.FunctionMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, + + ServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + } + + for typeName := range s.Provider.DataSourcesMap() { + resp.DataSources = append(resp.DataSources, tfprotov6.DataSourceMetadata{ + TypeName: typeName, + }) + } + + for typeName := range s.Provider.ListResourcesMap() { + resp.ListResources = append(resp.ListResources, tfprotov6.ListResourceMetadata{ + TypeName: typeName, + }) + } + + for typeName := range s.Provider.ResourcesMap() { + resp.Resources = append(resp.Resources, tfprotov6.ResourceMetadata{ + TypeName: typeName, + }) + } + + return resp, nil +} + +func (s ProviderServer) ApplyResourceChange(ctx context.Context, req *tfprotov6.ApplyResourceChangeRequest) (*tfprotov6.ApplyResourceChangeResponse, error) { + resp := &tfprotov6.ApplyResourceChangeResponse{} + + r, diag := ProviderResource(s.Provider, req.TypeName) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + schemaReq := resource.SchemaRequest{} + schemaResp := &resource.SchemaResponse{} + + r.Schema(ctx, schemaReq, schemaResp) + + resp.Diagnostics = schemaResp.Diagnostics + + if len(resp.Diagnostics) > 0 { + return resp, nil + } + + config, diag := DynamicValueToValue(schemaResp.Schema, req.Config) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + plannedState, diag := DynamicValueToValue(schemaResp.Schema, req.PlannedState) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + priorState, diag := DynamicValueToValue(schemaResp.Schema, req.PriorState) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + // Copy over identity if it's supported + identitySchemaReq := resource.IdentitySchemaRequest{} + identitySchemaResp := &resource.IdentitySchemaResponse{} + + r.IdentitySchema(ctx, identitySchemaReq, identitySchemaResp) + + resp.Diagnostics = identitySchemaResp.Diagnostics + + if len(resp.Diagnostics) > 0 { + return resp, nil + } + + var plannedIdentity *tftypes.Value + if identitySchemaResp.Schema != nil && req.PlannedIdentity != nil { + plannedIdentityVal, diag := IdentityDynamicValueToValue(identitySchemaResp.Schema, req.PlannedIdentity.IdentityData) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + plannedIdentity = &plannedIdentityVal + } + + var newIdentity *tftypes.Value + if priorState.IsNull() { + createReq := resource.CreateRequest{ + Config: config, + PlannedIdentity: plannedIdentity, + } + createResp := &resource.CreateResponse{ + NewState: config.Copy(), + NewIdentity: plannedIdentity, + } + + r.Create(ctx, createReq, createResp) + + resp.Diagnostics = createResp.Diagnostics + + if len(resp.Diagnostics) > 0 { + return resp, nil + } + + newState, diag := ValuetoDynamicValue(schemaResp.Schema, createResp.NewState) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + resp.NewState = newState + newIdentity = createResp.NewIdentity + } else if plannedState.IsNull() { + deleteReq := resource.DeleteRequest{ + PriorState: priorState, + } + deleteResp := &resource.DeleteResponse{} + + r.Delete(ctx, deleteReq, deleteResp) + + resp.Diagnostics = deleteResp.Diagnostics + + if len(resp.Diagnostics) > 0 { + return resp, nil + } + + resp.NewState = req.PlannedState + } else { + updateReq := resource.UpdateRequest{ + Config: config, + PlannedState: plannedState, + PlannedIdentity: plannedIdentity, + PriorState: priorState, + } + updateResp := &resource.UpdateResponse{ + NewState: plannedState.Copy(), + NewIdentity: plannedIdentity, + } + + r.Update(ctx, updateReq, updateResp) + + resp.Diagnostics = updateResp.Diagnostics + + if len(resp.Diagnostics) > 0 { + return resp, nil + } + + newState, diag := ValuetoDynamicValue(schemaResp.Schema, updateResp.NewState) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + resp.NewState = newState + newIdentity = updateResp.NewIdentity + } + + if newIdentity != nil { + newIdentityVal, diag := IdentityValuetoDynamicValue(identitySchemaResp.Schema, *newIdentity) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + resp.NewIdentity = &tfprotov6.ResourceIdentityData{ + IdentityData: newIdentityVal, + } + } + + return resp, nil +} + +func (s ProviderServer) ConfigureProvider(ctx context.Context, req *tfprotov6.ConfigureProviderRequest) (*tfprotov6.ConfigureProviderResponse, error) { + resp := &tfprotov6.ConfigureProviderResponse{} + + schemaReq := provider.SchemaRequest{} + schemaResp := &provider.SchemaResponse{} + + s.Provider.Schema(ctx, schemaReq, schemaResp) + + resp.Diagnostics = schemaResp.Diagnostics + + if len(resp.Diagnostics) > 0 { + return resp, nil + } + + config, diag := DynamicValueToValue(schemaResp.Schema, req.Config) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + configureReq := provider.ConfigureRequest{ + Config: config, + } + configureResp := &provider.ConfigureResponse{} + + s.Provider.Configure(ctx, configureReq, configureResp) + + resp.Diagnostics = configureResp.Diagnostics + + return resp, nil +} + +func (s ProviderServer) GetProviderSchema(ctx context.Context, req *tfprotov6.GetProviderSchemaRequest) (*tfprotov6.GetProviderSchemaResponse, error) { + providerReq := provider.SchemaRequest{} + providerResp := &provider.SchemaResponse{} + + s.Provider.Schema(ctx, providerReq, providerResp) + + resp := &tfprotov6.GetProviderSchemaResponse{ + // Functions and ephemeral resources not supported in this test SDK + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Diagnostics: providerResp.Diagnostics, + ListResourceSchemas: map[string]*tfprotov6.Schema{}, + Provider: providerResp.Schema, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + ServerCapabilities: &tfprotov6.ServerCapabilities{ + PlanDestroy: true, + }, + } + + for typeName, d := range s.Provider.DataSourcesMap() { + schemaReq := datasource.SchemaRequest{} + schemaResp := &datasource.SchemaResponse{} + + d.Schema(ctx, schemaReq, schemaResp) + + resp.Diagnostics = append(resp.Diagnostics, schemaResp.Diagnostics...) + + resp.DataSourceSchemas[typeName] = schemaResp.Schema + } + + for typeName, l := range s.Provider.ListResourcesMap() { + schemaReq := list.SchemaRequest{} + schemaResp := &list.SchemaResponse{} + + l.Schema(ctx, schemaReq, schemaResp) + + resp.Diagnostics = append(resp.Diagnostics, schemaResp.Diagnostics...) + + resp.ListResourceSchemas[typeName] = schemaResp.Schema + } + + for typeName, r := range s.Provider.ResourcesMap() { + schemaReq := resource.SchemaRequest{} + schemaResp := &resource.SchemaResponse{} + + r.Schema(ctx, schemaReq, schemaResp) + + resp.Diagnostics = append(resp.Diagnostics, schemaResp.Diagnostics...) + + resp.ResourceSchemas[typeName] = schemaResp.Schema + } + + return resp, nil +} + +func (s ProviderServer) GetResourceIdentitySchemas(ctx context.Context, req *tfprotov6.GetResourceIdentitySchemasRequest) (*tfprotov6.GetResourceIdentitySchemasResponse, error) { + resp := &tfprotov6.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov6.ResourceIdentitySchema{}, + } + + for typeName, r := range s.Provider.ResourcesMap() { + identitySchemaReq := resource.IdentitySchemaRequest{} + identitySchemaResp := &resource.IdentitySchemaResponse{} + + r.IdentitySchema(ctx, identitySchemaReq, identitySchemaResp) + + resp.Diagnostics = append(resp.Diagnostics, identitySchemaResp.Diagnostics...) + + if identitySchemaResp.Schema != nil { + resp.IdentitySchemas[typeName] = identitySchemaResp.Schema + } + } + + return resp, nil +} + +func (s ProviderServer) ImportResourceState(ctx context.Context, req *tfprotov6.ImportResourceStateRequest) (*tfprotov6.ImportResourceStateResponse, error) { + resp := &tfprotov6.ImportResourceStateResponse{} + + r, diag := ProviderResource(s.Provider, req.TypeName) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + schemaReq := resource.SchemaRequest{} + schemaResp := &resource.SchemaResponse{} + + r.Schema(ctx, schemaReq, schemaResp) + + resp.Diagnostics = schemaResp.Diagnostics + + if len(resp.Diagnostics) > 0 { + return resp, nil + } + + importReq := resource.ImportStateRequest{ + ID: req.ID, + } + importResp := &resource.ImportStateResponse{} + + // Copy over identity if it's supported + identitySchemaReq := resource.IdentitySchemaRequest{} + identitySchemaResp := &resource.IdentitySchemaResponse{} + + r.IdentitySchema(ctx, identitySchemaReq, identitySchemaResp) + + resp.Diagnostics = identitySchemaResp.Diagnostics + + if len(resp.Diagnostics) > 0 { + return resp, nil + } + + if identitySchemaResp.Schema != nil && req.Identity != nil { + identity, diag := IdentityDynamicValueToValue(identitySchemaResp.Schema, req.Identity.IdentityData) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + importReq.Identity = &identity + importResp.Identity = &identity + } + + r.ImportState(ctx, importReq, importResp) + + resp.Diagnostics = importResp.Diagnostics + + if len(resp.Diagnostics) > 0 { + return resp, nil + } + + if importResp.State.IsNull() { + resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Resource Missing Import Support", + Detail: "After import, the managed resource returned an empty state with no diagnostics. " + + "Implement import or raise an error diagnostic.", + }) + + return resp, nil + } + + state, diag := ValuetoDynamicValue(schemaResp.Schema, importResp.State) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + resp.ImportedResources = []*tfprotov6.ImportedResource{ + { + State: state, + TypeName: req.TypeName, + }, + } + + if importResp.Identity != nil { + identity, diag := IdentityValuetoDynamicValue(identitySchemaResp.Schema, *importResp.Identity) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + // There is only one imported resource, so this should always be safe + resp.ImportedResources[0].Identity = &tfprotov6.ResourceIdentityData{ + IdentityData: identity, + } + } + + return resp, nil +} + +func (s ProviderServer) PlanResourceChange(ctx context.Context, req *tfprotov6.PlanResourceChangeRequest) (*tfprotov6.PlanResourceChangeResponse, error) { + resp := &tfprotov6.PlanResourceChangeResponse{} + + r, diag := ProviderResource(s.Provider, req.TypeName) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + schemaReq := resource.SchemaRequest{} + schemaResp := &resource.SchemaResponse{} + + r.Schema(ctx, schemaReq, schemaResp) + + resp.Diagnostics = schemaResp.Diagnostics + + if len(resp.Diagnostics) > 0 { + return resp, nil + } + + config, diag := DynamicValueToValue(schemaResp.Schema, req.Config) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + priorState, diag := DynamicValueToValue(schemaResp.Schema, req.PriorState) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + proposedNewState, diag := DynamicValueToValue(schemaResp.Schema, req.ProposedNewState) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + if !proposedNewState.IsNull() && !proposedNewState.Equal(priorState) { + modifiedProposedNewState, err := tftypes.Transform(proposedNewState, func(path *tftypes.AttributePath, val tftypes.Value) (tftypes.Value, error) { + // we are only modifying attributes, not the entire resource + if len(path.Steps()) < 1 { + return val, nil + } + + configValIface, _, err := tftypes.WalkAttributePath(config, path) + + if err != nil && err != tftypes.ErrInvalidStep { + return val, fmt.Errorf("error walking attribute/block path during unknown marking: %w", err) + } + + configVal, ok := configValIface.(tftypes.Value) + + if !ok { + return val, fmt.Errorf("unexpected type during unknown marking: %T", configValIface) + } + + if !configVal.IsNull() { + return val, nil + } + + attribute := SchemaAttributeAtPath(schemaResp.Schema, path) + + if attribute == nil { + return val, nil + } + + if !attribute.Computed { + return val, nil + } + + return tftypes.NewValue(val.Type(), tftypes.UnknownValue), nil + }) + + if err != nil { + diag := &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Error Modifying ProposedNewState", + Detail: err.Error(), + } + + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil //nolint:nilerr // error via diagnostic, not gRPC + } + + proposedNewState = modifiedProposedNewState + } + + planReq := resource.PlanChangeRequest{ + Config: config, + PriorState: priorState, + ProposedNewState: proposedNewState, + } + planResp := &resource.PlanChangeResponse{ + PlannedState: proposedNewState.Copy(), + } + + // Copy over identity if it's supported + identitySchemaReq := resource.IdentitySchemaRequest{} + identitySchemaResp := &resource.IdentitySchemaResponse{} + + r.IdentitySchema(ctx, identitySchemaReq, identitySchemaResp) + + resp.Diagnostics = identitySchemaResp.Diagnostics + + if len(resp.Diagnostics) > 0 { + return resp, nil + } + + if identitySchemaResp.Schema != nil && req.PriorIdentity != nil { + priorIdentity, diag := IdentityDynamicValueToValue(identitySchemaResp.Schema, req.PriorIdentity.IdentityData) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + planReq.PriorIdentity = &priorIdentity + planResp.PlannedIdentity = &priorIdentity + } + + r.PlanChange(ctx, planReq, planResp) + + resp.Diagnostics = planResp.Diagnostics + resp.RequiresReplace = planResp.RequiresReplace + resp.Deferred = planResp.Deferred + + if len(resp.Diagnostics) > 0 { + return resp, nil + } + + plannedState, diag := ValuetoDynamicValue(schemaResp.Schema, planResp.PlannedState) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + if planResp.PlannedIdentity != nil { + plannedIdentity, diag := IdentityValuetoDynamicValue(identitySchemaResp.Schema, *planResp.PlannedIdentity) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + resp.PlannedIdentity = &tfprotov6.ResourceIdentityData{ + IdentityData: plannedIdentity, + } + } + + resp.PlannedState = plannedState + + return resp, nil +} + +func (s ProviderServer) ReadDataSource(ctx context.Context, req *tfprotov6.ReadDataSourceRequest) (*tfprotov6.ReadDataSourceResponse, error) { + resp := &tfprotov6.ReadDataSourceResponse{} + + d, diag := ProviderDataSource(s.Provider, req.TypeName) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + schemaReq := datasource.SchemaRequest{} + schemaResp := &datasource.SchemaResponse{} + + d.Schema(ctx, schemaReq, schemaResp) + + resp.Diagnostics = schemaResp.Diagnostics + + if len(resp.Diagnostics) > 0 { + return resp, nil + } + + config, diag := DynamicValueToValue(schemaResp.Schema, req.Config) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + readReq := datasource.ReadRequest{ + Config: config, + } + readResp := &datasource.ReadResponse{ + State: config.Copy(), + } + + d.Read(ctx, readReq, readResp) + + resp.Diagnostics = readResp.Diagnostics + + if len(resp.Diagnostics) > 0 { + return resp, nil + } + + state, diag := ValuetoDynamicValue(schemaResp.Schema, readResp.State) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + resp.State = state + + return resp, nil +} + +func (s ProviderServer) ReadResource(ctx context.Context, req *tfprotov6.ReadResourceRequest) (*tfprotov6.ReadResourceResponse, error) { + resp := &tfprotov6.ReadResourceResponse{} + + r, diag := ProviderResource(s.Provider, req.TypeName) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + schemaReq := resource.SchemaRequest{} + schemaResp := &resource.SchemaResponse{} + + r.Schema(ctx, schemaReq, schemaResp) + + resp.Diagnostics = schemaResp.Diagnostics + + if len(resp.Diagnostics) > 0 { + return resp, nil + } + + currentState, diag := DynamicValueToValue(schemaResp.Schema, req.CurrentState) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + readReq := resource.ReadRequest{ + CurrentState: currentState, + } + readResp := &resource.ReadResponse{ + NewState: currentState.Copy(), + } + + // Copy over identity if it's supported + identitySchemaReq := resource.IdentitySchemaRequest{} + identitySchemaResp := &resource.IdentitySchemaResponse{} + + r.IdentitySchema(ctx, identitySchemaReq, identitySchemaResp) + + resp.Diagnostics = identitySchemaResp.Diagnostics + + if len(resp.Diagnostics) > 0 { + return resp, nil + } + + if identitySchemaResp.Schema != nil && req.CurrentIdentity != nil { + currentIdentity, diag := IdentityDynamicValueToValue(identitySchemaResp.Schema, req.CurrentIdentity.IdentityData) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + readReq.CurrentIdentity = ¤tIdentity + readResp.NewIdentity = ¤tIdentity + } + + r.Read(ctx, readReq, readResp) + + resp.Diagnostics = readResp.Diagnostics + + if len(resp.Diagnostics) > 0 { + return resp, nil + } + + newState, diag := ValuetoDynamicValue(schemaResp.Schema, readResp.NewState) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + resp.NewState = newState + + if readResp.NewIdentity != nil { + newIdentity, diag := IdentityValuetoDynamicValue(identitySchemaResp.Schema, *readResp.NewIdentity) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + resp.NewIdentity = &tfprotov6.ResourceIdentityData{ + IdentityData: newIdentity, + } + } + + return resp, nil +} + +func (s ProviderServer) StopProvider(ctx context.Context, req *tfprotov6.StopProviderRequest) (*tfprotov6.StopProviderResponse, error) { + providerReq := provider.StopRequest{} + providerResp := &provider.StopResponse{} + + s.Provider.Stop(ctx, providerReq, providerResp) + + resp := &tfprotov6.StopProviderResponse{} + + if providerResp.Error != nil { + resp.Error = providerResp.Error.Error() + } + + return resp, nil +} + +func (s ProviderServer) UpgradeResourceState(ctx context.Context, req *tfprotov6.UpgradeResourceStateRequest) (*tfprotov6.UpgradeResourceStateResponse, error) { + resp := &tfprotov6.UpgradeResourceStateResponse{} + + r, diag := ProviderResource(s.Provider, req.TypeName) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + schemaReq := resource.SchemaRequest{} + schemaResp := &resource.SchemaResponse{} + + r.Schema(ctx, schemaReq, schemaResp) + + resp.Diagnostics = schemaResp.Diagnostics + + if len(resp.Diagnostics) > 0 { + return resp, nil + } + + // Define options to be used when unmarshalling raw state. + // IgnoreUndefinedAttributes will silently skip over fields in the JSON + // that do not have a matching entry in the schema. + unmarshalOpts := tfprotov6.UnmarshalOpts{ + ValueFromJSONOpts: tftypes.ValueFromJSONOpts{ + IgnoreUndefinedAttributes: true, + }, + } + + // Terraform CLI can call UpgradeResourceState even if the stored state + // version matches the current schema. Presumably this is to account for + // the previous terraform-plugin-sdk implementation, which handled some + // state fixups on behalf of Terraform CLI. This will attempt to roundtrip + // the prior RawState to a state matching the current schema. + if req.Version == schemaResp.Schema.Version { + rawStateValue, err := req.RawState.UnmarshalWithOpts(schemaResp.Schema.ValueType(), unmarshalOpts) + + if err != nil { + diag := &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unable to Read Previously Saved State for UpgradeResourceState", + Detail: "There was an error reading the saved resource state using the current resource schema: " + err.Error(), + } + + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil //nolint:nilerr // error via diagnostic, not gRPC + } + + upgradedState, diag := ValuetoDynamicValue(schemaResp.Schema, rawStateValue) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + resp.UpgradedState = upgradedState + + return resp, nil + } + + upgradeReq := resource.UpgradeStateRequest{} + upgradeResp := &resource.UpgradeStateResponse{} + + r.UpgradeState(ctx, upgradeReq, upgradeResp) + + resp.Diagnostics = upgradeResp.Diagnostics + + if len(resp.Diagnostics) > 0 { + return resp, nil + } + + upgradedState, diag := ValuetoDynamicValue(schemaResp.Schema, upgradeResp.UpgradedState) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + resp.UpgradedState = upgradedState + + return resp, nil +} + +func (s ProviderServer) UpgradeResourceIdentity(context.Context, *tfprotov6.UpgradeResourceIdentityRequest) (*tfprotov6.UpgradeResourceIdentityResponse, error) { + // TODO: This isn't currently being used by the testing framework provider, so no need to implement it until then. + return nil, errors.New("UpgradeResourceIdentity is not currently implemented in testprovider") +} + +func (s ProviderServer) ValidateDataResourceConfig(ctx context.Context, req *tfprotov6.ValidateDataResourceConfigRequest) (*tfprotov6.ValidateDataResourceConfigResponse, error) { + resp := &tfprotov6.ValidateDataResourceConfigResponse{} + + d, diag := ProviderDataSource(s.Provider, req.TypeName) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + schemaReq := datasource.SchemaRequest{} + schemaResp := &datasource.SchemaResponse{} + + d.Schema(ctx, schemaReq, schemaResp) + + resp.Diagnostics = schemaResp.Diagnostics + + if len(resp.Diagnostics) > 0 { + return resp, nil + } + + config, diag := DynamicValueToValue(schemaResp.Schema, req.Config) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + validateReq := datasource.ValidateConfigRequest{ + Config: config, + } + validateResp := &datasource.ValidateConfigResponse{} + + d.ValidateConfig(ctx, validateReq, validateResp) + + resp.Diagnostics = validateResp.Diagnostics + + return resp, nil +} + +func (s ProviderServer) ValidateProviderConfig(ctx context.Context, req *tfprotov6.ValidateProviderConfigRequest) (*tfprotov6.ValidateProviderConfigResponse, error) { + providerReq := provider.ValidateConfigRequest{} + providerResp := &provider.ValidateConfigResponse{} + + s.Provider.ValidateConfig(ctx, providerReq, providerResp) + + resp := &tfprotov6.ValidateProviderConfigResponse{ + Diagnostics: providerResp.Diagnostics, + PreparedConfig: req.Config, + } + + return resp, nil +} + +func (s ProviderServer) ValidateResourceConfig(ctx context.Context, req *tfprotov6.ValidateResourceConfigRequest) (*tfprotov6.ValidateResourceConfigResponse, error) { + resp := &tfprotov6.ValidateResourceConfigResponse{} + + r, diag := ProviderResource(s.Provider, req.TypeName) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + schemaReq := resource.SchemaRequest{} + schemaResp := &resource.SchemaResponse{} + + r.Schema(ctx, schemaReq, schemaResp) + + resp.Diagnostics = schemaResp.Diagnostics + + if len(resp.Diagnostics) > 0 { + return resp, nil + } + + config, diag := DynamicValueToValue(schemaResp.Schema, req.Config) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + validateReq := resource.ValidateConfigRequest{ + Config: config, + } + validateResp := &resource.ValidateConfigResponse{} + + r.ValidateConfig(ctx, validateReq, validateResp) + + resp.Diagnostics = validateResp.Diagnostics + + return resp, nil +} + +// Functions are not currently implemented in this test SDK +func (s ProviderServer) CallFunction(ctx context.Context, req *tfprotov6.CallFunctionRequest) (*tfprotov6.CallFunctionResponse, error) { + return &tfprotov6.CallFunctionResponse{}, nil +} + +func (s ProviderServer) GetFunctions(ctx context.Context, req *tfprotov6.GetFunctionsRequest) (*tfprotov6.GetFunctionsResponse, error) { + return &tfprotov6.GetFunctionsResponse{}, nil +} + +// Ephemeral resources are not currently implemented in this test SDK +func (s ProviderServer) OpenEphemeralResource(ctx context.Context, req *tfprotov6.OpenEphemeralResourceRequest) (*tfprotov6.OpenEphemeralResourceResponse, error) { + return &tfprotov6.OpenEphemeralResourceResponse{}, nil +} + +func (s ProviderServer) RenewEphemeralResource(ctx context.Context, req *tfprotov6.RenewEphemeralResourceRequest) (*tfprotov6.RenewEphemeralResourceResponse, error) { + return &tfprotov6.RenewEphemeralResourceResponse{}, nil +} + +func (s ProviderServer) CloseEphemeralResource(ctx context.Context, req *tfprotov6.CloseEphemeralResourceRequest) (*tfprotov6.CloseEphemeralResourceResponse, error) { + return &tfprotov6.CloseEphemeralResourceResponse{}, nil +} + +func (s ProviderServer) ValidateEphemeralResourceConfig(ctx context.Context, req *tfprotov6.ValidateEphemeralResourceConfigRequest) (*tfprotov6.ValidateEphemeralResourceConfigResponse, error) { + return &tfprotov6.ValidateEphemeralResourceConfigResponse{}, nil +} + +func (s ProviderServer) ListResource(ctx context.Context, req *tfprotov6.ListResourceRequest) (*tfprotov6.ListResourceServerStream, error) { + resultStream := &tfprotov6.ListResourceServerStream{} + respStream := &list.ListResultsStream{} + + // Copy over identity if it's supported + identitySchemaReq := resource.IdentitySchemaRequest{} + identitySchemaResp := &resource.IdentitySchemaResponse{} + + r, err := ProviderResource(s.Provider, req.TypeName) + if err != nil { + return nil, fmt.Errorf("failed to retrieve resource: %v", err) + } + r.IdentitySchema(ctx, identitySchemaReq, identitySchemaResp) + if len(identitySchemaResp.Diagnostics) > 0 { + return nil, fmt.Errorf("failed to retrieve resource schema: %v", identitySchemaResp.Diagnostics) + } + + listresource, diag := ProviderListResource(s.Provider, req.TypeName) + if diag != nil { + return nil, fmt.Errorf("failed to retrieve resource identity schema: %v", err) + } + + schemaReq := list.SchemaRequest{} + schemaResp := &list.SchemaResponse{} + + listresource.Schema(ctx, schemaReq, schemaResp) + if len(schemaResp.Diagnostics) > 0 { + return nil, fmt.Errorf("failed to retrieve resource schema: %v", schemaResp.Diagnostics) + } + + listReq := list.ListRequest{ + TypeName: req.TypeName, + IncludeResource: req.IncludeResource, + Limit: req.Limit, + ResourceSchema: schemaResp.Schema, + } + + listReq.Config, diag = DynamicValueToValue(schemaResp.Schema, req.Config) + if diag != nil { + return nil, fmt.Errorf("failed to convert config to value: %v", err) + } + + if identitySchemaResp.Schema != nil { + listReq.ResourceIdentitySchema = identitySchemaResp.Schema + } + + listresource.List(ctx, listReq, respStream) + + // If the provider returned a nil results stream, we return an empty stream. + if respStream.Results == nil { + resultStream.Results = func(push func(result tfprotov6.ListResourceResult) bool) {} + } + + resultStream.Results = processListResults(listReq, respStream.Results) + return resultStream, nil +} + +func (s ProviderServer) ValidateListResourceConfig(ctx context.Context, req *tfprotov6.ValidateListResourceConfigRequest) (*tfprotov6.ValidateListResourceConfigResponse, error) { + return &tfprotov6.ValidateListResourceConfigResponse{}, nil +} + +func processListResults(req list.ListRequest, stream iter.Seq[list.ListResult]) iter.Seq[tfprotov6.ListResourceResult] { + return func(push func(tfprotov6.ListResourceResult) bool) { + for result := range stream { + if !push(processListResult(req, result)) { + return + } + } + } +} + +// processListResult validates the content of a list.ListResult and returns a +// ListResult +func processListResult(req list.ListRequest, result list.ListResult) tfprotov6.ListResourceResult { + var listResourceResult tfprotov6.ListResourceResult + listResourceResult.Diagnostics = []*tfprotov6.Diagnostic{} + var diag *tfprotov6.Diagnostic + + // Allow any non-error diags to pass through + if len(result.Diagnostics) > 0 && result.DisplayName == "" && result.Identity == nil && result.Resource == nil { + return tfprotov6.ListResourceResult{ + Diagnostics: result.Diagnostics, + } + } + + if result.Diagnostics != nil { + return tfprotov6.ListResourceResult{ + Diagnostics: result.Diagnostics, + } + } + + if result.Identity == nil { + return tfprotov6.ListResourceResult{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Incomplete List Result", + Detail: "When listing resources, an implementation issue was found. " + + "This is always a problem with the provider. Please report this to the provider developers.\n\n" + + "The \"Identity\" field is nil.\n\n", + }, + }, + } + } + + if req.IncludeResource { + if result.Resource == nil { + listResourceResult.Diagnostics = append(listResourceResult.Diagnostics, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "Incomplete List Result", + Detail: "When listing resources, an implementation issue was found. " + + "This is always a problem with the provider. Please report this to the provider developers.\n\n" + + "The \"IncludeResource\" field in the ListRequest is true, but the \"Resource\" field in the ListResult is nil.\n\n", + }) + } + + listResourceResult.Resource, diag = ValuetoDynamicValue(req.ResourceSchema, *result.Resource) + listResourceResult.Diagnostics = append(listResourceResult.Diagnostics, diag) + return listResourceResult + + } + listResourceResult.Identity = &tfprotov6.ResourceIdentityData{} + + if result.Identity != nil { + listResourceResult.Identity.IdentityData, diag = IdentityValuetoDynamicValue(req.ResourceIdentitySchema, *result.Identity) + if diag != nil { + listResourceResult.Diagnostics = append(listResourceResult.Diagnostics, diag) + return listResourceResult + } + } + + listResourceResult.DisplayName = result.DisplayName + + return listResourceResult + +} diff --git a/internal/testing/testsdk/providerserver/providerserver_protov5.go b/internal/testing/testsdk/providerserver/providerserver_protov5.go new file mode 100644 index 000000000..6f93bdffe --- /dev/null +++ b/internal/testing/testsdk/providerserver/providerserver_protov5.go @@ -0,0 +1,144 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package providerserver + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/provider" +) + +var _ tfprotov5.ProviderServer = Protov5ProviderServer{} + +// NewProtov5ProviderServer returns a protocol version 5 provider server which only +// implements GetProviderSchema, for consumption with ProtoV5ProviderFactories. +func NewProtov5ProviderServer(p provider.Protov5Provider) func() (tfprotov5.ProviderServer, error) { + return NewProtov5ProviderServerWithError(p, nil) +} + +// NewProtov5ProviderServerWithError returns a protocol version 5 provider server, +// and an associated error for consumption with ProtoV5ProviderFactories. +func NewProtov5ProviderServerWithError(p provider.Protov5Provider, err error) func() (tfprotov5.ProviderServer, error) { + providerServer := Protov5ProviderServer{ + Provider: p, + } + + return func() (tfprotov5.ProviderServer, error) { + return providerServer, err + } +} + +// Protov5ProviderServer is a version 5 provider server that only implements GetProviderSchema. +type Protov5ProviderServer struct { + Provider provider.Protov5Provider +} + +// CallFunction implements tfprotov5.ProviderServer. +func (s Protov5ProviderServer) CallFunction(ctx context.Context, req *tfprotov5.CallFunctionRequest) (*tfprotov5.CallFunctionResponse, error) { + return &tfprotov5.CallFunctionResponse{}, nil +} + +// GetFunctions implements tfprotov5.ProviderServer. +func (s Protov5ProviderServer) GetFunctions(ctx context.Context, req *tfprotov5.GetFunctionsRequest) (*tfprotov5.GetFunctionsResponse, error) { + return &tfprotov5.GetFunctionsResponse{}, nil +} + +func (s Protov5ProviderServer) MoveResourceState(ctx context.Context, req *tfprotov5.MoveResourceStateRequest) (*tfprotov5.MoveResourceStateResponse, error) { + return &tfprotov5.MoveResourceStateResponse{}, nil +} + +func (s Protov5ProviderServer) GetMetadata(ctx context.Context, request *tfprotov5.GetMetadataRequest) (*tfprotov5.GetMetadataResponse, error) { + return &tfprotov5.GetMetadataResponse{}, nil +} + +func (s Protov5ProviderServer) ApplyResourceChange(ctx context.Context, req *tfprotov5.ApplyResourceChangeRequest) (*tfprotov5.ApplyResourceChangeResponse, error) { + return &tfprotov5.ApplyResourceChangeResponse{}, nil +} + +func (s Protov5ProviderServer) ConfigureProvider(ctx context.Context, req *tfprotov5.ConfigureProviderRequest) (*tfprotov5.ConfigureProviderResponse, error) { + return &tfprotov5.ConfigureProviderResponse{}, nil +} + +func (s Protov5ProviderServer) GetProviderSchema(ctx context.Context, req *tfprotov5.GetProviderSchemaRequest) (*tfprotov5.GetProviderSchemaResponse, error) { + providerReq := provider.Protov5SchemaRequest{} + providerResp := &provider.Protov5SchemaResponse{} + + s.Provider.Schema(ctx, providerReq, providerResp) + + resp := &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + Diagnostics: providerResp.Diagnostics, + Provider: providerResp.Schema, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + ServerCapabilities: &tfprotov5.ServerCapabilities{ + PlanDestroy: true, + }, + } + + return resp, nil +} + +func (s Protov5ProviderServer) ImportResourceState(ctx context.Context, req *tfprotov5.ImportResourceStateRequest) (*tfprotov5.ImportResourceStateResponse, error) { + return &tfprotov5.ImportResourceStateResponse{}, nil +} + +func (s Protov5ProviderServer) PlanResourceChange(ctx context.Context, req *tfprotov5.PlanResourceChangeRequest) (*tfprotov5.PlanResourceChangeResponse, error) { + return &tfprotov5.PlanResourceChangeResponse{}, nil +} + +func (s Protov5ProviderServer) PrepareProviderConfig(ctx context.Context, request *tfprotov5.PrepareProviderConfigRequest) (*tfprotov5.PrepareProviderConfigResponse, error) { + return &tfprotov5.PrepareProviderConfigResponse{}, nil +} + +func (s Protov5ProviderServer) ReadDataSource(ctx context.Context, req *tfprotov5.ReadDataSourceRequest) (*tfprotov5.ReadDataSourceResponse, error) { + return &tfprotov5.ReadDataSourceResponse{}, nil +} + +func (s Protov5ProviderServer) ReadResource(ctx context.Context, req *tfprotov5.ReadResourceRequest) (*tfprotov5.ReadResourceResponse, error) { + return &tfprotov5.ReadResourceResponse{}, nil +} + +func (s Protov5ProviderServer) StopProvider(ctx context.Context, req *tfprotov5.StopProviderRequest) (*tfprotov5.StopProviderResponse, error) { + return &tfprotov5.StopProviderResponse{}, nil +} + +func (s Protov5ProviderServer) UpgradeResourceState(ctx context.Context, req *tfprotov5.UpgradeResourceStateRequest) (*tfprotov5.UpgradeResourceStateResponse, error) { + return &tfprotov5.UpgradeResourceStateResponse{}, nil +} + +func (s Protov5ProviderServer) ValidateDataSourceConfig(ctx context.Context, request *tfprotov5.ValidateDataSourceConfigRequest) (*tfprotov5.ValidateDataSourceConfigResponse, error) { + return &tfprotov5.ValidateDataSourceConfigResponse{}, nil +} + +func (s Protov5ProviderServer) ValidateResourceTypeConfig(ctx context.Context, request *tfprotov5.ValidateResourceTypeConfigRequest) (*tfprotov5.ValidateResourceTypeConfigResponse, error) { + return &tfprotov5.ValidateResourceTypeConfigResponse{}, nil +} + +func (s Protov5ProviderServer) OpenEphemeralResource(ctx context.Context, req *tfprotov5.OpenEphemeralResourceRequest) (*tfprotov5.OpenEphemeralResourceResponse, error) { + return &tfprotov5.OpenEphemeralResourceResponse{}, nil +} + +func (s Protov5ProviderServer) RenewEphemeralResource(ctx context.Context, req *tfprotov5.RenewEphemeralResourceRequest) (*tfprotov5.RenewEphemeralResourceResponse, error) { + return &tfprotov5.RenewEphemeralResourceResponse{}, nil +} + +func (s Protov5ProviderServer) CloseEphemeralResource(ctx context.Context, req *tfprotov5.CloseEphemeralResourceRequest) (*tfprotov5.CloseEphemeralResourceResponse, error) { + return &tfprotov5.CloseEphemeralResourceResponse{}, nil +} + +func (s Protov5ProviderServer) ValidateEphemeralResourceConfig(ctx context.Context, req *tfprotov5.ValidateEphemeralResourceConfigRequest) (*tfprotov5.ValidateEphemeralResourceConfigResponse, error) { + return &tfprotov5.ValidateEphemeralResourceConfigResponse{}, nil +} + +func (s Protov5ProviderServer) GetResourceIdentitySchemas(context.Context, *tfprotov5.GetResourceIdentitySchemasRequest) (*tfprotov5.GetResourceIdentitySchemasResponse, error) { + return &tfprotov5.GetResourceIdentitySchemasResponse{}, nil +} + +func (s Protov5ProviderServer) UpgradeResourceIdentity(context.Context, *tfprotov5.UpgradeResourceIdentityRequest) (*tfprotov5.UpgradeResourceIdentityResponse, error) { + return &tfprotov5.UpgradeResourceIdentityResponse{}, nil +} diff --git a/internal/testing/testsdk/providerserver/resources.go b/internal/testing/testsdk/providerserver/resources.go new file mode 100644 index 000000000..21307dd84 --- /dev/null +++ b/internal/testing/testsdk/providerserver/resources.go @@ -0,0 +1,24 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package providerserver + +import ( + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/provider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" +) + +func ProviderResource(p provider.Provider, typeName string) (resource.Resource, *tfprotov6.Diagnostic) { + r, ok := p.ResourcesMap()[typeName] + + if !ok { + return nil, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Missing Resource Type", + Detail: "The provider does not define the resource type: " + typeName, + } + } + + return r, nil +} diff --git a/internal/testing/testsdk/providerserver/schema.go b/internal/testing/testsdk/providerserver/schema.go new file mode 100644 index 000000000..42600be44 --- /dev/null +++ b/internal/testing/testsdk/providerserver/schema.go @@ -0,0 +1,57 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package providerserver + +import ( + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func SchemaAttributeAtPath(schema *tfprotov6.Schema, path *tftypes.AttributePath) *tfprotov6.SchemaAttribute { + if schema == nil || schema.Block == nil || path == nil || len(path.Steps()) == 0 { + return nil + } + + steps := path.Steps() + nextStep := steps[0] + remainingSteps := steps[1:] + + switch nextStep := nextStep.(type) { + case tftypes.AttributeName: + for _, attribute := range schema.Block.Attributes { + if attribute == nil { + continue + } + + if attribute.Name != string(nextStep) { + continue + } + + if len(remainingSteps) == 0 { + return attribute + } + + // If needed, recursive attribute.NestedType handling would go here. + } + + for _, block := range schema.Block.BlockTypes { + if block == nil { + continue + } + + if block.TypeName != string(nextStep) { + continue + } + + // Blocks cannot be computed. + if len(remainingSteps) == 0 { + return nil + } + + // If needed, recursive block handling would go here. + } + } + + return nil +} diff --git a/internal/testing/testsdk/providerserver/tftypes.go b/internal/testing/testsdk/providerserver/tftypes.go new file mode 100644 index 000000000..a15ba1143 --- /dev/null +++ b/internal/testing/testsdk/providerserver/tftypes.go @@ -0,0 +1,121 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package providerserver + +import ( + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func DynamicValueToValue(schema *tfprotov6.Schema, dynamicValue *tfprotov6.DynamicValue) (tftypes.Value, *tfprotov6.Diagnostic) { + if schema == nil { + diag := &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unable to Convert DynamicValue", + Detail: "Converting the DynamicValue to Value returned an unexpected error: missing schema", + } + + return tftypes.NewValue(tftypes.Object{}, nil), diag + } + + if dynamicValue == nil { + return tftypes.NewValue(schema.ValueType(), nil), nil + } + + value, err := dynamicValue.Unmarshal(schema.ValueType()) + + if err != nil { + diag := &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unable to Convert DynamicValue", + Detail: "Converting the DynamicValue to Value returned an unexpected error: " + err.Error(), + } + + return value, diag + } + + return value, nil +} + +func ValuetoDynamicValue(schema *tfprotov6.Schema, value tftypes.Value) (*tfprotov6.DynamicValue, *tfprotov6.Diagnostic) { + if schema == nil { + diag := &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unable to Convert Value", + Detail: "Converting the Value to DynamicValue returned an unexpected error: missing schema", + } + + return nil, diag + } + + dynamicValue, err := tfprotov6.NewDynamicValue(schema.ValueType(), value) + + if err != nil { + diag := &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unable to Convert Value", + Detail: "Converting the Value to DynamicValue returned an unexpected error: " + err.Error(), + } + + return &dynamicValue, diag + } + + return &dynamicValue, nil +} + +func IdentityDynamicValueToValue(schema *tfprotov6.ResourceIdentitySchema, dynamicValue *tfprotov6.DynamicValue) (tftypes.Value, *tfprotov6.Diagnostic) { + if schema == nil { + diag := &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unable to Convert DynamicValue", + Detail: "Converting the DynamicValue to Value returned an unexpected error: missing identity schema", + } + + return tftypes.NewValue(tftypes.Object{}, nil), diag + } + + if dynamicValue == nil { + return tftypes.NewValue(schema.ValueType(), nil), nil + } + + value, err := dynamicValue.Unmarshal(schema.ValueType()) + + if err != nil { + diag := &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unable to Convert DynamicValue", + Detail: "Converting the DynamicValue to Value returned an unexpected error: " + err.Error(), + } + + return value, diag + } + + return value, nil +} + +func IdentityValuetoDynamicValue(schema *tfprotov6.ResourceIdentitySchema, value tftypes.Value) (*tfprotov6.DynamicValue, *tfprotov6.Diagnostic) { + if schema == nil { + diag := &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unable to Convert Value", + Detail: "Converting the Value to DynamicValue returned an unexpected error: missing identity schema", + } + + return nil, diag + } + + dynamicValue, err := tfprotov6.NewDynamicValue(schema.ValueType(), value) + + if err != nil { + diag := &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unable to Convert Value", + Detail: "Converting the Value to DynamicValue returned an unexpected error: " + err.Error(), + } + + return &dynamicValue, diag + } + + return &dynamicValue, nil +} diff --git a/internal/testing/testsdk/resource/doc.go b/internal/testing/testsdk/resource/doc.go new file mode 100644 index 000000000..aa04d35c3 --- /dev/null +++ b/internal/testing/testsdk/resource/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package resource provides testsdk handling of the managed resource concept. +package resource diff --git a/internal/testing/testsdk/resource/resource.go b/internal/testing/testsdk/resource/resource.go new file mode 100644 index 000000000..2a64e323f --- /dev/null +++ b/internal/testing/testsdk/resource/resource.go @@ -0,0 +1,126 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package resource + +import ( + "context" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +type Resource interface { + Create(context.Context, CreateRequest, *CreateResponse) + Delete(context.Context, DeleteRequest, *DeleteResponse) + ImportState(context.Context, ImportStateRequest, *ImportStateResponse) + PlanChange(context.Context, PlanChangeRequest, *PlanChangeResponse) + Read(context.Context, ReadRequest, *ReadResponse) + IdentitySchema(context.Context, IdentitySchemaRequest, *IdentitySchemaResponse) + Schema(context.Context, SchemaRequest, *SchemaResponse) + Update(context.Context, UpdateRequest, *UpdateResponse) + UpgradeState(context.Context, UpgradeStateRequest, *UpgradeStateResponse) + ValidateConfig(context.Context, ValidateConfigRequest, *ValidateConfigResponse) +} + +type CreateRequest struct { + Config tftypes.Value + PlannedIdentity *tftypes.Value +} + +type CreateResponse struct { + Diagnostics []*tfprotov6.Diagnostic + NewState tftypes.Value + NewIdentity *tftypes.Value + NewQuery tftypes.Value +} + +type DeleteRequest struct { + PriorState tftypes.Value +} + +type DeleteResponse struct { + Diagnostics []*tfprotov6.Diagnostic +} + +type IdentitySchemaRequest struct{} + +type IdentitySchemaResponse struct { + Diagnostics []*tfprotov6.Diagnostic + Schema *tfprotov6.ResourceIdentitySchema +} + +type ImportStateRequest struct { + ID string + Identity *tftypes.Value +} + +type ImportStateResponse struct { + Diagnostics []*tfprotov6.Diagnostic + State tftypes.Value + Identity *tftypes.Value +} + +type PlanChangeRequest struct { + Config tftypes.Value + PriorState tftypes.Value + PriorIdentity *tftypes.Value + ProposedNewState tftypes.Value +} + +type PlanChangeResponse struct { + Deferred *tfprotov6.Deferred + Diagnostics []*tfprotov6.Diagnostic + PlannedState tftypes.Value + PlannedIdentity *tftypes.Value + RequiresReplace []*tftypes.AttributePath +} + +type ReadRequest struct { + CurrentState tftypes.Value + CurrentIdentity *tftypes.Value +} + +type ReadResponse struct { + Diagnostics []*tfprotov6.Diagnostic + NewState tftypes.Value + NewIdentity *tftypes.Value + NewQuery tftypes.Value +} + +type SchemaRequest struct{} + +type SchemaResponse struct { + Diagnostics []*tfprotov6.Diagnostic + Schema *tfprotov6.Schema +} + +type UpdateRequest struct { + Config tftypes.Value + PlannedState tftypes.Value + PlannedIdentity *tftypes.Value + PriorState tftypes.Value +} + +type UpdateResponse struct { + Diagnostics []*tfprotov6.Diagnostic + NewState tftypes.Value + NewIdentity *tftypes.Value +} + +type UpgradeStateRequest struct { + RawState *tfprotov6.RawState + Version int64 +} + +type UpgradeStateResponse struct { + Diagnostics []*tfprotov6.Diagnostic + UpgradedState tftypes.Value +} + +type ValidateConfigRequest struct { + Config tftypes.Value +} + +type ValidateConfigResponse struct { + Diagnostics []*tfprotov6.Diagnostic +} diff --git a/internal/teststep/config.go b/internal/teststep/config.go new file mode 100644 index 000000000..5df477f08 --- /dev/null +++ b/internal/teststep/config.go @@ -0,0 +1,269 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package teststep + +import ( + "context" + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/hashicorp/terraform-plugin-testing/config" +) + +const ( + rawConfigFileName = "terraform_plugin_test.tf" + rawConfigFileNameJSON = rawConfigFileName + ".json" + rawQueryConfigFileName = "terraform_plugin_test.tfquery.hcl" +) + +var ( + // Expected to match: + // provider "example" { + // provider "example"{ + // provider example { + // provider example{ + // provider"example"{ + providerConfigBlockRegex = regexp.MustCompile(`provider(\s*"[a-zA-Z0-9_-]+"\s*|\s+[a-zA-Z0-9_-]+\s*){`) + // Expected to match: + // terraform { + // terraform{ + terraformConfigBlockRegex = regexp.MustCompile(`terraform\s*{`) +) + +// Config defines an interface implemented by all types +// that represent Terraform configuration: +// +// - [config.configurationDirectory] +// - [config.configurationFile] +// - [config.configurationString] +type Config interface { + HasConfigurationFiles() bool + HasProviderBlock(context.Context) (bool, error) + HasTerraformBlock(context.Context) (bool, error) + Write(context.Context, string) error + Append(string) Config + WriteQuery(context.Context, string) error +} + +// PrepareConfigurationRequest is used to simplify the generation of +// a ConfigurationRequest which is required when calling the +// Configuration func. +type PrepareConfigurationRequest struct { + Directory config.TestStepConfigFunc + File config.TestStepConfigFunc + Raw string + TestStepConfigRequest config.TestStepConfigRequest +} + +// Exec returns a Configuration request which is required when +// calling the Configuration func. +func (p PrepareConfigurationRequest) Exec() ConfigurationRequest { + directory := Pointer(p.Directory.Exec(p.TestStepConfigRequest)) + file := Pointer(p.File.Exec(p.TestStepConfigRequest)) + raw := Pointer(p.Raw) + + return ConfigurationRequest{ + Directory: directory, + File: file, + Raw: raw, + } +} + +// ConfigurationRequest is used by the Configuration func to determine +// the underlying type to instantiate. +type ConfigurationRequest struct { + Directory *string + File *string + Raw *string +} + +// Validate ensures that only one of Directory, File or Raw are non-empty. +func (c ConfigurationRequest) Validate() error { + var configSet []string + + if c.Directory != nil && *c.Directory != "" { + configSet = append(configSet, "directory") + } + + if c.File != nil && *c.File != "" { + configSet = append(configSet, "file") + } + + if c.Raw != nil && *c.Raw != "" { + configSet = append(configSet, "raw") + } + + if len(configSet) > 1 { + configSetStr := strings.Join(configSet, `, `) + + i := strings.LastIndex(configSetStr, ", ") + + if i != -1 { + configSetStr = configSetStr[:i] + " and " + configSetStr[i+len(", "):] + } + + return fmt.Errorf(`%s are populated, only one of "directory", "file", or "raw" is allowed`, configSetStr) + } + + return nil +} + +// Configuration uses the supplied ConfigurationRequest to determine +// which of the types that implement Config to instantiate. If none +// of the fields in ConfigurationRequest are populated nil is returned. +func Configuration(req ConfigurationRequest) Config { + if req.Directory != nil && *req.Directory != "" { + return configurationDirectory{ + directory: *req.Directory, + } + } + + if req.File != nil && *req.File != "" { + return configurationFile{ + file: *req.File, + } + } + + if req.Raw != nil && *req.Raw != "" { + return configurationString{ + raw: *req.Raw, + } + } + + return nil +} + +// copyFiles accepts a path to a directory and a destination. Only +// files in the path directory are copied, any nested directories +// are ignored. +func copyFiles(path string, dstPath string) error { + infos, err := os.ReadDir(path) + + if err != nil { + return err + } + + for _, info := range infos { + srcPath := filepath.Join(path, info.Name()) + + if info.IsDir() { + continue + } else { + _, err = copyFile(srcPath, dstPath) + + if err != nil { + return err + } + } + + } + return nil +} + +// copyFile accepts a path to a file and a destination, +// copying the file from path to destination. +func copyFile(path string, dstPath string) (string, error) { + srcF, err := os.Open(path) + + if err != nil { + return "", err + } + + defer srcF.Close() + + di, err := os.Stat(dstPath) + + if err != nil { + return "", err + } + + if di.IsDir() { + _, file := filepath.Split(path) + dstPath = filepath.Join(dstPath, file) + } + + dstF, err := os.Create(dstPath) + + if err != nil { + return "", err + } + + defer dstF.Close() + + if _, err := io.Copy(dstF, srcF); err != nil { + return "", err + } + + return dstPath, nil +} + +// appendToFile accepts a path to a file and a string, +// appending the file from path to destination. +func appendToFile(path string, content string) error { + f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, os.ModeAppend) + if err != nil { + return err + } + defer f.Close() + + if _, err := io.WriteString(f, content); err != nil { + return err + } + + return nil +} + +// filesContains accepts a string representing a directory and a +// regular expression. For each file that is found within the +// directory fileContains func is called. Any nested directories +// within the directory specified by dir are ignored. +func filesContains(dir string, find *regexp.Regexp) (bool, error) { + dirEntries, err := os.ReadDir(dir) + + if err != nil { + return false, err + } + + for _, dirEntry := range dirEntries { + if dirEntry.IsDir() { + continue + } + + path := filepath.Join(dir, dirEntry.Name()) + + contains, err := fileContains(path, find) + + if err != nil { + return false, err + } + + if contains { + return true, nil + } + } + + return false, nil +} + +// fileContains accepts a path and a regular expression. The +// file is read and the supplied regular expression is used +// to determine whether the file contains the specified string. +func fileContains(path string, find *regexp.Regexp) (bool, error) { + f, err := os.ReadFile(path) + + if err != nil { + return false, err + } + + return find.MatchString(string(f)), nil +} + +// Pointer returns a pointer to any type. +func Pointer[T any](in T) *T { + return &in +} diff --git a/internal/teststep/config_test.go b/internal/teststep/config_test.go new file mode 100644 index 000000000..7f7061b72 --- /dev/null +++ b/internal/teststep/config_test.go @@ -0,0 +1,271 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package teststep + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/config" +) + +func TestPrepareConfigurationRequest_Exec(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + prepareConfigRequest PrepareConfigurationRequest + expected ConfigurationRequest + }{ + "directory": { + prepareConfigRequest: PrepareConfigurationRequest{ + Directory: func(request config.TestStepConfigRequest) string { return "directory" }, + }, + expected: ConfigurationRequest{ + Directory: Pointer("directory"), + File: Pointer(""), + Raw: Pointer(""), + }, + }, + "file": { + prepareConfigRequest: PrepareConfigurationRequest{ + File: func(request config.TestStepConfigRequest) string { return "file" }, + }, + expected: ConfigurationRequest{ + Directory: Pointer(""), + File: Pointer("file"), + Raw: Pointer(""), + }, + }, + "raw": { + prepareConfigRequest: PrepareConfigurationRequest{ + Raw: "str", + }, + expected: ConfigurationRequest{ + Directory: Pointer(""), + File: Pointer(""), + Raw: Pointer("str"), + }, + }, + "directory-file": { + prepareConfigRequest: PrepareConfigurationRequest{ + Directory: func(request config.TestStepConfigRequest) string { return "directory" }, + File: func(request config.TestStepConfigRequest) string { return "file" }, + }, + expected: ConfigurationRequest{ + Directory: Pointer("directory"), + File: Pointer("file"), + Raw: Pointer(""), + }, + }, + "directory-raw": { + prepareConfigRequest: PrepareConfigurationRequest{ + Directory: func(request config.TestStepConfigRequest) string { return "directory" }, + Raw: "str", + }, + expected: ConfigurationRequest{ + Directory: Pointer("directory"), + File: Pointer(""), + Raw: Pointer("str"), + }, + }, + "file-raw": { + prepareConfigRequest: PrepareConfigurationRequest{ + File: func(request config.TestStepConfigRequest) string { return "file" }, + Raw: "str", + }, + expected: ConfigurationRequest{ + Directory: Pointer(""), + File: Pointer("file"), + Raw: Pointer("str"), + }, + }, + "directory-file-raw": { + prepareConfigRequest: PrepareConfigurationRequest{ + Directory: func(request config.TestStepConfigRequest) string { return "directory" }, + File: func(request config.TestStepConfigRequest) string { return "file" }, + Raw: "str", + }, + expected: ConfigurationRequest{ + Directory: Pointer("directory"), + File: Pointer("file"), + Raw: Pointer("str"), + }, + }, + } + + comparer := cmp.Comparer(func(x, y ConfigurationRequest) bool { + if x.Directory != nil && y.Directory == nil { + return false + } + + if x.Directory == nil && y.Directory != nil { + return false + } + + if *x.Directory != *y.Directory { + return false + } + + if x.File != nil && y.File == nil { + return false + } + + if x.File == nil && y.File != nil { + return false + } + + if *x.File != *y.File { + return false + } + + if x.Raw != nil && y.Raw == nil { + return false + } + + if x.Raw == nil && y.Raw != nil { + return false + } + + if *x.Raw != *y.Raw { + return false + } + + return true + }) + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.prepareConfigRequest.Exec() + + if diff := cmp.Diff(testCase.expected, got, comparer); diff != "" { + t.Errorf("expected %+v, got %+v", testCase.expected, got) + } + + }) + } +} + +func TestConfigurationRequest_Validate(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + configRequest ConfigurationRequest + expectedError string + }{ + "directory": { + configRequest: ConfigurationRequest{ + Directory: Pointer("directory"), + }, + }, + "file": { + configRequest: ConfigurationRequest{ + Raw: Pointer("file"), + }, + }, + "raw": { + configRequest: ConfigurationRequest{ + Raw: Pointer("raw"), + }, + }, + "directory-file": { + configRequest: ConfigurationRequest{ + Directory: Pointer("directory"), + File: Pointer("file"), + }, + expectedError: `directory and file are populated, only one of "directory", "file", or "raw" is allowed`, + }, + "directory-raw": { + configRequest: ConfigurationRequest{ + Directory: Pointer("directory"), + Raw: Pointer("raw"), + }, + expectedError: `directory and raw are populated, only one of "directory", "file", or "raw" is allowed`, + }, + "directory-file-raw": { + configRequest: ConfigurationRequest{ + Directory: Pointer("directory"), + File: Pointer("file"), + Raw: Pointer("raw"), + }, + expectedError: `directory, file and raw are populated, only one of "directory", "file", or "raw" is allowed`, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + err := testCase.configRequest.Validate() + + if testCase.expectedError == "" && err != nil { + t.Errorf("unexpected error %s", err) + } + + if testCase.expectedError != "" && err == nil { + t.Errorf("expected error but got none") + } + + if testCase.expectedError != "" && err != nil { + if diff := cmp.Diff(err.Error(), testCase.expectedError); diff != "" { + t.Errorf("expected error %s, got error %s", testCase.expectedError, err) + } + } + }) + } +} + +func TestConfiguration(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + configRequest ConfigurationRequest + expected Config + }{ + "directory": { + configRequest: ConfigurationRequest{ + Directory: Pointer("directory"), + }, + expected: configurationDirectory{ + directory: "directory", + }, + }, + "file": { + configRequest: ConfigurationRequest{ + File: Pointer("file"), + }, + expected: configurationFile{ + file: "file", + }, + }, + "raw": { + configRequest: ConfigurationRequest{ + Raw: Pointer("str"), + }, + expected: configurationString{ + raw: "str", + }, + }, + } + + allowUnexported := cmp.AllowUnexported( + configurationDirectory{}, + configurationFile{}, + configurationString{}, + ) + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := Configuration(testCase.configRequest) + + if diff := cmp.Diff(testCase.expected, got, allowUnexported); diff != "" { + t.Errorf("expected %+v, got %+v", testCase.expected, got) + } + }) + } +} diff --git a/internal/teststep/directory.go b/internal/teststep/directory.go new file mode 100644 index 000000000..1afc45a2d --- /dev/null +++ b/internal/teststep/directory.go @@ -0,0 +1,132 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package teststep + +import ( + "context" + "fmt" + "hash/crc32" + "os" + "path/filepath" +) + +var _ Config = configurationDirectory{} + +type configurationDirectory struct { + directory string + + // appendedConfig is a map of filenames to content + appendedConfig map[string]string +} + +// HasConfigurationFiles is used during validation to ensure that +// ExternalProviders are not declared at the TestCase or TestStep +// level when using TestStep.ConfigDirectory. +func (c configurationDirectory) HasConfigurationFiles() bool { + return true +} + +// HasProviderBlock returns true if the Config has declared a provider +// configuration block, e.g. provider "examplecloud" {...} +func (c configurationDirectory) HasProviderBlock(ctx context.Context) (bool, error) { + configDirectory := c.directory + + if !filepath.IsAbs(configDirectory) { + pwd, err := os.Getwd() + + if err != nil { + return false, err + } + + configDirectory = filepath.Join(pwd, configDirectory) + } + + contains, err := filesContains(configDirectory, providerConfigBlockRegex) + + if err != nil { + return false, err + } + + return contains, nil +} + +// HasTerraformBlock returns true if the Config has declared a terraform +// configuration block, e.g. terraform {...} +func (c configurationDirectory) HasTerraformBlock(ctx context.Context) (bool, error) { + configDirectory := c.directory + + if !filepath.IsAbs(configDirectory) { + pwd, err := os.Getwd() + + if err != nil { + return false, err + } + + configDirectory = filepath.Join(pwd, configDirectory) + } + + contains, err := filesContains(configDirectory, terraformConfigBlockRegex) + + if err != nil { + return false, err + } + + return contains, nil +} + +func (c configurationDirectory) WriteQuery(ctx context.Context, dest string) error { + panic("WriteQuery not supported for configurationDirectory") +} + +// Write copies all files from directory to destination. +func (c configurationDirectory) Write(ctx context.Context, dest string) error { + configDirectory := c.directory + + if !filepath.IsAbs(configDirectory) { + pwd, err := os.Getwd() + + if err != nil { + return err + } + + configDirectory = filepath.Join(pwd, configDirectory) + } + + err := copyFiles(configDirectory, dest) + if err != nil { + return err + } + + err = c.writeAppendedConfig(dest) + if err != nil { + return err + } + + return nil +} + +func (c configurationDirectory) Append(config string) Config { + if c.appendedConfig == nil { + c.appendedConfig = make(map[string]string) + } + + checksum := crc32.Checksum([]byte(config), crc32.IEEETable) + filename := fmt.Sprintf("terraform_plugin_test_%d.tf", checksum) + + c.appendedConfig[filename] = config + return c +} + +func (c configurationDirectory) writeAppendedConfig(dstPath string) error { + for filename, config := range c.appendedConfig { + outFilename := filepath.Join(dstPath, filename) + + err := os.WriteFile(outFilename, []byte(config), 0700) + if err != nil { + return err + } + } + + return nil +} diff --git a/internal/teststep/directory_test.go b/internal/teststep/directory_test.go new file mode 100644 index 000000000..cb88d214f --- /dev/null +++ b/internal/teststep/directory_test.go @@ -0,0 +1,699 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package teststep + +import ( + "context" + "os" + "path/filepath" + "regexp" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestConfigurationDirectory_HasProviderBlock(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + configDirectory configurationDirectory + expected bool + expectedError *regexp.Regexp + }{ + "not-directory": { + configDirectory: configurationDirectory{ + directory: "testdata/empty_file/main.tf", + }, + expectedError: regexp.MustCompile(`.*not a directory`), + }, + "no-config": { + configDirectory: configurationDirectory{ + directory: "testdata/empty_dir", + }, + expected: false, + }, + "provider-meta-attribute": { + configDirectory: configurationDirectory{ + directory: "testdata/provider_meta_attribute", + }, + expected: false, + }, + "provider-object-attribute": { + configDirectory: configurationDirectory{ + directory: "testdata/provider_object_attribute", + }, + expected: false, + }, + "provider-string-attribute": { + configDirectory: configurationDirectory{ + directory: "testdata/provider_string_attribute", + }, + expected: false, + }, + "provider-block-quoted-with-attributes": { + configDirectory: configurationDirectory{ + directory: "testdata/provider_block_quoted_with_attributes", + }, + expected: true, + }, + "provider-block-quoted-with-attributes-no-spaces": { + configDirectory: configurationDirectory{ + directory: "testdata/provider_block_quoted_with_attributes_no_spaces", + }, + expected: true, + }, + "provider-block-unquoted-with-attributes": { + configDirectory: configurationDirectory{ + directory: "testdata/provider_block_unquoted_with_attributes", + }, + expected: true, + }, + "provider-block-unquoted-with-attributes_no-trailing-space": { + configDirectory: configurationDirectory{ + directory: "testdata/provider_block_unquoted_with_attributes_no_trailing_space", + }, + expected: true, + }, + "provider-block-quoted-without-attributes": { + configDirectory: configurationDirectory{ + directory: "testdata/provider_block_quoted_without_attributes", + }, + expected: true, + }, + "provider-block-quoted-without-attributes-no-spaces": { + configDirectory: configurationDirectory{ + directory: "testdata/provider_block_quoted_without_attributes_no_spaces", + }, + expected: true, + }, + "provider-block-unquoted-without-attributes": { + configDirectory: configurationDirectory{ + directory: "testdata/provider_block_unquoted_without_attributes", + }, + expected: true, + }, + "provider-block-unquoted-without-attributes-no-trailing-space": { + configDirectory: configurationDirectory{ + directory: "testdata/provider_block_unquoted_without_attributes_no_trailing_space", + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.configDirectory.HasProviderBlock(context.Background()) + + if testCase.expectedError == nil && err != nil { + t.Errorf("unexpected error %s", err) + } + + if testCase.expectedError != nil && err == nil { + t.Errorf("expected error but got none") + } + + if testCase.expectedError != nil && err != nil { + if !testCase.expectedError.MatchString(err.Error()) { + t.Errorf("expected error %s, got error %s", testCase.expectedError.String(), err) + } + } + + if diff := cmp.Diff(testCase.expected, got); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestConfigurationDirectory_HasProviderBlock_AbsolutePath(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + configDirectory configurationDirectory + expected bool + expectedError *regexp.Regexp + }{ + "not-directory": { + configDirectory: configurationDirectory{ + directory: "testdata/empty_file/main.tf", + }, + expectedError: regexp.MustCompile(`.*not a directory`), + }, + "no-config": { + configDirectory: configurationDirectory{ + directory: "testdata/empty_dir", + }, + expected: false, + }, + "provider-meta-attribute": { + configDirectory: configurationDirectory{ + directory: "testdata/provider_meta_attribute", + }, + expected: false, + }, + "provider-object-attribute": { + configDirectory: configurationDirectory{ + directory: "testdata/provider_object_attribute", + }, + expected: false, + }, + "provider-string-attribute": { + configDirectory: configurationDirectory{ + directory: "testdata/provider_string_attribute", + }, + expected: false, + }, + "provider-block-quoted-with-attributes": { + configDirectory: configurationDirectory{ + directory: "testdata/provider_block_quoted_with_attributes", + }, + expected: true, + }, + "provider-block-quoted-with-attributes-no-spaces": { + configDirectory: configurationDirectory{ + directory: "testdata/provider_block_quoted_with_attributes_no_spaces", + }, + expected: true, + }, + "provider-block-unquoted-with-attributes": { + configDirectory: configurationDirectory{ + directory: "testdata/provider_block_unquoted_with_attributes", + }, + expected: true, + }, + "provider-block-unquoted-with-attributes_no-trailing-space": { + configDirectory: configurationDirectory{ + directory: "testdata/provider_block_unquoted_with_attributes_no_trailing_space", + }, + expected: true, + }, + "provider-block-quoted-without-attributes": { + configDirectory: configurationDirectory{ + directory: "testdata/provider_block_quoted_without_attributes", + }, + expected: true, + }, + "provider-block-quoted-without-attributes-no-spaces": { + configDirectory: configurationDirectory{ + directory: "testdata/provider_block_quoted_without_attributes_no_spaces", + }, + expected: true, + }, + "provider-block-unquoted-without-attributes": { + configDirectory: configurationDirectory{ + directory: "testdata/provider_block_unquoted_without_attributes", + }, + expected: true, + }, + "provider-block-unquoted-without-attributes-no-trailing-space": { + configDirectory: configurationDirectory{ + directory: "testdata/provider_block_unquoted_without_attributes_no_trailing_space", + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + pwd, err := os.Getwd() + + if err != nil { + t.Errorf("error getting wd: %s", err) + } + + testCase.configDirectory.directory = filepath.Join(pwd, testCase.configDirectory.directory) + + got, err := testCase.configDirectory.HasProviderBlock(context.Background()) + + if testCase.expectedError == nil && err != nil { + t.Errorf("unexpected error %s", err) + } + + if testCase.expectedError != nil && err == nil { + t.Errorf("expected error but got none") + } + + if testCase.expectedError != nil && err != nil { + if !testCase.expectedError.MatchString(err.Error()) { + t.Errorf("expected error %s, got error %s", testCase.expectedError.String(), err) + } + } + + if diff := cmp.Diff(testCase.expected, got); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestConfigurationDirectory_HasTerraformBlock(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + configDirectory configurationDirectory + expected bool + expectedError *regexp.Regexp + }{ + "not-directory": { + configDirectory: configurationDirectory{ + directory: "testdata/empty_file/main.tf", + }, + expectedError: regexp.MustCompile(`.*not a directory`), + }, + "no-config": { + configDirectory: configurationDirectory{ + directory: "testdata/empty_dir", + }, + expected: false, + }, + "terraform-meta-attribute": { + configDirectory: configurationDirectory{ + directory: "testdata/terraform_meta_attribute", + }, + expected: false, + }, + "terraform-object-attribute": { + configDirectory: configurationDirectory{ + directory: "testdata/terraform_object_attribute", + }, + expected: false, + }, + "terraform-string-attribute": { + configDirectory: configurationDirectory{ + directory: "testdata/terraform_string_attribute", + }, + expected: false, + }, + "terraform-block": { + configDirectory: configurationDirectory{ + directory: "testdata/terraform_block", + }, + expected: true, + }, + "terraform-block-no-space": { + configDirectory: configurationDirectory{ + directory: "testdata/terraform_block_no_space", + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.configDirectory.HasTerraformBlock(context.Background()) + + if testCase.expectedError == nil && err != nil { + t.Errorf("unexpected error %s", err) + } + + if testCase.expectedError != nil && err == nil { + t.Errorf("expected error but got none") + } + + if testCase.expectedError != nil && err != nil { + if !testCase.expectedError.MatchString(err.Error()) { + t.Errorf("expected error %s, got error %s", testCase.expectedError.String(), err) + } + } + + if diff := cmp.Diff(testCase.expected, got); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestConfigurationDirectory_HasTerraformBlock_AbsolutePath(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + configDirectory configurationDirectory + expected bool + expectedError *regexp.Regexp + }{ + "not-directory": { + configDirectory: configurationDirectory{ + directory: "testdata/empty_file/main.tf", + }, + expectedError: regexp.MustCompile(`.*not a directory`), + }, + "no-config": { + configDirectory: configurationDirectory{ + directory: "testdata/empty_dir", + }, + expected: false, + }, + "terraform-meta-attribute": { + configDirectory: configurationDirectory{ + directory: "testdata/terraform_meta_attribute", + }, + expected: false, + }, + "terraform-object-attribute": { + configDirectory: configurationDirectory{ + directory: "testdata/terraform_object_attribute", + }, + expected: false, + }, + "terraform-string-attribute": { + configDirectory: configurationDirectory{ + directory: "testdata/terraform_string_attribute", + }, + expected: false, + }, + "terraform-block": { + configDirectory: configurationDirectory{ + directory: "testdata/terraform_block", + }, + expected: true, + }, + "terraform-block-no-space": { + configDirectory: configurationDirectory{ + directory: "testdata/terraform_block_no_space", + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + pwd, err := os.Getwd() + + if err != nil { + t.Errorf("error getting wd: %s", err) + } + + testCase.configDirectory.directory = filepath.Join(pwd, testCase.configDirectory.directory) + + got, err := testCase.configDirectory.HasTerraformBlock(context.Background()) + + if testCase.expectedError == nil && err != nil { + t.Errorf("unexpected error %s", err) + } + + if testCase.expectedError != nil && err == nil { + t.Errorf("expected error but got none") + } + + if testCase.expectedError != nil && err != nil { + if !testCase.expectedError.MatchString(err.Error()) { + t.Errorf("expected error %s, got error %s", testCase.expectedError.String(), err) + } + } + + if diff := cmp.Diff(testCase.expected, got); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestConfigurationDirectory_Write(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + configDirectory configurationDirectory + expectedError *regexp.Regexp + }{ + "not-directory": { + configDirectory: configurationDirectory{ + directory: "testdata/empty_file/main.tf", + }, + expectedError: regexp.MustCompile(`.*not a directory`), + }, + "no-config": { + configDirectory: configurationDirectory{ + directory: "testdata/empty_dir", + }, + }, + "dir-single-file": { + configDirectory: configurationDirectory{ + directory: "testdata/random", + }, + }, + "dir-multiple-files": { + configDirectory: configurationDirectory{ + directory: "testdata/random_multiple_files", + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + tempDir := t.TempDir() + + err := testCase.configDirectory.Write(context.Background(), tempDir) + + if testCase.expectedError == nil && err != nil { + t.Errorf("unexpected error %s", err) + } + + if testCase.expectedError != nil && err == nil { + t.Errorf("expected error but got none") + } + + if testCase.expectedError != nil && err != nil { + if !testCase.expectedError.MatchString(err.Error()) { + t.Errorf("expected error %s, got error %s", testCase.expectedError.String(), err) + } + } + + if err == nil { + dirEntries, err := os.ReadDir(testCase.configDirectory.directory) + + if err != nil { + t.Errorf("error reading directory: %s", err) + } + + tempDirEntries, err := os.ReadDir(tempDir) + + if err != nil { + t.Errorf("error reading temp directory: %s", err) + } + + if len(dirEntries) != len(tempDirEntries) { + t.Errorf("expected %d dir entries, got %d dir entries", dirEntries, tempDirEntries) + } + + for k, v := range dirEntries { + dirEntryInfo, err := v.Info() + + if err != nil { + t.Errorf("error getting dir entry info: %s", err) + } + + tempDirEntryInfo, err := tempDirEntries[k].Info() + + if err != nil { + t.Errorf("error getting temp dir entry info: %s", err) + } + + if diff := cmp.Diff(tempDirEntryInfo, dirEntryInfo, fileInfoComparer); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + } + } + }) + } +} + +func TestConfigurationDirectory_Write_AbsolutePath(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + configDirectory configurationDirectory + expectedError *regexp.Regexp + }{ + "not-directory": { + configDirectory: configurationDirectory{ + directory: "testdata/empty_file/main.tf", + }, + expectedError: regexp.MustCompile(`.*not a directory`), + }, + "no-config": { + configDirectory: configurationDirectory{ + directory: "testdata/empty_dir", + }, + }, + "dir-single-file": { + configDirectory: configurationDirectory{ + directory: "testdata/random", + }, + }, + "dir-multiple-files": { + configDirectory: configurationDirectory{ + directory: "testdata/random_multiple_files", + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + tempDir := t.TempDir() + + pwd, err := os.Getwd() + + if err != nil { + t.Errorf("error getting wd: %s", err) + } + + testCase.configDirectory.directory = filepath.Join(pwd, testCase.configDirectory.directory) + + err = testCase.configDirectory.Write(context.Background(), tempDir) + + if testCase.expectedError == nil && err != nil { + t.Errorf("unexpected error %s", err) + } + + if testCase.expectedError != nil && err == nil { + t.Errorf("expected error but got none") + } + + if testCase.expectedError != nil && err != nil { + if !testCase.expectedError.MatchString(err.Error()) { + t.Errorf("expected error %s, got error %s", testCase.expectedError.String(), err) + } + } + + if err == nil { + dirEntries, err := os.ReadDir(testCase.configDirectory.directory) + + if err != nil { + t.Errorf("error reading directory: %s", err) + } + + tempDirEntries, err := os.ReadDir(tempDir) + + if err != nil { + t.Errorf("error reading temp directory: %s", err) + } + + if len(dirEntries) != len(tempDirEntries) { + t.Errorf("expected %d dir entries, got %d dir entries", dirEntries, tempDirEntries) + } + + for k, v := range dirEntries { + dirEntryInfo, err := v.Info() + + if err != nil { + t.Errorf("error getting dir entry info: %s", err) + } + + tempDirEntryInfo, err := tempDirEntries[k].Info() + + if err != nil { + t.Errorf("error getting temp dir entry info: %s", err) + } + + if diff := cmp.Diff(tempDirEntryInfo, dirEntryInfo, fileInfoComparer); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + } + } + }) + } +} + +func TestConfigurationDirectory_Write_WithAppendedConfig(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + configDirectory configurationDirectory + expectedError *regexp.Regexp + }{ + "dir-single-file": { + configDirectory: configurationDirectory{ + directory: "testdata/random", + appendedConfig: map[string]string{ + "import.tf": `terraform {\nimport\n{\nto = satellite.the_moon\nid = "moon"\n}\n}\n`, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + tempDir := t.TempDir() + + err := testCase.configDirectory.Write(context.Background(), tempDir) + if err != nil { + t.Errorf("unexpected error %s", err) + } + + dirEntries, err := os.ReadDir(testCase.configDirectory.directory) + if err != nil { + t.Errorf("error reading directory: %s", err) + } + + tempDirEntries, err := os.ReadDir(tempDir) + + if err != nil { + t.Errorf("error reading temp directory: %s", err) + } + + if len(tempDirEntries)-len(dirEntries) != 1 { + t.Errorf("expected %d dir entries, got %d dir entries", len(dirEntries)+1, tempDirEntries) + } + + for _, entry := range dirEntries { + filename := entry.Name() + expectedContent, err := os.ReadFile(filepath.Join(testCase.configDirectory.directory, filename)) + if err != nil { + t.Errorf("error reading file from config directory %s: %s", filename, err) + } + + content, err := os.ReadFile(filepath.Join(tempDir, filename)) + if err != nil { + t.Errorf("error reading generated file %s: %s", filename, err) + } + + if diff := cmp.Diff(expectedContent, content); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + } + + appendedConfigFiles := testCase.configDirectory.appendedConfig + for filename, expectedContent := range appendedConfigFiles { + content, err := os.ReadFile(filepath.Join(tempDir, filename)) + if err != nil { + t.Errorf("error reading appended config file %s: %s", filename, err) + } + + if diff := cmp.Diff([]byte(expectedContent), content); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + } + }) + } +} + +var fileInfoComparer = cmp.Comparer(func(x, y os.FileInfo) bool { + if x.Name() != y.Name() { + return false + } + + if x.Mode() != y.Mode() { + return false + } + + if x.Size() != y.Size() { + return false + } + + return true +}) diff --git a/internal/teststep/file.go b/internal/teststep/file.go new file mode 100644 index 000000000..a4862b05d --- /dev/null +++ b/internal/teststep/file.go @@ -0,0 +1,112 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package teststep + +import ( + "context" + "os" + "path/filepath" +) + +var _ Config = configurationFile{} + +type configurationFile struct { + file string + appendedConfig string +} + +// HasConfigurationFiles is used during validation to ensure that +// ExternalProviders are not declared at the TestCase or TestStep +// level when using TestStep.ConfigFile. +func (c configurationFile) HasConfigurationFiles() bool { + return true +} + +// HasProviderBlock returns true if the Config has declared a provider +// configuration block, e.g. provider "examplecloud" {...} +func (c configurationFile) HasProviderBlock(ctx context.Context) (bool, error) { + configFile := c.file + + if !filepath.IsAbs(configFile) { + pwd, err := os.Getwd() + + if err != nil { + return false, err + } + + configFile = filepath.Join(pwd, configFile) + } + + contains, err := fileContains(configFile, providerConfigBlockRegex) + + if err != nil { + return false, err + } + + return contains, nil +} + +// HasTerraformBlock returns true if the Config has declared a terraform +// configuration block, e.g. terraform {...} +func (c configurationFile) HasTerraformBlock(ctx context.Context) (bool, error) { + configFile := c.file + + if !filepath.IsAbs(configFile) { + pwd, err := os.Getwd() + + if err != nil { + return false, err + } + + configFile = filepath.Join(pwd, configFile) + } + + contains, err := fileContains(configFile, terraformConfigBlockRegex) + + if err != nil { + return false, err + } + + return contains, nil +} + +func (c configurationFile) WriteQuery(ctx context.Context, dest string) error { + panic("WriteQuery not supported for configurationFile") +} + +// Write copies file from c.file to destination. +func (c configurationFile) Write(ctx context.Context, dest string) error { + configFile := c.file + + if !filepath.IsAbs(configFile) { + pwd, err := os.Getwd() + + if err != nil { + return err + } + + configFile = filepath.Join(pwd, configFile) + } + + destPath, err := copyFile(configFile, dest) + if err != nil { + return err + } + + if len(c.appendedConfig) > 0 { + err := appendToFile(destPath, c.appendedConfig) + if err != nil { + return err + } + } + + return nil +} + +func (c configurationFile) Append(config string) Config { + return configurationFile{ + file: c.file, + appendedConfig: config, + } +} diff --git a/internal/teststep/file_test.go b/internal/teststep/file_test.go new file mode 100644 index 000000000..b6f89bed6 --- /dev/null +++ b/internal/teststep/file_test.go @@ -0,0 +1,599 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package teststep + +import ( + "context" + "os" + "path/filepath" + "regexp" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-testing/config" +) + +func TestConfigurationFile_HasProviderBlock(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + configFile configurationFile + expected bool + expectedError *regexp.Regexp + }{ + "not-file": { + configFile: configurationFile{ + file: "testdata/empty_file/not_a_real_file.tf", + }, + expectedError: regexp.MustCompile(`.*no such file or directory`), + }, + "no-config": { + configFile: configurationFile{ + file: "testdata/empty_file/main.tf", + }, + expected: false, + }, + "provider-meta-attribute": { + configFile: configurationFile{ + file: "testdata/provider_meta_attribute/main.tf", + }, + expected: false, + }, + "provider-object-attribute": { + configFile: configurationFile{ + file: "testdata/provider_object_attribute/main.tf", + }, + expected: false, + }, + "provider-string-attribute": { + configFile: configurationFile{ + file: "testdata/provider_string_attribute/main.tf", + }, + expected: false, + }, + "provider-block-quoted-with-attributes": { + configFile: configurationFile{ + file: "testdata/provider_block_quoted_with_attributes/main.tf", + }, + expected: true, + }, + "provider-block-quoted-with-attributes-no-spaces": { + configFile: configurationFile{ + file: "testdata/provider_block_quoted_with_attributes_no_spaces/main.tf", + }, + expected: true, + }, + "provider-block-unquoted-with-attributes": { + configFile: configurationFile{ + file: "testdata/provider_block_unquoted_with_attributes/main.tf", + }, + expected: true, + }, + "provider-block-unquoted-with-attributes-no-trailing-space": { + configFile: configurationFile{ + file: "testdata/provider_block_unquoted_with_attributes_no_trailing_space/main.tf", + }, + expected: true, + }, + "provider-block-quoted-without-attributes": { + configFile: configurationFile{ + file: "testdata/provider_block_quoted_without_attributes/main.tf", + }, + expected: true, + }, + "provider-block-quoted-without-attributes-no-spaces": { + configFile: configurationFile{ + file: "testdata/provider_block_quoted_without_attributes_no_spaces/main.tf", + }, + expected: true, + }, + "provider-block-unquoted-without-attributes": { + configFile: configurationFile{ + file: "testdata/provider_block_unquoted_without_attributes/main.tf", + }, + expected: true, + }, + "provider-block-unquoted-without-attributes-no-trailing-space": { + configFile: configurationFile{ + file: "testdata/provider_block_unquoted_without_attributes_no_trailing_space/main.tf", + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.configFile.HasProviderBlock(context.Background()) + + if testCase.expectedError == nil && err != nil { + t.Errorf("unexpected error %s", err) + } + + if testCase.expectedError != nil && err == nil { + t.Errorf("expected error but got none") + } + + if testCase.expectedError != nil && err != nil { + if !testCase.expectedError.MatchString(err.Error()) { + t.Errorf("expected error %s, got error %s", testCase.expectedError.String(), err) + } + } + + if diff := cmp.Diff(testCase.expected, got); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestConfigurationFile_HasProviderBlock_AbsolutePath(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + configFile configurationFile + expected bool + expectedError *regexp.Regexp + }{ + "not-file": { + configFile: configurationFile{ + file: "testdata/empty_file/not_a_real_file.tf", + }, + expectedError: regexp.MustCompile(`.*no such file or directory`), + }, + "no-config": { + configFile: configurationFile{ + file: "testdata/empty_file/main.tf", + }, + expected: false, + }, + "provider-meta-attribute": { + configFile: configurationFile{ + file: "testdata/provider_meta_attribute/main.tf", + }, + expected: false, + }, + "provider-object-attribute": { + configFile: configurationFile{ + file: "testdata/provider_object_attribute/main.tf", + }, + expected: false, + }, + "provider-string-attribute": { + configFile: configurationFile{ + file: "testdata/provider_string_attribute/main.tf", + }, + expected: false, + }, + "provider-block-quoted-with-attributes": { + configFile: configurationFile{ + file: "testdata/provider_block_quoted_with_attributes/main.tf", + }, + expected: true, + }, + "provider-block-quoted-with-attributes-no-spaces": { + configFile: configurationFile{ + file: "testdata/provider_block_quoted_with_attributes_no_spaces/main.tf", + }, + expected: true, + }, + "provider-block-unquoted-with-attributes": { + configFile: configurationFile{ + file: "testdata/provider_block_unquoted_with_attributes/main.tf", + }, + expected: true, + }, + "provider-block-unquoted-with-attributes-no-trailing-space": { + configFile: configurationFile{ + file: "testdata/provider_block_unquoted_with_attributes_no_trailing_space/main.tf", + }, + expected: true, + }, + "provider-block-quoted-without-attributes": { + configFile: configurationFile{ + file: "testdata/provider_block_quoted_without_attributes/main.tf", + }, + expected: true, + }, + "provider-block-quoted-without-attributes-no-spaces": { + configFile: configurationFile{ + file: "testdata/provider_block_quoted_without_attributes_no_spaces/main.tf", + }, + expected: true, + }, + "provider-block-unquoted-without-attributes": { + configFile: configurationFile{ + file: "testdata/provider_block_unquoted_without_attributes/main.tf", + }, + expected: true, + }, + "provider-block-unquoted-without-attributes-no-trailing-space": { + configFile: configurationFile{ + file: "testdata/provider_block_unquoted_without_attributes_no_trailing_space/main.tf", + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + pwd, err := os.Getwd() + + if err != nil { + t.Errorf("error getting wd: %s", err) + } + + testCase.configFile.file = filepath.Join(pwd, testCase.configFile.file) + + got, err := testCase.configFile.HasProviderBlock(context.Background()) + + if testCase.expectedError == nil && err != nil { + t.Errorf("unexpected error %s", err) + } + + if testCase.expectedError != nil && err == nil { + t.Errorf("expected error but got none") + } + + if testCase.expectedError != nil && err != nil { + if !testCase.expectedError.MatchString(err.Error()) { + t.Errorf("expected error %s, got error %s", testCase.expectedError.String(), err) + } + } + + if diff := cmp.Diff(testCase.expected, got); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestConfigurationFile_HasTerraformBlock(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + configFile configurationFile + expected bool + expectedError *regexp.Regexp + }{ + "not-file": { + configFile: configurationFile{ + file: "testdata/empty_file/not_a_real_file.tf", + }, + expectedError: regexp.MustCompile(`.*no such file or directory`), + }, + "no-config": { + configFile: configurationFile{ + file: "testdata/empty_file/main.tf", + }, + expected: false, + }, + "terraform-meta-attribute": { + configFile: configurationFile{ + file: "testdata/terraform_meta_attribute/main.tf", + }, + expected: false, + }, + "terraform-object-attribute": { + configFile: configurationFile{ + file: "testdata/terraform_object_attribute/main.tf", + }, + expected: false, + }, + "terraform-string-attribute": { + configFile: configurationFile{ + file: "testdata/terraform_string_attribute/main.tf", + }, + expected: false, + }, + "terraform-block": { + configFile: configurationFile{ + file: "testdata/terraform_block/main.tf", + }, + expected: true, + }, + "terraform-block-no-space": { + configFile: configurationFile{ + file: "testdata/terraform_block_no_space/main.tf", + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.configFile.HasTerraformBlock(context.Background()) + + if testCase.expectedError == nil && err != nil { + t.Errorf("unexpected error %s", err) + } + + if testCase.expectedError != nil && err == nil { + t.Errorf("expected error but got none") + } + + if testCase.expectedError != nil && err != nil { + if !testCase.expectedError.MatchString(err.Error()) { + t.Errorf("expected error %s, got error %s", testCase.expectedError.String(), err) + } + } + + if diff := cmp.Diff(testCase.expected, got); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestConfigurationFile_HasTerraformBlock_AbsolutePath(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + configFile configurationFile + expected bool + expectedError *regexp.Regexp + }{ + "not-file": { + configFile: configurationFile{ + file: "testdata/empty_file/not_a_real_file.tf", + }, + expectedError: regexp.MustCompile(`.*no such file or directory`), + }, + "no-config": { + configFile: configurationFile{ + file: "testdata/empty_file/main.tf", + }, + expected: false, + }, + "terraform-meta-attribute": { + configFile: configurationFile{ + file: "testdata/terraform_meta_attribute/main.tf", + }, + expected: false, + }, + "terraform-object-attribute": { + configFile: configurationFile{ + file: "testdata/terraform_object_attribute/main.tf", + }, + expected: false, + }, + "terraform-string-attribute": { + configFile: configurationFile{ + file: "testdata/terraform_string_attribute/main.tf", + }, + expected: false, + }, + "terraform-block": { + configFile: configurationFile{ + file: "testdata/terraform_block/main.tf", + }, + expected: true, + }, + "terraform-block-no-space": { + configFile: configurationFile{ + file: "testdata/terraform_block_no_space/main.tf", + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + pwd, err := os.Getwd() + + if err != nil { + t.Errorf("error getting wd: %s", err) + } + + testCase.configFile.file = filepath.Join(pwd, testCase.configFile.file) + + got, err := testCase.configFile.HasTerraformBlock(context.Background()) + + if testCase.expectedError == nil && err != nil { + t.Errorf("unexpected error %s", err) + } + + if testCase.expectedError != nil && err == nil { + t.Errorf("expected error but got none") + } + + if testCase.expectedError != nil && err != nil { + if !testCase.expectedError.MatchString(err.Error()) { + t.Errorf("expected error %s, got error %s", testCase.expectedError.String(), err) + } + } + + if diff := cmp.Diff(testCase.expected, got); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestConfigurationFile_Write(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + configFile configurationFile + expectedError *regexp.Regexp + }{ + "not-file": { + configFile: configurationFile{ + file: "testdata/empty_file/not_a_real_file.tf", + }, + expectedError: regexp.MustCompile(`.*no such file or directory`), + }, + "file": { + configFile: configurationFile{ + file: "testdata/random/random.tf", + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + tempDir := t.TempDir() + + err := testCase.configFile.Write(context.Background(), tempDir) + + if testCase.expectedError == nil && err != nil { + t.Errorf("unexpected error %s", err) + } + + if testCase.expectedError != nil && err == nil { + t.Errorf("expected error but got none") + } + + if testCase.expectedError != nil && err != nil { + if !testCase.expectedError.MatchString(err.Error()) { + t.Errorf("expected error %s, got error %s", testCase.expectedError.String(), err) + } + } + + if err == nil { + fileInfo, err := os.Lstat(testCase.configFile.file) + + if err != nil { + t.Errorf("error getting dir entry info: %s", err) + } + + tempFileInfo, err := os.Lstat(filepath.Join(tempDir, filepath.Base(testCase.configFile.file))) + + if err != nil { + t.Errorf("error getting temp dir entry info: %s", err) + } + + if diff := cmp.Diff(tempFileInfo, fileInfo, fileInfoComparer); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + } + }) + } +} + +func TestConfigurationFile_Write_AbsolutePath(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + configFile configurationFile + expectedError *regexp.Regexp + }{ + "not-file": { + configFile: configurationFile{ + file: "testdata/empty_file/not_a_real_file.tf", + }, + expectedError: regexp.MustCompile(`.*no such file or directory`), + }, + "file": { + configFile: configurationFile{ + file: "testdata/random/random.tf", + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + tempDir := t.TempDir() + + pwd, err := os.Getwd() + + if err != nil { + t.Errorf("error getting wd: %s", err) + } + + testCase.configFile.file = filepath.Join(pwd, testCase.configFile.file) + + err = testCase.configFile.Write(context.Background(), tempDir) + + if testCase.expectedError == nil && err != nil { + t.Errorf("unexpected error %s", err) + } + + if testCase.expectedError != nil && err == nil { + t.Errorf("expected error but got none") + } + + if testCase.expectedError != nil && err != nil { + if !testCase.expectedError.MatchString(err.Error()) { + t.Errorf("expected error %s, got error %s", testCase.expectedError.String(), err) + } + } + + if err == nil { + fileInfo, err := os.Lstat(testCase.configFile.file) + + if err != nil { + t.Errorf("error getting dir entry info: %s", err) + } + + tempFileInfo, err := os.Lstat(filepath.Join(tempDir, filepath.Base(testCase.configFile.file))) + + if err != nil { + t.Errorf("error getting temp dir entry info: %s", err) + } + + if diff := cmp.Diff(tempFileInfo, fileInfo, fileInfoComparer); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + } + }) + } +} + +func TestConfigFile_Append(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + filename string + appendContent string + expectedContent string + }{ + "append content to a ConfigFile": { + filename: `testdata/main.tf`, // Contains `// Hello world` + appendContent: `terraform {}`, + expectedContent: "# Copyright (c) HashiCorp, Inc.\n# SPDX-License-Identifier: MPL-2.0\n\n// Hello world" + "\n" + "terraform {}", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + prepareConfigRequest := PrepareConfigurationRequest{ + File: func(config.TestStepConfigRequest) string { + return testCase.filename + }, + } + + teststepConfig := Configuration(prepareConfigRequest.Exec()) + teststepConfig = teststepConfig.Append(testCase.appendContent) + + tempdir := t.TempDir() + if err := teststepConfig.Write(context.Background(), tempdir); err != nil { + t.Fatalf("failed to write file: %s", err) + } + + got, err := os.ReadFile(filepath.Join(tempdir, filepath.Base(testCase.filename))) + if err != nil { + t.Fatalf("failed to read file: %s", err) + } + + gotS := string(got[:]) + if diff := cmp.Diff(testCase.expectedContent, gotS); diff != "" { + t.Errorf("expected %+v, got %+v", testCase.expectedContent, gotS) + } + }) + } +} diff --git a/internal/teststep/string.go b/internal/teststep/string.go new file mode 100644 index 000000000..ccb8fb3e8 --- /dev/null +++ b/internal/teststep/string.go @@ -0,0 +1,82 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package teststep + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" +) + +var _ Config = configurationString{} + +type configurationString struct { + raw string +} + +// HasConfigurationFiles is used during validation to allow declaration +// of ExternalProviders at the TestCase or TestStep level when using +// TestStep.Config. +func (c configurationString) HasConfigurationFiles() bool { + return false +} + +// HasProviderBlock returns true if the Config has declared a provider +// configuration block, e.g. provider "examplecloud" {...} +func (c configurationString) HasProviderBlock(ctx context.Context) (bool, error) { + return providerConfigBlockRegex.MatchString(c.raw), nil +} + +// HasTerraformBlock returns true if the Config has declared a terraform +// configuration block, e.g. terraform {...} +func (c configurationString) HasTerraformBlock(ctx context.Context) (bool, error) { + return terraformConfigBlockRegex.MatchString(c.raw), nil +} + +// Write creates a file and writes c.raw into it. +func (c configurationString) Write(ctx context.Context, dest string) error { + outFilename := filepath.Join(dest, rawConfigFileName) + rmFilename := filepath.Join(dest, rawConfigFileNameJSON) + + bCfg := []byte(c.raw) + + if json.Valid(bCfg) { + outFilename, rmFilename = rmFilename, outFilename + } + + if err := os.Remove(rmFilename); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("unable to remove %q: %w", rmFilename, err) + } + + err := os.WriteFile(outFilename, bCfg, 0700) + + if err != nil { + return err + } + + return nil +} + +// WriteQuery creates a file and writes c.raw into it. +func (c configurationString) WriteQuery(ctx context.Context, dest string) error { + outFilename := filepath.Join(dest, rawQueryConfigFileName) + + bCfg := []byte(c.raw) + + err := os.WriteFile(outFilename, bCfg, 0700) + if err != nil { + return err + } + + return nil +} + +func (c configurationString) Append(config string) Config { + return configurationString{ + raw: strings.Join([]string{c.raw, config}, "\n"), + } +} diff --git a/internal/teststep/string_test.go b/internal/teststep/string_test.go new file mode 100644 index 000000000..a55501efc --- /dev/null +++ b/internal/teststep/string_test.go @@ -0,0 +1,302 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package teststep + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestConfiguration_HasProviderBlock(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + configRaw configurationString + expected bool + }{ + "no-config": { + expected: false, + }, + "provider-meta-attribute": { + configRaw: configurationString{ + raw: ` +resource "test_test" "test" { + provider = test.test +} +`, + }, + expected: false, + }, + "provider-object-attribute": { + configRaw: configurationString{ + raw: ` +resource "test_test" "test" { + test = { + provider = { + test = true + } + } +} +`, + }, + expected: false, + }, + "provider-string-attribute": { + configRaw: configurationString{ + raw: ` +resource "test_test" "test" { + test = { + provider = "test" + } +} +`, + }, + expected: false, + }, + "provider-block-quoted-with-attributes": { + configRaw: configurationString{ + raw: ` +provider "test" { + test = true +} + +resource "test_test" "test" {} +`, + }, + expected: true, + }, + "provider-block-quoted-with-attributes-no-spaces": { + configRaw: configurationString{ + raw: ` +provider"test"{ + test = true +} + +resource "test_test" "test" {} +`, + }, + expected: true, + }, + "provider-block-unquoted-with-attributes": { + configRaw: configurationString{ + raw: ` +provider test { + test = true +} + +resource "test_test" "test" {} +`, + }, + expected: true, + }, + "provider-block-unquoted-with-attributes-no-trailing-space": { + configRaw: configurationString{ + raw: ` +provider test{ + test = true +} + +resource "test_test" "test" {} +`, + }, + expected: true, + }, + "provider-block-quoted-without-attributes": { + configRaw: configurationString{ + raw: ` +provider "test" {} + +resource "test_test" "test" {} +`, + }, + expected: true, + }, + "provider-block-quoted-without-attributes-no-spaces": { + configRaw: configurationString{ + raw: ` +provider"test"{} + +resource "test_test" "test" {} +`, + }, + expected: true, + }, + "provider-block-unquoted-without-attributes": { + configRaw: configurationString{ + raw: ` +provider test {} + +resource "test_test" "test" {} +`, + }, + expected: true, + }, + "provider-block-unquoted-without-attributes-no-trailing-space": { + configRaw: configurationString{ + raw: ` +provider test{} + +resource "test_test" "test" {} +`, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.configRaw.HasProviderBlock(context.Background()) + + if err != nil { + t.Errorf("unexpected error: %s", err) + } + + if diff := cmp.Diff(testCase.expected, got); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestConfiguration_HasTerraformBlock(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + configRaw configurationString + expected bool + }{ + "no-config": { + expected: false, + }, + "terraform-meta-attribute": { + configRaw: configurationString{ + raw: ` +resource "test_test" "test" { + terraform = test.test +} +`, + }, + expected: false, + }, + "terraform-object-attribute": { + configRaw: configurationString{ + raw: ` +resource "test_test" "test" { + test = { + terraform = { + test = true + } + } +} +`, + }, + expected: false, + }, + "terraform-string-attribute": { + configRaw: configurationString{ + raw: ` +resource "test_test" "test" { + test = { + terraform = "test" + } +} +`, + }, + expected: false, + }, + "terraform-block": { + configRaw: configurationString{ + raw: ` +terraform { + test = true +} + +resource "test_test" "test" {} +`, + }, + expected: true, + }, + "terraform-block-no-space": { + configRaw: configurationString{ + raw: ` +terraform{ + test = true +} + +resource "test_test" "test" {} +`, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.configRaw.HasTerraformBlock(context.Background()) + + if err != nil { + t.Errorf("unexpected error: %s", err) + } + + if diff := cmp.Diff(testCase.expected, got); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestConfigurationString_Write(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + configRaw configurationString + }{ + "raw": { + configRaw: configurationString{ + ` +provider "test" { + test = true +} + +resource "test_test" "test" {} +`, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + tempDir := t.TempDir() + + err := testCase.configRaw.Write(context.Background(), tempDir) + + if err != nil { + t.Errorf("unexpected error: %s", err) + } + + expectedBytes := []byte(testCase.configRaw.raw) + + gotBytes, err := os.ReadFile(filepath.Join(tempDir, rawConfigFileName)) + + if err != nil { + t.Errorf("error reading file: %s", err) + } + + if diff := cmp.Diff(gotBytes, expectedBytes); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + }) + } +} diff --git a/internal/teststep/testdata/empty_dir/.gitignore b/internal/teststep/testdata/empty_dir/.gitignore new file mode 100644 index 000000000..86d0cb272 --- /dev/null +++ b/internal/teststep/testdata/empty_dir/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore \ No newline at end of file diff --git a/internal/teststep/testdata/empty_file/main.tf b/internal/teststep/testdata/empty_file/main.tf new file mode 100644 index 000000000..48753c8fa --- /dev/null +++ b/internal/teststep/testdata/empty_file/main.tf @@ -0,0 +1,3 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + diff --git a/internal/teststep/testdata/main.tf b/internal/teststep/testdata/main.tf new file mode 100644 index 000000000..ba356816b --- /dev/null +++ b/internal/teststep/testdata/main.tf @@ -0,0 +1,4 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +// Hello world diff --git a/internal/teststep/testdata/provider_block_quoted_with_attributes/main.tf b/internal/teststep/testdata/provider_block_quoted_with_attributes/main.tf new file mode 100644 index 000000000..e69dcbf09 --- /dev/null +++ b/internal/teststep/testdata/provider_block_quoted_with_attributes/main.tf @@ -0,0 +1,8 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +provider "test" { + test = true +} + +resource "test_test" "test" {} \ No newline at end of file diff --git a/internal/teststep/testdata/provider_block_quoted_with_attributes_no_spaces/main.tf b/internal/teststep/testdata/provider_block_quoted_with_attributes_no_spaces/main.tf new file mode 100644 index 000000000..44f320878 --- /dev/null +++ b/internal/teststep/testdata/provider_block_quoted_with_attributes_no_spaces/main.tf @@ -0,0 +1,8 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +provider"test"{ + test = true +} + +resource "test_test" "test" {} \ No newline at end of file diff --git a/internal/teststep/testdata/provider_block_quoted_without_attributes/main.tf b/internal/teststep/testdata/provider_block_quoted_without_attributes/main.tf new file mode 100644 index 000000000..86dec6091 --- /dev/null +++ b/internal/teststep/testdata/provider_block_quoted_without_attributes/main.tf @@ -0,0 +1,6 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +provider "test" {} + +resource "test_test" "test" {} \ No newline at end of file diff --git a/internal/teststep/testdata/provider_block_quoted_without_attributes_no_spaces/main.tf b/internal/teststep/testdata/provider_block_quoted_without_attributes_no_spaces/main.tf new file mode 100644 index 000000000..72f098294 --- /dev/null +++ b/internal/teststep/testdata/provider_block_quoted_without_attributes_no_spaces/main.tf @@ -0,0 +1,6 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +provider"test"{} + +resource "test_test" "test" {} \ No newline at end of file diff --git a/internal/teststep/testdata/provider_block_unquoted_with_attributes/main.tf b/internal/teststep/testdata/provider_block_unquoted_with_attributes/main.tf new file mode 100644 index 000000000..43e43e72b --- /dev/null +++ b/internal/teststep/testdata/provider_block_unquoted_with_attributes/main.tf @@ -0,0 +1,8 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +provider test { + test = true +} + +resource "test_test" "test" {} \ No newline at end of file diff --git a/internal/teststep/testdata/provider_block_unquoted_with_attributes_no_trailing_space/main.tf b/internal/teststep/testdata/provider_block_unquoted_with_attributes_no_trailing_space/main.tf new file mode 100644 index 000000000..f6ada99ad --- /dev/null +++ b/internal/teststep/testdata/provider_block_unquoted_with_attributes_no_trailing_space/main.tf @@ -0,0 +1,8 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +provider test{ + test = true +} + +resource "test_test" "test" {} \ No newline at end of file diff --git a/internal/teststep/testdata/provider_block_unquoted_without_attributes/main.tf b/internal/teststep/testdata/provider_block_unquoted_without_attributes/main.tf new file mode 100644 index 000000000..0a8fe79de --- /dev/null +++ b/internal/teststep/testdata/provider_block_unquoted_without_attributes/main.tf @@ -0,0 +1,6 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +provider test {} + +resource "test_test" "test" {} \ No newline at end of file diff --git a/internal/teststep/testdata/provider_block_unquoted_without_attributes_no_trailing_space/main.tf b/internal/teststep/testdata/provider_block_unquoted_without_attributes_no_trailing_space/main.tf new file mode 100644 index 000000000..d49f61008 --- /dev/null +++ b/internal/teststep/testdata/provider_block_unquoted_without_attributes_no_trailing_space/main.tf @@ -0,0 +1,6 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +provider test{} + +resource "test_test" "test" {} \ No newline at end of file diff --git a/internal/teststep/testdata/provider_meta_attribute/main.tf b/internal/teststep/testdata/provider_meta_attribute/main.tf new file mode 100644 index 000000000..13420e204 --- /dev/null +++ b/internal/teststep/testdata/provider_meta_attribute/main.tf @@ -0,0 +1,6 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "test_test" "test" { + provider = test.test +} \ No newline at end of file diff --git a/internal/teststep/testdata/provider_object_attribute/main.tf b/internal/teststep/testdata/provider_object_attribute/main.tf new file mode 100644 index 000000000..66743ac12 --- /dev/null +++ b/internal/teststep/testdata/provider_object_attribute/main.tf @@ -0,0 +1,10 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "test_test" "test" { + test = { + provider = { + test = true + } + } +} \ No newline at end of file diff --git a/internal/teststep/testdata/provider_string_attribute/main.tf b/internal/teststep/testdata/provider_string_attribute/main.tf new file mode 100644 index 000000000..529972c04 --- /dev/null +++ b/internal/teststep/testdata/provider_string_attribute/main.tf @@ -0,0 +1,8 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "test_test" "test" { + test = { + provider = "test" + } +} \ No newline at end of file diff --git a/internal/teststep/testdata/random/random.tf b/internal/teststep/testdata/random/random.tf new file mode 100644 index 000000000..b75cfbb6b --- /dev/null +++ b/internal/teststep/testdata/random/random.tf @@ -0,0 +1,19 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.5.1" + } + } +} + +provider "random" {} + +resource "random_password" "test" { + length = 8 + + numeric = false +} \ No newline at end of file diff --git a/internal/teststep/testdata/random_multiple_files/provider.tf b/internal/teststep/testdata/random_multiple_files/provider.tf new file mode 100644 index 000000000..4e7b51e38 --- /dev/null +++ b/internal/teststep/testdata/random_multiple_files/provider.tf @@ -0,0 +1,4 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +provider "random" {} \ No newline at end of file diff --git a/internal/teststep/testdata/random_multiple_files/random.tf b/internal/teststep/testdata/random_multiple_files/random.tf new file mode 100644 index 000000000..6ca8f0bb1 --- /dev/null +++ b/internal/teststep/testdata/random_multiple_files/random.tf @@ -0,0 +1,8 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "random_password" "test" { + length = 8 + + numeric = false +} \ No newline at end of file diff --git a/internal/teststep/testdata/random_multiple_files/terraform.tf b/internal/teststep/testdata/random_multiple_files/terraform.tf new file mode 100644 index 000000000..1aaa98022 --- /dev/null +++ b/internal/teststep/testdata/random_multiple_files/terraform.tf @@ -0,0 +1,11 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_providers { + random = { + source = "registry.terraform.io/hashicorp/random" + version = "3.5.1" + } + } +} \ No newline at end of file diff --git a/internal/teststep/testdata/terraform_block/main.tf b/internal/teststep/testdata/terraform_block/main.tf new file mode 100644 index 000000000..9efabf7c2 --- /dev/null +++ b/internal/teststep/testdata/terraform_block/main.tf @@ -0,0 +1,8 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + test = true +} + +resource "test_test" "test" {} \ No newline at end of file diff --git a/internal/teststep/testdata/terraform_block_no_space/main.tf b/internal/teststep/testdata/terraform_block_no_space/main.tf new file mode 100644 index 000000000..d88036771 --- /dev/null +++ b/internal/teststep/testdata/terraform_block_no_space/main.tf @@ -0,0 +1,8 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform{ + test = true +} + +resource "test_test" "test" {} \ No newline at end of file diff --git a/internal/teststep/testdata/terraform_meta_attribute/main.tf b/internal/teststep/testdata/terraform_meta_attribute/main.tf new file mode 100644 index 000000000..980c48eed --- /dev/null +++ b/internal/teststep/testdata/terraform_meta_attribute/main.tf @@ -0,0 +1,6 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "test_test" "test" { + terraform = test.test +} \ No newline at end of file diff --git a/internal/teststep/testdata/terraform_object_attribute/main.tf b/internal/teststep/testdata/terraform_object_attribute/main.tf new file mode 100644 index 000000000..929baf2a5 --- /dev/null +++ b/internal/teststep/testdata/terraform_object_attribute/main.tf @@ -0,0 +1,10 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "test_test" "test" { + test = { + terraform = { + test = true + } + } +} \ No newline at end of file diff --git a/internal/teststep/testdata/terraform_string_attribute/main.tf b/internal/teststep/testdata/terraform_string_attribute/main.tf new file mode 100644 index 000000000..dd98b628a --- /dev/null +++ b/internal/teststep/testdata/terraform_string_attribute/main.tf @@ -0,0 +1,8 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "test_test" "test" { + test = { + terraform = "test" + } +} \ No newline at end of file diff --git a/knownvalue/bool.go b/knownvalue/bool.go new file mode 100644 index 000000000..fba10ee86 --- /dev/null +++ b/knownvalue/bool.go @@ -0,0 +1,44 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "fmt" + "strconv" +) + +var _ Check = boolValue{} + +type boolValue struct { + value bool +} + +// CheckValue determines whether the passed value is of type bool, and +// contains a matching bool value. +func (v boolValue) CheckValue(other any) error { + otherVal, ok := other.(bool) + + if !ok { + return fmt.Errorf("expected bool value for Bool check, got: %T", other) + } + + if otherVal != v.value { + return fmt.Errorf("expected value %t for Bool check, got: %t", v.value, otherVal) + } + + return nil +} + +// String returns the string representation of the bool value. +func (v boolValue) String() string { + return strconv.FormatBool(v.value) +} + +// Bool returns a Check for asserting equality between the +// supplied bool and the value passed to the CheckValue method. +func Bool(value bool) boolValue { + return boolValue{ + value: value, + } +} diff --git a/knownvalue/bool_func.go b/knownvalue/bool_func.go new file mode 100644 index 000000000..15135c10f --- /dev/null +++ b/knownvalue/bool_func.go @@ -0,0 +1,39 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import "fmt" + +var _ Check = boolFunc{} + +type boolFunc struct { + checkFunc func(v bool) error +} + +// CheckValue determines whether the passed value is of type bool, and +// returns no error from the provided check function +func (v boolFunc) CheckValue(other any) error { + val, ok := other.(bool) + + if !ok { + return fmt.Errorf("expected bool value for BoolFunc check, got: %T", other) + } + + return v.checkFunc(val) +} + +// String returns the bool representation of the value. +func (v boolFunc) String() string { + // Validation is up the the implementer of the function, so there are no + // bool literal or regex comparers to print here + return "BoolFunc" +} + +// BoolFunc returns a Check for passing the bool value in state +// to the provided check function +func BoolFunc(fn func(v bool) error) boolFunc { + return boolFunc{ + checkFunc: fn, + } +} diff --git a/knownvalue/bool_func_test.go b/knownvalue/bool_func_test.go new file mode 100644 index 000000000..9d2b362b1 --- /dev/null +++ b/knownvalue/bool_func_test.go @@ -0,0 +1,75 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestBoolFunc_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "nil": { + self: knownvalue.BoolFunc(func(bool) error { return nil }), + expectedError: fmt.Errorf("expected bool value for BoolFunc check, got: "), + }, + "wrong-type": { + self: knownvalue.BoolFunc(func(bool) error { return nil }), + other: json.Number("1.234"), + expectedError: fmt.Errorf("expected bool value for BoolFunc check, got: json.Number"), + }, + "failure": { + self: knownvalue.BoolFunc(func(b bool) error { + if b != true { + return fmt.Errorf("%t was not true", b) + } + return nil + }), + other: false, + expectedError: fmt.Errorf("%t was not true", false), + }, + "success": { + self: knownvalue.BoolFunc(func(b bool) error { + if b != true { + return fmt.Errorf("%t was not foo", b) + } + return nil + }), + other: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolFunc_String(t *testing.T) { + t.Parallel() + + got := knownvalue.BoolFunc(func(bool) error { return nil }).String() + + if diff := cmp.Diff(got, "BoolFunc"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/bool_test.go b/knownvalue/bool_test.go new file mode 100644 index 000000000..aa962fec8 --- /dev/null +++ b/knownvalue/bool_test.go @@ -0,0 +1,81 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestBoolValue_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.Bool(false), + expectedError: fmt.Errorf("expected bool value for Bool check, got: "), + }, + "zero-other": { + self: knownvalue.Bool(false), + other: false, // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.Bool(false), + expectedError: fmt.Errorf("expected bool value for Bool check, got: "), + }, + "wrong-type": { + self: knownvalue.Bool(true), + other: 1.23, + expectedError: fmt.Errorf("expected bool value for Bool check, got: float64"), + }, + "not-equal": { + self: knownvalue.Bool(true), + other: false, + expectedError: fmt.Errorf("expected value true for Bool check, got: false"), + }, + "equal": { + self: knownvalue.Bool(true), + other: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolValue_String(t *testing.T) { + t.Parallel() + + got := knownvalue.Bool(true).String() + + if diff := cmp.Diff(got, "true"); 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/knownvalue/check.go b/knownvalue/check.go new file mode 100644 index 000000000..cef532c94 --- /dev/null +++ b/knownvalue/check.go @@ -0,0 +1,14 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +// Check defines an interface that is implemented to determine whether type and value match. Individual +// implementations determine how the match is performed (e.g., exact match, partial match). +type Check interface { + // CheckValue should assert the given known value against any expectations. Use the error + // return to signal unexpected values or implementation errors. + CheckValue(value any) error + // String should return a string representation of the type and value. + String() string +} diff --git a/knownvalue/doc.go b/knownvalue/doc.go new file mode 100644 index 000000000..4041c3e93 --- /dev/null +++ b/knownvalue/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package knownvalue contains the known value interface, and types implementing the known value interface. +package knownvalue diff --git a/knownvalue/float32.go b/knownvalue/float32.go new file mode 100644 index 000000000..ee02fdcb1 --- /dev/null +++ b/knownvalue/float32.go @@ -0,0 +1,51 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "encoding/json" + "fmt" + "strconv" +) + +var _ Check = float32Exact{} + +type float32Exact struct { + value float32 +} + +// CheckValue determines whether the passed value is of type float32, and +// contains a matching float32 value. +func (v float32Exact) CheckValue(other any) error { + jsonNum, ok := other.(json.Number) + + if !ok { + return fmt.Errorf("expected json.Number value for Float32Exact check, got: %T", other) + } + + otherVal, err := strconv.ParseFloat(string(jsonNum), 32) + + if err != nil { + return fmt.Errorf("expected json.Number to be parseable as float32 value for Float32Exact check: %s", err) + } + + if float32(otherVal) != v.value { + return fmt.Errorf("expected value %s for Float32Exact check, got: %s", v.String(), strconv.FormatFloat(otherVal, 'f', -1, 32)) + } + + return nil +} + +// String returns the string representation of the float32 value. +func (v float32Exact) String() string { + return strconv.FormatFloat(float64(v.value), 'f', -1, 32) +} + +// Float32Exact returns a Check for asserting equality between the +// supplied float32 and the value passed to the CheckValue method. +func Float32Exact(value float32) float32Exact { + return float32Exact{ + value: value, + } +} diff --git a/knownvalue/float32_func.go b/knownvalue/float32_func.go new file mode 100644 index 000000000..5d3b55fcd --- /dev/null +++ b/knownvalue/float32_func.go @@ -0,0 +1,48 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "encoding/json" + "fmt" + "strconv" +) + +var _ Check = float32Func{} + +type float32Func struct { + checkFunc func(v float32) error +} + +// CheckValue determines whether the passed value is of type float32, and +// returns no error from the provided check function +func (v float32Func) CheckValue(other any) error { + jsonNum, ok := other.(json.Number) + + if !ok { + return fmt.Errorf("expected json.Number value for Float32Func check, got: %T", other) + } + + otherVal, err := strconv.ParseFloat(string(jsonNum), 32) + if err != nil { + return fmt.Errorf("expected json.Number to be parseable as float32 value for Float32Func check: %s", err) + } + + return v.checkFunc(float32(otherVal)) +} + +// String returns the float32 representation of the value. +func (v float32Func) String() string { + // Validation is up the the implementer of the function, so there are no + // float32 literal or regex comparers to print here + return "Float32Func" +} + +// Float32Func returns a Check for passing the float32 value in state +// to the provided check function +func Float32Func(fn func(v float32) error) float32Func { + return float32Func{ + checkFunc: fn, + } +} diff --git a/knownvalue/float32_func_test.go b/knownvalue/float32_func_test.go new file mode 100644 index 000000000..01b928e31 --- /dev/null +++ b/knownvalue/float32_func_test.go @@ -0,0 +1,80 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestFloat32Func_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "nil": { + self: knownvalue.Float32Func(func(float32) error { return nil }), + expectedError: fmt.Errorf("expected json.Number value for Float32Func check, got: "), + }, + "wrong-type": { + self: knownvalue.Float32Func(func(float32) error { return nil }), + other: "wrongtype", + expectedError: fmt.Errorf("expected json.Number value for Float32Func check, got: string"), + }, + "no-digits": { + self: knownvalue.Float32Func(func(float32) error { return nil }), + other: json.Number("str"), + expectedError: fmt.Errorf("expected json.Number to be parseable as float32 value for Float32Func check: strconv.ParseFloat: parsing \"str\": invalid syntax"), + }, + "failure": { + self: knownvalue.Float32Func(func(f float32) error { + if f != 1.1 { + return fmt.Errorf("%f was not 1.1", f) + } + return nil + }), + other: json.Number("1.2"), + expectedError: fmt.Errorf("%f was not 1.1", 1.2), + }, + "success": { + self: knownvalue.Float32Func(func(f float32) error { + if f != 1.1 { + return fmt.Errorf("%f was not 1.1", f) + } + return nil + }), + other: json.Number("1.1"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32Func_String(t *testing.T) { + t.Parallel() + + got := knownvalue.Float32Func(func(float32) error { return nil }).String() + + if diff := cmp.Diff(got, "Float32Func"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/float32_test.go b/knownvalue/float32_test.go new file mode 100644 index 000000000..ecab29c0d --- /dev/null +++ b/knownvalue/float32_test.go @@ -0,0 +1,73 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestFloat32Value_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.Float32Exact(0), + expectedError: fmt.Errorf("expected json.Number value for Float32Exact check, got: "), + }, + "zero-other": { + self: knownvalue.Float32Exact(0), + other: json.Number("0.0"), // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.Float32Exact(1.234), + expectedError: fmt.Errorf("expected json.Number value for Float32Exact check, got: "), + }, + "wrong-type": { + self: knownvalue.Float32Exact(1.234), + other: json.Number("str"), + expectedError: fmt.Errorf("expected json.Number to be parseable as float32 value for Float32Exact check: strconv.ParseFloat: parsing \"str\": invalid syntax"), + }, + "not-equal": { + self: knownvalue.Float32Exact(1.234), + other: json.Number("4.321"), + expectedError: fmt.Errorf("expected value 1.234 for Float32Exact check, got: 4.321"), + }, + "equal": { + self: knownvalue.Float32Exact(1.234), + other: json.Number("1.234"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32Value_String(t *testing.T) { + t.Parallel() + + got := knownvalue.Float32Exact(1.234567890123e+03).String() + + if diff := cmp.Diff(got, "1234.5679"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/float64.go b/knownvalue/float64.go new file mode 100644 index 000000000..bacdaa6fa --- /dev/null +++ b/knownvalue/float64.go @@ -0,0 +1,51 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "encoding/json" + "fmt" + "strconv" +) + +var _ Check = float64Exact{} + +type float64Exact struct { + value float64 +} + +// CheckValue determines whether the passed value is of type float64, and +// contains a matching float64 value. +func (v float64Exact) CheckValue(other any) error { + jsonNum, ok := other.(json.Number) + + if !ok { + return fmt.Errorf("expected json.Number value for Float64Exact check, got: %T", other) + } + + otherVal, err := jsonNum.Float64() + + if err != nil { + return fmt.Errorf("expected json.Number to be parseable as float64 value for Float64Exact check: %s", err) + } + + if otherVal != v.value { + return fmt.Errorf("expected value %s for Float64Exact check, got: %s", v.String(), strconv.FormatFloat(otherVal, 'f', -1, 64)) + } + + return nil +} + +// String returns the string representation of the float64 value. +func (v float64Exact) String() string { + return strconv.FormatFloat(v.value, 'f', -1, 64) +} + +// Float64Exact returns a Check for asserting equality between the +// supplied float64 and the value passed to the CheckValue method. +func Float64Exact(value float64) float64Exact { + return float64Exact{ + value: value, + } +} diff --git a/knownvalue/float64_func.go b/knownvalue/float64_func.go new file mode 100644 index 000000000..56f17e0c1 --- /dev/null +++ b/knownvalue/float64_func.go @@ -0,0 +1,48 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "encoding/json" + "fmt" + "strconv" +) + +var _ Check = float64Func{} + +type float64Func struct { + checkFunc func(v float64) error +} + +// CheckValue determines whether the passed value is of type float64, and +// returns no error from the provided check function +func (v float64Func) CheckValue(other any) error { + jsonNum, ok := other.(json.Number) + + if !ok { + return fmt.Errorf("expected json.Number value for Float64Func check, got: %T", other) + } + + otherVal, err := strconv.ParseFloat(string(jsonNum), 64) + if err != nil { + return fmt.Errorf("expected json.Number to be parseable as float64 value for Float64Func check: %s", err) + } + + return v.checkFunc(otherVal) +} + +// String returns the float64 representation of the value. +func (v float64Func) String() string { + // Validation is up the the implementer of the function, so there are no + // float64 literal or regex comparers to print here + return "Float64Func" +} + +// Float64Func returns a Check for passing the float64 value in state +// to the provided check function +func Float64Func(fn func(v float64) error) float64Func { + return float64Func{ + checkFunc: fn, + } +} diff --git a/knownvalue/float64_func_test.go b/knownvalue/float64_func_test.go new file mode 100644 index 000000000..8bc3884fe --- /dev/null +++ b/knownvalue/float64_func_test.go @@ -0,0 +1,80 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestFloat64Func_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "nil": { + self: knownvalue.Float64Func(func(float64) error { return nil }), + expectedError: fmt.Errorf("expected json.Number value for Float64Func check, got: "), + }, + "wrong-type": { + self: knownvalue.Float64Func(func(float64) error { return nil }), + other: "wrongtype", + expectedError: fmt.Errorf("expected json.Number value for Float64Func check, got: string"), + }, + "no-digits": { + self: knownvalue.Float64Func(func(float64) error { return nil }), + other: json.Number("str"), + expectedError: fmt.Errorf("expected json.Number to be parseable as float64 value for Float64Func check: strconv.ParseFloat: parsing \"str\": invalid syntax"), + }, + "failure": { + self: knownvalue.Float64Func(func(f float64) error { + if f != 1.1 { + return fmt.Errorf("%f was not 1.1", f) + } + return nil + }), + other: json.Number("1.2"), + expectedError: fmt.Errorf("%f was not 1.1", 1.2), + }, + "success": { + self: knownvalue.Float64Func(func(f float64) error { + if f != 1.1 { + return fmt.Errorf("%f was not 1.1", f) + } + return nil + }), + other: json.Number("1.1"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64Func_String(t *testing.T) { + t.Parallel() + + got := knownvalue.Float64Func(func(float64) error { return nil }).String() + + if diff := cmp.Diff(got, "Float64Func"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/float64_test.go b/knownvalue/float64_test.go new file mode 100644 index 000000000..26b2cf309 --- /dev/null +++ b/knownvalue/float64_test.go @@ -0,0 +1,73 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestFloat64Value_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.Float64Exact(0), + expectedError: fmt.Errorf("expected json.Number value for Float64Exact check, got: "), + }, + "zero-other": { + self: knownvalue.Float64Exact(0), + other: json.Number("0.0"), // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.Float64Exact(1.234), + expectedError: fmt.Errorf("expected json.Number value for Float64Exact check, got: "), + }, + "wrong-type": { + self: knownvalue.Float64Exact(1.234), + other: json.Number("str"), + expectedError: fmt.Errorf("expected json.Number to be parseable as float64 value for Float64Exact check: strconv.ParseFloat: parsing \"str\": invalid syntax"), + }, + "not-equal": { + self: knownvalue.Float64Exact(1.234), + other: json.Number("4.321"), + expectedError: fmt.Errorf("expected value 1.234 for Float64Exact check, got: 4.321"), + }, + "equal": { + self: knownvalue.Float64Exact(1.234), + other: json.Number("1.234"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64Value_String(t *testing.T) { + t.Parallel() + + got := knownvalue.Float64Exact(1.234567890123e+09).String() + + if diff := cmp.Diff(got, "1234567890.123"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/int32.go b/knownvalue/int32.go new file mode 100644 index 000000000..49dd30bb3 --- /dev/null +++ b/knownvalue/int32.go @@ -0,0 +1,51 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "encoding/json" + "fmt" + "strconv" +) + +var _ Check = int32Exact{} + +type int32Exact struct { + value int32 +} + +// CheckValue determines whether the passed value is of type int32, and +// contains a matching int32 value. +func (v int32Exact) CheckValue(other any) error { + jsonNum, ok := other.(json.Number) + + if !ok { + return fmt.Errorf("expected json.Number value for Int32Exact check, got: %T", other) + } + + otherVal, err := strconv.ParseInt(string(jsonNum), 10, 32) + + if err != nil { + return fmt.Errorf("expected json.Number to be parseable as int32 value for Int32Exact check: %s", err) + } + + if int32(otherVal) != v.value { + return fmt.Errorf("expected value %d for Int32Exact check, got: %d", v.value, otherVal) + } + + return nil +} + +// String returns the string representation of the int32 value. +func (v int32Exact) String() string { + return strconv.FormatInt(int64(v.value), 10) +} + +// Int32Exact returns a Check for asserting equality between the +// supplied int32 and the value passed to the CheckValue method. +func Int32Exact(value int32) int32Exact { + return int32Exact{ + value: value, + } +} diff --git a/knownvalue/int32_func.go b/knownvalue/int32_func.go new file mode 100644 index 000000000..587f7aae6 --- /dev/null +++ b/knownvalue/int32_func.go @@ -0,0 +1,48 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "encoding/json" + "fmt" + "strconv" +) + +var _ Check = int32Func{} + +type int32Func struct { + checkFunc func(v int32) error +} + +// CheckValue determines whether the passed value is of type int32, and +// returns no error from the provided check function +func (v int32Func) CheckValue(other any) error { + jsonNum, ok := other.(json.Number) + + if !ok { + return fmt.Errorf("expected json.Number value for Int32Func check, got: %T", other) + } + + otherVal, err := strconv.ParseInt(string(jsonNum), 10, 32) + if err != nil { + return fmt.Errorf("expected json.Number to be parseable as int32 value for Int32Func check: %s", err) + } + + return v.checkFunc(int32(otherVal)) +} + +// String returns the int32 representation of the value. +func (v int32Func) String() string { + // Validation is up the the implementer of the function, so there are no + // int32 literal or regex comparers to print here + return "Int32Func" +} + +// Int32Func returns a Check for passing the int32 value in state +// to the provided check function +func Int32Func(fn func(v int32) error) int32Func { + return int32Func{ + checkFunc: fn, + } +} diff --git a/knownvalue/int32_func_test.go b/knownvalue/int32_func_test.go new file mode 100644 index 000000000..41776732d --- /dev/null +++ b/knownvalue/int32_func_test.go @@ -0,0 +1,80 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestInt32Func_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "nil": { + self: knownvalue.Int32Func(func(int32) error { return nil }), + expectedError: fmt.Errorf("expected json.Number value for Int32Func check, got: "), + }, + "wrong-type": { + self: knownvalue.Int32Func(func(int32) error { return nil }), + other: "wrongtype", + expectedError: fmt.Errorf("expected json.Number value for Int32Func check, got: string"), + }, + "no-digits": { + self: knownvalue.Int32Func(func(int32) error { return nil }), + other: json.Number("str"), + expectedError: fmt.Errorf("expected json.Number to be parseable as int32 value for Int32Func check: strconv.ParseInt: parsing \"str\": invalid syntax"), + }, + "failure": { + self: knownvalue.Int32Func(func(i int32) error { + if i != 1 { + return fmt.Errorf("%d was not 1", i) + } + return nil + }), + other: json.Number("2"), + expectedError: fmt.Errorf("%d was not 1", 2), + }, + "success": { + self: knownvalue.Int32Func(func(i int32) error { + if i != 1 { + return fmt.Errorf("%d was not 1", i) + } + return nil + }), + other: json.Number("1"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32Func_String(t *testing.T) { + t.Parallel() + + got := knownvalue.Int32Func(func(int32) error { return nil }).String() + + if diff := cmp.Diff(got, "Int32Func"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/int32_test.go b/knownvalue/int32_test.go new file mode 100644 index 000000000..158724ca0 --- /dev/null +++ b/knownvalue/int32_test.go @@ -0,0 +1,73 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestInt32Value_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.Int32Exact(0), + expectedError: fmt.Errorf("expected json.Number value for Int32Exact check, got: "), + }, + "zero-other": { + self: knownvalue.Int32Exact(0), + other: json.Number("0"), // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.Int32Exact(1234), + expectedError: fmt.Errorf("expected json.Number value for Int32Exact check, got: "), + }, + "wrong-type": { + self: knownvalue.Int32Exact(1234), + other: json.Number("str"), + expectedError: fmt.Errorf("expected json.Number to be parseable as int32 value for Int32Exact check: strconv.ParseInt: parsing \"str\": invalid syntax"), + }, + "not-equal": { + self: knownvalue.Int32Exact(1234), + other: json.Number("4321"), + expectedError: fmt.Errorf("expected value 1234 for Int32Exact check, got: 4321"), + }, + "equal": { + self: knownvalue.Int32Exact(1234), + other: json.Number("1234"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32Value_String(t *testing.T) { + t.Parallel() + + got := knownvalue.Int32Exact(123456789).String() + + if diff := cmp.Diff(got, "123456789"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/int64.go b/knownvalue/int64.go new file mode 100644 index 000000000..19f803622 --- /dev/null +++ b/knownvalue/int64.go @@ -0,0 +1,51 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "encoding/json" + "fmt" + "strconv" +) + +var _ Check = int64Exact{} + +type int64Exact struct { + value int64 +} + +// CheckValue determines whether the passed value is of type int64, and +// contains a matching int64 value. +func (v int64Exact) CheckValue(other any) error { + jsonNum, ok := other.(json.Number) + + if !ok { + return fmt.Errorf("expected json.Number value for Int64Exact check, got: %T", other) + } + + otherVal, err := jsonNum.Int64() + + if err != nil { + return fmt.Errorf("expected json.Number to be parseable as int64 value for Int64Exact check: %s", err) + } + + if otherVal != v.value { + return fmt.Errorf("expected value %d for Int64Exact check, got: %d", v.value, otherVal) + } + + return nil +} + +// String returns the string representation of the int64 value. +func (v int64Exact) String() string { + return strconv.FormatInt(v.value, 10) +} + +// Int64Exact returns a Check for asserting equality between the +// supplied int64 and the value passed to the CheckValue method. +func Int64Exact(value int64) int64Exact { + return int64Exact{ + value: value, + } +} diff --git a/knownvalue/int64_func.go b/knownvalue/int64_func.go new file mode 100644 index 000000000..1269355c4 --- /dev/null +++ b/knownvalue/int64_func.go @@ -0,0 +1,48 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "encoding/json" + "fmt" + "strconv" +) + +var _ Check = int64Func{} + +type int64Func struct { + checkFunc func(v int64) error +} + +// CheckValue determines whether the passed value is of type int64, and +// returns no error from the provided check function +func (v int64Func) CheckValue(other any) error { + jsonNum, ok := other.(json.Number) + + if !ok { + return fmt.Errorf("expected json.Number value for Int64Func check, got: %T", other) + } + + otherVal, err := strconv.ParseInt(string(jsonNum), 10, 64) + if err != nil { + return fmt.Errorf("expected json.Number to be parseable as int64 value for Int64Func check: %s", err) + } + + return v.checkFunc(otherVal) +} + +// String returns the int64 representation of the value. +func (v int64Func) String() string { + // Validation is up the the implementer of the function, so there are no + // int64 literal or regex comparers to print here + return "Int64Func" +} + +// Int64Func returns a Check for passing the int64 value in state +// to the provided check function +func Int64Func(fn func(v int64) error) int64Func { + return int64Func{ + checkFunc: fn, + } +} diff --git a/knownvalue/int64_func_test.go b/knownvalue/int64_func_test.go new file mode 100644 index 000000000..56613f70c --- /dev/null +++ b/knownvalue/int64_func_test.go @@ -0,0 +1,80 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestInt64Func_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "nil": { + self: knownvalue.Int64Func(func(int64) error { return nil }), + expectedError: fmt.Errorf("expected json.Number value for Int64Func check, got: "), + }, + "wrong-type": { + self: knownvalue.Int64Func(func(int64) error { return nil }), + other: "wrongtype", + expectedError: fmt.Errorf("expected json.Number value for Int64Func check, got: string"), + }, + "no-digits": { + self: knownvalue.Int64Func(func(int64) error { return nil }), + other: json.Number("str"), + expectedError: fmt.Errorf("expected json.Number to be parseable as int64 value for Int64Func check: strconv.ParseInt: parsing \"str\": invalid syntax"), + }, + "failure": { + self: knownvalue.Int64Func(func(i int64) error { + if i != 1 { + return fmt.Errorf("%d was not 1", i) + } + return nil + }), + other: json.Number("2"), + expectedError: fmt.Errorf("%d was not 1", 2), + }, + "success": { + self: knownvalue.Int64Func(func(i int64) error { + if i != 1 { + return fmt.Errorf("%d was not 1", i) + } + return nil + }), + other: json.Number("1"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64Func_String(t *testing.T) { + t.Parallel() + + got := knownvalue.Int64Func(func(int64) error { return nil }).String() + + if diff := cmp.Diff(got, "Int64Func"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/int64_test.go b/knownvalue/int64_test.go new file mode 100644 index 000000000..2f7cd7c50 --- /dev/null +++ b/knownvalue/int64_test.go @@ -0,0 +1,73 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestInt64Value_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.Int64Exact(0), + expectedError: fmt.Errorf("expected json.Number value for Int64Exact check, got: "), + }, + "zero-other": { + self: knownvalue.Int64Exact(0), + other: json.Number("0"), // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.Int64Exact(1234), + expectedError: fmt.Errorf("expected json.Number value for Int64Exact check, got: "), + }, + "wrong-type": { + self: knownvalue.Int64Exact(1234), + other: json.Number("str"), + expectedError: fmt.Errorf("expected json.Number to be parseable as int64 value for Int64Exact check: strconv.ParseInt: parsing \"str\": invalid syntax"), + }, + "not-equal": { + self: knownvalue.Int64Exact(1234), + other: json.Number("4321"), + expectedError: fmt.Errorf("expected value 1234 for Int64Exact check, got: 4321"), + }, + "equal": { + self: knownvalue.Int64Exact(1234), + other: json.Number("1234"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64Value_String(t *testing.T) { + t.Parallel() + + got := knownvalue.Int64Exact(1234567890123).String() + + if diff := cmp.Diff(got, "1234567890123"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/list.go b/knownvalue/list.go new file mode 100644 index 000000000..500175241 --- /dev/null +++ b/knownvalue/list.go @@ -0,0 +1,67 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "fmt" +) + +var _ Check = listExact{} + +type listExact struct { + value []Check +} + +// CheckValue determines whether the passed value is of type []any, and +// contains matching slice entries in the same sequence. +func (v listExact) CheckValue(other any) error { + otherVal, ok := other.([]any) + + if !ok { + return fmt.Errorf("expected []any value for ListExact check, got: %T", other) + } + + if len(otherVal) != len(v.value) { + expectedElements := "elements" + actualElements := "elements" + + if len(v.value) == 1 { + expectedElements = "element" + } + + if len(otherVal) == 1 { + actualElements = "element" + } + + return fmt.Errorf("expected %d %s for ListExact check, got %d %s", len(v.value), expectedElements, len(otherVal), actualElements) + } + + for i := 0; i < len(v.value); i++ { + if err := v.value[i].CheckValue(otherVal[i]); err != nil { + return fmt.Errorf("list element index %d: %s", i, err) + } + } + + return nil +} + +// String returns the string representation of the value. +func (v listExact) String() string { + var listVals []string + + for _, val := range v.value { + listVals = append(listVals, val.String()) + } + + return fmt.Sprintf("%s", listVals) +} + +// ListExact returns a Check for asserting equality between the +// supplied []Check and the value passed to the CheckValue method. +// This is an order-dependent check. +func ListExact(value []Check) listExact { + return listExact{ + value: value, + } +} diff --git a/knownvalue/list_partial.go b/knownvalue/list_partial.go new file mode 100644 index 000000000..7d6e7ee20 --- /dev/null +++ b/knownvalue/list_partial.go @@ -0,0 +1,89 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "bytes" + "fmt" + "sort" + "strings" +) + +var _ Check = listPartial{} + +type listPartial struct { + value map[int]Check +} + +// CheckValue determines whether the passed value is of type []any, and +// contains matching slice entries in the same sequence. +func (v listPartial) CheckValue(other any) error { + otherVal, ok := other.([]any) + + if !ok { + return fmt.Errorf("expected []any value for ListPartial check, got: %T", other) + } + + var keys []int + + for k := range v.value { + keys = append(keys, k) + } + + sort.SliceStable(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + for _, k := range keys { + if len(otherVal) <= k { + return fmt.Errorf("missing element index %d for ListPartial check", k) + } + + if err := v.value[k].CheckValue(otherVal[k]); err != nil { + return fmt.Errorf("list element %d: %s", k, err) + } + } + + return nil +} + +// String returns the string representation of the value. +func (v listPartial) String() string { + var b bytes.Buffer + + b.WriteString("[") + + var keys []int + + var listVals []string + + for k := range v.value { + keys = append(keys, k) + } + + sort.SliceStable(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + for _, k := range keys { + listVals = append(listVals, fmt.Sprintf("%d:%s", k, v.value[k])) + } + + b.WriteString(strings.Join(listVals, " ")) + + b.WriteString("]") + + return b.String() +} + +// ListPartial returns a Check for asserting partial equality between the +// supplied map[int]Check and the value passed to the CheckValue method. The +// map keys represent the zero-ordered element indices within the list that is +// being checked. Only the elements at the indices defined within the +// supplied map[int]Check are checked. +func ListPartial(value map[int]Check) listPartial { + return listPartial{ + value: value, + } +} diff --git a/knownvalue/list_partial_test.go b/knownvalue/list_partial_test.go new file mode 100644 index 000000000..eafb8d2e1 --- /dev/null +++ b/knownvalue/list_partial_test.go @@ -0,0 +1,138 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestListValuePartial_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.ListPartial(map[int]knownvalue.Check{}), + expectedError: fmt.Errorf("expected []any value for ListPartial check, got: "), + }, + "zero-other": { + self: knownvalue.ListPartial(map[int]knownvalue.Check{}), + other: []any{}, // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.Float64Exact(1.23), + 2: knownvalue.Float64Exact(4.56), + 3: knownvalue.Float64Exact(7.89), + }), + expectedError: fmt.Errorf("expected []any value for ListPartial check, got: "), + }, + "wrong-type": { + self: knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.Float64Exact(1.23), + 2: knownvalue.Float64Exact(4.56), + 3: knownvalue.Float64Exact(7.89), + }), + other: 1.234, + expectedError: fmt.Errorf("expected []any value for ListPartial check, got: float64"), + }, + "empty": { + self: knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.Float64Exact(1.23), + 2: knownvalue.Float64Exact(4.56), + 3: knownvalue.Float64Exact(7.89), + }), + other: []any{}, + expectedError: fmt.Errorf("missing element index 0 for ListPartial check"), + }, + "wrong-length": { + self: knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.Float64Exact(1.23), + 2: knownvalue.Float64Exact(4.56), + 3: knownvalue.Float64Exact(7.89), + }), + other: []any{ + json.Number("1.23"), + json.Number("4.56"), + }, + expectedError: fmt.Errorf("missing element index 2 for ListPartial check"), + }, + "not-equal": { + self: knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.Float64Exact(1.23), + 2: knownvalue.Float64Exact(4.56), + 3: knownvalue.Float64Exact(7.89), + }), + other: []any{ + json.Number("1.23"), + json.Number("4.56"), + json.Number("6.54"), + json.Number("5.46"), + }, + expectedError: fmt.Errorf("list element 2: expected value 4.56 for Float64Exact check, got: 6.54"), + }, + "wrong-order": { + self: knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.Float64Exact(1.23), + 2: knownvalue.Float64Exact(4.56), + 3: knownvalue.Float64Exact(7.89), + }), + other: []any{ + json.Number("1.23"), + json.Number("0.00"), + json.Number("7.89"), + json.Number("4.56"), + }, + expectedError: fmt.Errorf("list element 2: expected value 4.56 for Float64Exact check, got: 7.89"), + }, + "equal": { + self: knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.Float64Exact(1.23), + 2: knownvalue.Float64Exact(4.56), + 3: knownvalue.Float64Exact(7.89), + }), + other: []any{ + json.Number("1.23"), + json.Number("0.00"), + json.Number("4.56"), + json.Number("7.89"), + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListValuePartial_String(t *testing.T) { + t.Parallel() + + got := knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.Float64Exact(1.23), + 2: knownvalue.Float64Exact(4.56), + 3: knownvalue.Float64Exact(7.89), + }).String() + + if diff := cmp.Diff(got, "[0:1.23 2:4.56 3:7.89]"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/list_size.go b/knownvalue/list_size.go new file mode 100644 index 000000000..d95192310 --- /dev/null +++ b/knownvalue/list_size.go @@ -0,0 +1,55 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "fmt" + "strconv" +) + +var _ Check = listSizeExact{} + +type listSizeExact struct { + size int +} + +// CheckValue verifies that the passed value is a list, map, object, +// or set, and contains a matching number of elements. +func (v listSizeExact) CheckValue(other any) error { + otherVal, ok := other.([]any) + + if !ok { + return fmt.Errorf("expected []any value for ListSizeExact check, got: %T", other) + } + + if len(otherVal) != v.size { + expectedElements := "elements" + actualElements := "elements" + + if v.size == 1 { + expectedElements = "element" + } + + if len(otherVal) == 1 { + actualElements = "element" + } + + return fmt.Errorf("expected %d %s for ListSizeExact check, got %d %s", v.size, expectedElements, len(otherVal), actualElements) + } + + return nil +} + +// String returns the string representation of the value. +func (v listSizeExact) String() string { + return strconv.FormatInt(int64(v.size), 10) +} + +// ListSizeExact returns a Check for asserting that +// a list has size elements. +func ListSizeExact(size int) listSizeExact { + return listSizeExact{ + size: size, + } +} diff --git a/knownvalue/list_size_test.go b/knownvalue/list_size_test.go new file mode 100644 index 000000000..89e97d9d4 --- /dev/null +++ b/knownvalue/list_size_test.go @@ -0,0 +1,84 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestListElements_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.ListSizeExact(0), + expectedError: fmt.Errorf("expected []any value for ListSizeExact check, got: "), + }, + "zero-other": { + self: knownvalue.ListSizeExact(0), + other: []any{}, // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.ListSizeExact(3), + expectedError: fmt.Errorf("expected []any value for ListSizeExact check, got: "), + }, + "wrong-type": { + self: knownvalue.ListSizeExact(3), + other: 1.234, + expectedError: fmt.Errorf("expected []any value for ListSizeExact check, got: float64"), + }, + "empty": { + self: knownvalue.ListSizeExact(3), + other: []any{}, + expectedError: fmt.Errorf("expected 3 elements for ListSizeExact check, got 0 elements"), + }, + "wrong-length": { + self: knownvalue.ListSizeExact(3), + other: []any{ + int64(123), + int64(456), + }, + expectedError: fmt.Errorf("expected 3 elements for ListSizeExact check, got 2 elements"), + }, + "equal": { + self: knownvalue.ListSizeExact(3), + other: []any{ + int64(123), + int64(456), + int64(789), + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListElements_String(t *testing.T) { + t.Parallel() + + got := knownvalue.ListSizeExact(2).String() + + if diff := cmp.Diff(got, "2"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/list_test.go b/knownvalue/list_test.go new file mode 100644 index 000000000..6b6b700d2 --- /dev/null +++ b/knownvalue/list_test.go @@ -0,0 +1,135 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestListValue_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.ListExact([]knownvalue.Check{}), + expectedError: fmt.Errorf("expected []any value for ListExact check, got: "), + }, + "zero-other": { + self: knownvalue.ListExact([]knownvalue.Check{}), + other: []any{}, // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.ListExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Int64Exact(456), + knownvalue.Int64Exact(789), + }), + expectedError: fmt.Errorf("expected []any value for ListExact check, got: "), + }, + "wrong-type": { + self: knownvalue.ListExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Int64Exact(456), + knownvalue.Int64Exact(789), + }), + other: 1.234, + expectedError: fmt.Errorf("expected []any value for ListExact check, got: float64"), + }, + "empty": { + self: knownvalue.ListExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Int64Exact(456), + knownvalue.Int64Exact(789), + }), + other: []any{}, + expectedError: fmt.Errorf("expected 3 elements for ListExact check, got 0 elements"), + }, + "wrong-length": { + self: knownvalue.ListExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Int64Exact(456), + knownvalue.Int64Exact(789), + }), + other: []any{ + int64(123), + int64(456), + }, + expectedError: fmt.Errorf("expected 3 elements for ListExact check, got 2 elements"), + }, + "not-equal": { + self: knownvalue.ListExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Int64Exact(456), + knownvalue.Int64Exact(789), + }), + other: []any{ + json.Number("123"), + json.Number("456"), + json.Number("654"), + }, + expectedError: fmt.Errorf("list element index 2: expected value 789 for Int64Exact check, got: 654"), + }, + "wrong-order": { + self: knownvalue.ListExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Int64Exact(456), + knownvalue.Int64Exact(789), + }), + other: []any{ + json.Number("123"), + json.Number("789"), + json.Number("456"), + }, + expectedError: fmt.Errorf("list element index 1: expected value 456 for Int64Exact check, got: 789"), + }, + "equal": { + self: knownvalue.ListExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Int64Exact(456), + knownvalue.Int64Exact(789), + }), + other: []any{ + json.Number("123"), + json.Number("456"), + json.Number("789"), + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListValue_String(t *testing.T) { + t.Parallel() + + got := knownvalue.ListExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Int64Exact(456), + knownvalue.Int64Exact(789), + }).String() + + if diff := cmp.Diff(got, "[123 456 789]"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/map.go b/knownvalue/map.go new file mode 100644 index 000000000..f4027c9df --- /dev/null +++ b/knownvalue/map.go @@ -0,0 +1,93 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "fmt" + "sort" +) + +var _ Check = mapExact{} + +type mapExact struct { + value map[string]Check +} + +// CheckValue determines whether the passed value is of type map[string]any, and +// contains matching map entries. +func (v mapExact) CheckValue(other any) error { + otherVal, ok := other.(map[string]any) + + if !ok { + return fmt.Errorf("expected map[string]any value for MapExact check, got: %T", other) + } + + if len(otherVal) != len(v.value) { + expectedElements := "elements" + actualElements := "elements" + + if len(v.value) == 1 { + expectedElements = "element" + } + + if len(otherVal) == 1 { + actualElements = "element" + } + + return fmt.Errorf("expected %d %s for MapExact check, got %d %s", len(v.value), expectedElements, len(otherVal), actualElements) + } + + var keys []string + + for k := range v.value { + keys = append(keys, k) + } + + sort.SliceStable(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + for _, k := range keys { + otherValItem, ok := otherVal[k] + + if !ok { + return fmt.Errorf("missing element %s for MapExact check", k) + } + + if err := v.value[k].CheckValue(otherValItem); err != nil { + return fmt.Errorf("%s map element: %s", k, err) + } + } + + return nil +} + +// String returns the string representation of the value. +func (v mapExact) String() string { + var keys []string + + for k := range v.value { + keys = append(keys, k) + } + + sort.SliceStable(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + mapVals := make(map[string]string, len(keys)) + + for _, k := range keys { + mapVals[k] = v.value[k].String() + } + + return fmt.Sprintf("%v", mapVals) +} + +// MapExact returns a Check for asserting equality between the +// supplied map[string]Check and the value passed to the CheckValue method. +func MapExact(value map[string]Check) mapExact { + return mapExact{ + value: value, + } +} diff --git a/knownvalue/map_partial.go b/knownvalue/map_partial.go new file mode 100644 index 000000000..860b2adff --- /dev/null +++ b/knownvalue/map_partial.go @@ -0,0 +1,80 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "fmt" + "sort" +) + +var _ Check = mapPartial{} + +type mapPartial struct { + value map[string]Check +} + +// CheckValue determines whether the passed value is of type map[string]any, and +// contains matching map entries. +func (v mapPartial) CheckValue(other any) error { + otherVal, ok := other.(map[string]any) + + if !ok { + return fmt.Errorf("expected map[string]any value for MapPartial check, got: %T", other) + } + + var keys []string + + for k := range v.value { + keys = append(keys, k) + } + + sort.SliceStable(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + for _, k := range keys { + otherValItem, ok := otherVal[k] + + if !ok { + return fmt.Errorf("missing element %s for MapPartial check", k) + } + + if err := v.value[k].CheckValue(otherValItem); err != nil { + return fmt.Errorf("%s map element: %s", k, err) + } + } + + return nil +} + +// String returns the string representation of the value. +func (v mapPartial) String() string { + var keys []string + + for k := range v.value { + keys = append(keys, k) + } + + sort.SliceStable(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + mapVals := make(map[string]string, len(keys)) + + for _, k := range keys { + mapVals[k] = v.value[k].String() + } + + return fmt.Sprintf("%v", mapVals) +} + +// MapPartial returns a Check for asserting partial equality between the +// supplied map[string]Check and the value passed to the CheckValue method. Only +// the elements at the map keys defined within the supplied map[string]Check are +// checked. +func MapPartial(value map[string]Check) mapPartial { + return mapPartial{ + value: value, + } +} diff --git a/knownvalue/map_partial_test.go b/knownvalue/map_partial_test.go new file mode 100644 index 000000000..1cc3c2897 --- /dev/null +++ b/knownvalue/map_partial_test.go @@ -0,0 +1,140 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestMapValuePartial_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.MapPartial(map[string]knownvalue.Check{}), + expectedError: fmt.Errorf("expected map[string]any value for MapPartial check, got: "), + }, + "zero-other": { + self: knownvalue.MapPartial(map[string]knownvalue.Check{}), + other: map[string]any{}, // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.MapPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "three": knownvalue.Float64Exact(7.89), + }), + expectedError: fmt.Errorf("expected map[string]any value for MapPartial check, got: "), + }, + "wrong-type": { + self: knownvalue.MapPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "three": knownvalue.Float64Exact(7.89), + }), + other: 1.234, + expectedError: fmt.Errorf("expected map[string]any value for MapPartial check, got: float64"), + }, + "empty": { + self: knownvalue.MapPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{}, + expectedError: fmt.Errorf("missing element one for MapPartial check"), + }, + "wrong-length": { + self: knownvalue.MapPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "one": json.Number("1.23"), + "two": json.Number("4.56"), + }, + expectedError: fmt.Errorf("missing element three for MapPartial check"), + }, + "not-equal": { + self: knownvalue.MapPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "one": json.Number("1.23"), + "two": json.Number("4.56"), + "three": json.Number("6.54"), + }, + expectedError: fmt.Errorf("three map element: expected value 7.89 for Float64Exact check, got: 6.54"), + }, + "wrong-order": { + self: knownvalue.MapPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "one": json.Number("1.23"), + "two": json.Number("7.89"), + "three": json.Number("4.56"), + }, + expectedError: fmt.Errorf("three map element: expected value 7.89 for Float64Exact check, got: 4.56"), + }, + "key-not-found": { + self: knownvalue.MapPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "four": json.Number("1.23"), + "five": json.Number("7.89"), + "six": json.Number("4.56"), + }, + expectedError: fmt.Errorf("missing element one for MapPartial check"), + }, + "equal": { + self: knownvalue.MapPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "one": json.Number("1.23"), + "two": json.Number("4.56"), + "three": json.Number("7.89"), + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapValuePartial_String(t *testing.T) { + t.Parallel() + + got := knownvalue.MapPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "three": knownvalue.Float64Exact(7.89), + }).String() + + if diff := cmp.Diff(got, "map[one:1.23 three:7.89]"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/map_size.go b/knownvalue/map_size.go new file mode 100644 index 000000000..c0966823c --- /dev/null +++ b/knownvalue/map_size.go @@ -0,0 +1,55 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "fmt" + "strconv" +) + +var _ Check = mapSizeExact{} + +type mapSizeExact struct { + size int +} + +// CheckValue verifies that the passed value is a list, map, object, +// or set, and contains a matching number of elements. +func (v mapSizeExact) CheckValue(other any) error { + otherVal, ok := other.(map[string]any) + + if !ok { + return fmt.Errorf("expected map[string]any value for MapSizeExact check, got: %T", other) + } + + if len(otherVal) != v.size { + expectedElements := "elements" + actualElements := "elements" + + if v.size == 1 { + expectedElements = "element" + } + + if len(otherVal) == 1 { + actualElements = "element" + } + + return fmt.Errorf("expected %d %s for MapSizeExact check, got %d %s", v.size, expectedElements, len(otherVal), actualElements) + } + + return nil +} + +// String returns the string representation of the value. +func (v mapSizeExact) String() string { + return strconv.Itoa(v.size) +} + +// MapSizeExact returns a Check for asserting that +// a map has size elements. +func MapSizeExact(size int) mapSizeExact { + return mapSizeExact{ + size: size, + } +} diff --git a/knownvalue/map_size_test.go b/knownvalue/map_size_test.go new file mode 100644 index 000000000..d90814017 --- /dev/null +++ b/knownvalue/map_size_test.go @@ -0,0 +1,84 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestMapElements_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.MapSizeExact(0), + expectedError: fmt.Errorf("expected map[string]any value for MapSizeExact check, got: "), + }, + "zero-other": { + self: knownvalue.MapSizeExact(0), + other: map[string]any{}, // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.MapSizeExact(3), + expectedError: fmt.Errorf("expected map[string]any value for MapSizeExact check, got: "), + }, + "wrong-type": { + self: knownvalue.MapSizeExact(3), + other: 1.234, + expectedError: fmt.Errorf("expected map[string]any value for MapSizeExact check, got: float64"), + }, + "empty": { + self: knownvalue.MapSizeExact(3), + other: map[string]any{}, + expectedError: fmt.Errorf("expected 3 elements for MapSizeExact check, got 0 elements"), + }, + "wrong-length": { + self: knownvalue.MapSizeExact(3), + other: map[string]any{ + "one": int64(123), + "two": int64(456), + }, + expectedError: fmt.Errorf("expected 3 elements for MapSizeExact check, got 2 elements"), + }, + "equal": { + self: knownvalue.MapSizeExact(3), + other: map[string]any{ + "one": int64(123), + "two": int64(456), + "three": int64(789), + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapElements_String(t *testing.T) { + t.Parallel() + + got := knownvalue.MapSizeExact(2).String() + + if diff := cmp.Diff(got, "2"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/map_test.go b/knownvalue/map_test.go new file mode 100644 index 000000000..59ff34458 --- /dev/null +++ b/knownvalue/map_test.go @@ -0,0 +1,148 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestMapValue_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.MapExact(map[string]knownvalue.Check{}), + expectedError: fmt.Errorf("expected map[string]any value for MapExact check, got: "), + }, + "zero-other": { + self: knownvalue.MapExact(map[string]knownvalue.Check{}), + other: map[string]any{}, // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.MapExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + expectedError: fmt.Errorf("expected map[string]any value for MapExact check, got: "), + }, + "wrong-type": { + self: knownvalue.MapExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + other: 1.234, + expectedError: fmt.Errorf("expected map[string]any value for MapExact check, got: float64"), + }, + "empty": { + self: knownvalue.MapExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{}, + expectedError: fmt.Errorf("expected 3 elements for MapExact check, got 0 elements"), + }, + "wrong-length": { + self: knownvalue.MapExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "one": json.Number("1.23"), + "two": json.Number("4.56"), + }, + expectedError: fmt.Errorf("expected 3 elements for MapExact check, got 2 elements"), + }, + "not-equal": { + self: knownvalue.MapExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "one": json.Number("1.23"), + "two": json.Number("4.56"), + "three": json.Number("6.54"), + }, + expectedError: fmt.Errorf("three map element: expected value 7.89 for Float64Exact check, got: 6.54"), + }, + "wrong-order": { + self: knownvalue.MapExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "one": json.Number("1.23"), + "two": json.Number("7.89"), + "three": json.Number("4.56"), + }, + expectedError: fmt.Errorf("three map element: expected value 7.89 for Float64Exact check, got: 4.56"), + }, + "key-not-found": { + self: knownvalue.MapExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "four": json.Number("1.23"), + "five": json.Number("7.89"), + "six": json.Number("4.56"), + }, + expectedError: fmt.Errorf("missing element one for MapExact check"), + }, + "equal": { + self: knownvalue.MapExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "one": json.Number("1.23"), + "two": json.Number("4.56"), + "three": json.Number("7.89"), + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapValue_String(t *testing.T) { + t.Parallel() + + got := knownvalue.MapExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }).String() + + if diff := cmp.Diff(got, "map[one:1.23 three:7.89 two:4.56]"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/not_null.go b/knownvalue/not_null.go new file mode 100644 index 000000000..d7ae68904 --- /dev/null +++ b/knownvalue/not_null.go @@ -0,0 +1,32 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "fmt" +) + +var _ Check = notNull{} + +type notNull struct{} + +// CheckValue determines whether the passed value is nil. +func (v notNull) CheckValue(other any) error { + if other == nil { + return fmt.Errorf("expected non-nil value for NotNull check, got: %T", other) + } + + return nil +} + +// String returns the string representation of notNull. +func (v notNull) String() string { + return "not-null" +} + +// NotNull returns a Check for asserting the value passed +// to the CheckValue method is not nil. +func NotNull() notNull { + return notNull{} +} diff --git a/knownvalue/not_null_test.go b/knownvalue/not_null_test.go new file mode 100644 index 000000000..4ed3e44d5 --- /dev/null +++ b/knownvalue/not_null_test.go @@ -0,0 +1,59 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestNotNullValue_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.NotNull(), + expectedError: fmt.Errorf("expected non-nil value for NotNull check, got: "), + }, + "not-nil": { + self: knownvalue.NotNull(), + other: nil, + expectedError: fmt.Errorf("expected non-nil value for NotNull check, got: "), + }, + "equal": { + self: knownvalue.NotNull(), + other: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNotNullValue_String(t *testing.T) { + t.Parallel() + + got := knownvalue.NotNull().String() + + if diff := cmp.Diff(got, "not-null"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/null.go b/knownvalue/null.go new file mode 100644 index 000000000..24e6b7e2b --- /dev/null +++ b/knownvalue/null.go @@ -0,0 +1,32 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "fmt" +) + +var _ Check = null{} + +type null struct{} + +// CheckValue determines whether the passed value is nil. +func (v null) CheckValue(other any) error { + if other != nil { + return fmt.Errorf("expected nil value for Null check, got: %T", other) + } + + return nil +} + +// String returns the string representation of null. +func (v null) String() string { + return "null" +} + +// Null returns a Check for asserting the value passed +// to the CheckValue method is nil. +func Null() null { + return null{} +} diff --git a/knownvalue/null_test.go b/knownvalue/null_test.go new file mode 100644 index 000000000..d6cb27a2d --- /dev/null +++ b/knownvalue/null_test.go @@ -0,0 +1,58 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestNullValue_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.Null(), + }, + "not-nil": { + self: knownvalue.Null(), + other: false, + expectedError: fmt.Errorf("expected nil value for Null check, got: bool"), + }, + "equal": { + self: knownvalue.Null(), + other: nil, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNullValue_String(t *testing.T) { + t.Parallel() + + got := knownvalue.Null().String() + + if diff := cmp.Diff(got, "null"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/number.go b/knownvalue/number.go new file mode 100644 index 000000000..d101b1f68 --- /dev/null +++ b/knownvalue/number.go @@ -0,0 +1,56 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "encoding/json" + "fmt" + "math/big" +) + +var _ Check = numberExact{} + +type numberExact struct { + value *big.Float +} + +// CheckValue determines whether the passed value is of type *big.Float, and +// contains a matching *big.Float value. +func (v numberExact) CheckValue(other any) error { + if v.value == nil { + return fmt.Errorf("value in NumberExact check is nil") + } + + jsonNum, ok := other.(json.Number) + + if !ok { + return fmt.Errorf("expected json.Number value for NumberExact check, got: %T", other) + } + + otherVal, _, err := big.ParseFloat(jsonNum.String(), 10, 512, big.ToNearestEven) + + if err != nil { + return fmt.Errorf("expected json.Number to be parseable as big.Float value for NumberExact check: %s", err) + } + + if v.value.Cmp(otherVal) != 0 { + return fmt.Errorf("expected value %s for NumberExact check, got: %s", v.String(), otherVal.Text('f', -1)) + } + + return nil +} + +// String returns the string representation of the *big.Float value. +func (v numberExact) String() string { + return v.value.Text('f', -1) +} + +// NumberExact returns a Check for asserting equality between the +// supplied *big.Float and the value passed to the CheckValue method. +// The CheckValue method uses 512-bit precision to perform this assertion. +func NumberExact(value *big.Float) numberExact { + return numberExact{ + value: value, + } +} diff --git a/knownvalue/number_func.go b/knownvalue/number_func.go new file mode 100644 index 000000000..75cce4938 --- /dev/null +++ b/knownvalue/number_func.go @@ -0,0 +1,48 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "encoding/json" + "fmt" + "math/big" +) + +var _ Check = numberFunc{} + +type numberFunc struct { + checkFunc func(v *big.Float) error +} + +// CheckValue determines whether the passed value is of type int64, and +// returns no error from the provided check function +func (v numberFunc) CheckValue(other any) error { + jsonNum, ok := other.(json.Number) + + if !ok { + return fmt.Errorf("expected json.Number value for NumberFunc check, got: %T", other) + } + + otherVal, _, err := big.ParseFloat(jsonNum.String(), 10, 512, big.ToNearestEven) + if err != nil { + return fmt.Errorf("expected json.Number to be parseable as big.Float value for NumberFunc check: %s", err) + } + + return v.checkFunc(otherVal) +} + +// String returns the int64 representation of the value. +func (v numberFunc) String() string { + // Validation is up the the implementer of the function, so there are no + // int64 literal or regex comparers to print here + return "NumberFunc" +} + +// NumberFunc returns a Check for passing the int64 value in state +// to the provided check function +func NumberFunc(fn func(v *big.Float) error) numberFunc { + return numberFunc{ + checkFunc: fn, + } +} diff --git a/knownvalue/number_func_test.go b/knownvalue/number_func_test.go new file mode 100644 index 000000000..e9075c6e5 --- /dev/null +++ b/knownvalue/number_func_test.go @@ -0,0 +1,95 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "encoding/json" + "fmt" + "math/big" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestNumberFunc_CheckValue(t *testing.T) { + t.Parallel() + + expected, _, err := big.ParseFloat("1.797693134862315797693134862315797693134862315", 10, 512, big.ToNearestEven) + if err != nil { + t.Errorf("%s", err) + } + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "nil": { + self: knownvalue.NumberFunc(func(*big.Float) error { return nil }), + expectedError: fmt.Errorf("expected json.Number value for NumberFunc check, got: "), + }, + "wrong-type": { + self: knownvalue.NumberFunc(func(*big.Float) error { return nil }), + other: "wrongtype", + expectedError: fmt.Errorf("expected json.Number value for NumberFunc check, got: string"), + }, + "no-digits": { + self: knownvalue.NumberFunc(func(*big.Float) error { return nil }), + other: json.Number("str"), + expectedError: fmt.Errorf("expected json.Number to be parseable as big.Float value for NumberFunc check: number has no digits"), + }, + "failure": { + self: knownvalue.NumberFunc(func(i *big.Float) error { + if i.Cmp(expected) != 0 { + return fmt.Errorf("%s was not %s", i.Text('f', -1), expected.Text('f', -1)) + } + return nil + }), + other: json.Number("1.667114241575161769818551140818851511176942075"), + expectedError: fmt.Errorf("1.667114241575161769818551140818851511176942075 was not 1.797693134862315797693134862315797693134862315"), + }, + "success-precise-number": { + self: knownvalue.NumberFunc(func(i *big.Float) error { + if i.Cmp(expected) != 0 { + return fmt.Errorf("%s was not %s", i.Text('f', -1), expected.Text('f', -1)) + } + return nil + }), + other: json.Number("1.797693134862315797693134862315797693134862315"), + }, + "success-whole-number": { + self: knownvalue.NumberFunc(func(i *big.Float) error { + if i.Cmp(big.NewFloat(1)) != 0 { + return fmt.Errorf("%s was not %s", i.Text('f', -1), big.NewFloat(1).Text('f', -1)) + } + return nil + }), + other: json.Number("1"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberFunc_String(t *testing.T) { + t.Parallel() + + got := knownvalue.NumberFunc(func(*big.Float) error { return nil }).String() + + if diff := cmp.Diff(got, "NumberFunc"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/number_test.go b/knownvalue/number_test.go new file mode 100644 index 000000000..26446f50e --- /dev/null +++ b/knownvalue/number_test.go @@ -0,0 +1,87 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "encoding/json" + "fmt" + "math/big" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestNumberValue_Equal(t *testing.T) { + t.Parallel() + + bigFloat, _, err := big.ParseFloat("1.797693134862315797693134862315797693134862315", 10, 512, big.ToNearestEven) + + if err != nil { + t.Errorf("%s", err) + } + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.NumberExact(nil), + expectedError: fmt.Errorf("value in NumberExact check is nil"), + }, + "zero-other": { + self: knownvalue.NumberExact(nil), + other: json.Number("1.797693134862315797693134862315797693134862314"), // checking against the underlying value field zero-value + expectedError: fmt.Errorf("value in NumberExact check is nil"), + }, + "nil": { + self: knownvalue.NumberExact(bigFloat), + expectedError: fmt.Errorf("expected json.Number value for NumberExact check, got: "), + }, + "wrong-type": { + self: knownvalue.NumberExact(bigFloat), + other: json.Number("str"), + expectedError: fmt.Errorf("expected json.Number to be parseable as big.Float value for NumberExact check: number has no digits"), + }, + "not-equal": { + self: knownvalue.NumberExact(bigFloat), + other: json.Number("1.797693134862315797693134862315797693134862314"), + expectedError: fmt.Errorf("expected value 1.797693134862315797693134862315797693134862315 for NumberExact check, got: 1.797693134862315797693134862315797693134862314"), + }, + "equal": { + self: knownvalue.NumberExact(bigFloat), + other: json.Number("1.797693134862315797693134862315797693134862315"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberValue_String(t *testing.T) { + t.Parallel() + + bigFloat, _, err := big.ParseFloat("1.797693134862315797693134862315797693134862315", 10, 512, big.ToNearestEven) + + if err != nil { + t.Errorf("%s", err) + } + + got := knownvalue.NumberExact(bigFloat).String() + + if diff := cmp.Diff(got, "1.797693134862315797693134862315797693134862315"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/object.go b/knownvalue/object.go new file mode 100644 index 000000000..cc97542c4 --- /dev/null +++ b/knownvalue/object.go @@ -0,0 +1,116 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "fmt" + "maps" + "slices" + "sort" +) + +var _ Check = objectExact{} + +type objectExact struct { + value map[string]Check +} + +// CheckValue determines whether the passed value is of type map[string]any, and +// contains matching object entries. +func (v objectExact) CheckValue(other any) error { + otherVal, ok := other.(map[string]any) + + if !ok { + return fmt.Errorf("expected map[string]any value for ObjectExact check, got: %T", other) + } + + if len(otherVal) != len(v.value) { + deltaMsg := "" + if len(otherVal) > len(v.value) { + deltaMsg = createDeltaString(otherVal, v.value, "actual value has extra attribute(s): ") + } else { + deltaMsg = createDeltaString(v.value, otherVal, "actual value is missing attribute(s): ") + } + + return fmt.Errorf("expected %d attribute(s) for ObjectExact check, got %d attribute(s): %s", len(v.value), len(otherVal), deltaMsg) + } + + var keys []string + + for k := range v.value { + keys = append(keys, k) + } + + sort.SliceStable(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + for _, k := range keys { + otherValItem, ok := otherVal[k] + + if !ok { + return fmt.Errorf("missing attribute %s for ObjectExact check", k) + } + + if err := v.value[k].CheckValue(otherValItem); err != nil { + return fmt.Errorf("%s object attribute: %s", k, err) + } + } + + return nil +} + +// String returns the string representation of the value. +func (v objectExact) String() string { + var keys []string + + for k := range v.value { + keys = append(keys, k) + } + + sort.SliceStable(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + mapVals := make(map[string]string, len(keys)) + + for _, k := range keys { + mapVals[k] = v.value[k].String() + } + + return fmt.Sprintf("%v", mapVals) +} + +// ObjectExact returns a Check for asserting equality between the supplied +// map[string]Check and the value passed to the CheckValue method. The map +// keys represent object attribute names. +func ObjectExact(value map[string]Check) objectExact { + return objectExact{ + value: value, + } +} + +// createDeltaString prints the map keys that are present in mapA and not present in mapB +func createDeltaString[T any, V any](mapA map[string]T, mapB map[string]V, msgPrefix string) string { + deltaMsg := "" + + deltaMap := make(map[string]T, len(mapA)) + maps.Copy(deltaMap, mapA) + for key := range mapB { + delete(deltaMap, key) + } + + deltaKeys := slices.Sorted(maps.Keys(deltaMap)) + + for i, k := range deltaKeys { + if i == 0 { + deltaMsg += msgPrefix + } else { + deltaMsg += ", " + } + deltaMsg += fmt.Sprintf("%q", k) + } + + return deltaMsg +} diff --git a/knownvalue/object_partial.go b/knownvalue/object_partial.go new file mode 100644 index 000000000..775ab4c34 --- /dev/null +++ b/knownvalue/object_partial.go @@ -0,0 +1,80 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "fmt" + "sort" +) + +var _ Check = objectPartial{} + +type objectPartial struct { + value map[string]Check +} + +// CheckValue determines whether the passed value is of type map[string]any, and +// contains matching map entries. +func (v objectPartial) CheckValue(other any) error { + otherVal, ok := other.(map[string]any) + + if !ok { + return fmt.Errorf("expected map[string]any value for ObjectPartial check, got: %T", other) + } + + var keys []string + + for k := range v.value { + keys = append(keys, k) + } + + sort.SliceStable(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + for _, k := range keys { + otherValItem, ok := otherVal[k] + + if !ok { + return fmt.Errorf("missing attribute %s for ObjectPartial check", k) + } + + if err := v.value[k].CheckValue(otherValItem); err != nil { + return fmt.Errorf("%s object attribute: %s", k, err) + } + } + + return nil +} + +// String returns the string representation of the value. +func (v objectPartial) String() string { + var keys []string + + for k := range v.value { + keys = append(keys, k) + } + + sort.SliceStable(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + mapVals := make(map[string]string, len(keys)) + + for _, k := range keys { + mapVals[k] = v.value[k].String() + } + + return fmt.Sprintf("%v", mapVals) +} + +// ObjectPartial returns a Check for asserting partial equality between the +// supplied map[string]Check and the value passed to the CheckValue method. The map +// keys represent object attribute names. Only the object attributes defined by the +// map keys within the supplied map[string]Check are checked. +func ObjectPartial(value map[string]Check) objectPartial { + return objectPartial{ + value: value, + } +} diff --git a/knownvalue/object_partial_test.go b/knownvalue/object_partial_test.go new file mode 100644 index 000000000..7fae38086 --- /dev/null +++ b/knownvalue/object_partial_test.go @@ -0,0 +1,140 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestObjectValuePartial_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.ObjectPartial(map[string]knownvalue.Check{}), + expectedError: fmt.Errorf("expected map[string]any value for ObjectPartial check, got: "), + }, + "zero-other": { + self: knownvalue.ObjectPartial(map[string]knownvalue.Check{}), + other: map[string]any{}, // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.ObjectPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "three": knownvalue.Float64Exact(7.89), + }), + expectedError: fmt.Errorf("expected map[string]any value for ObjectPartial check, got: "), + }, + "wrong-type": { + self: knownvalue.ObjectPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "three": knownvalue.Float64Exact(7.89), + }), + other: 1.234, + expectedError: fmt.Errorf("expected map[string]any value for ObjectPartial check, got: float64"), + }, + "empty": { + self: knownvalue.ObjectPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{}, + expectedError: fmt.Errorf("missing attribute one for ObjectPartial check"), + }, + "wrong-length": { + self: knownvalue.ObjectPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "one": json.Number("1.23"), + "two": json.Number("4.56"), + }, + expectedError: fmt.Errorf("missing attribute three for ObjectPartial check"), + }, + "not-equal": { + self: knownvalue.ObjectPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "one": json.Number("1.23"), + "two": json.Number("4.56"), + "three": json.Number("6.54"), + }, + expectedError: fmt.Errorf("three object attribute: expected value 7.89 for Float64Exact check, got: 6.54"), + }, + "wrong-order": { + self: knownvalue.ObjectPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "one": json.Number("1.23"), + "two": json.Number("7.89"), + "three": json.Number("4.56"), + }, + expectedError: fmt.Errorf("three object attribute: expected value 7.89 for Float64Exact check, got: 4.56"), + }, + "key-not-found": { + self: knownvalue.ObjectPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "four": json.Number("1.23"), + "five": json.Number("7.89"), + "six": json.Number("4.56"), + }, + expectedError: fmt.Errorf("missing attribute one for ObjectPartial check"), + }, + "equal": { + self: knownvalue.ObjectPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "one": json.Number("1.23"), + "two": json.Number("4.56"), + "three": json.Number("7.89"), + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectValuePartial_String(t *testing.T) { + t.Parallel() + + got := knownvalue.ObjectPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "three": knownvalue.Float64Exact(7.89), + }).String() + + if diff := cmp.Diff(got, "map[one:1.23 three:7.89]"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/object_test.go b/knownvalue/object_test.go new file mode 100644 index 000000000..173aba0ae --- /dev/null +++ b/knownvalue/object_test.go @@ -0,0 +1,188 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestObjectValue_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.ObjectExact(map[string]knownvalue.Check{}), + expectedError: fmt.Errorf("expected map[string]any value for ObjectExact check, got: "), + }, + "zero-other": { + self: knownvalue.ObjectExact(map[string]knownvalue.Check{}), + other: map[string]any{}, // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.ObjectExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + expectedError: fmt.Errorf("expected map[string]any value for ObjectExact check, got: "), + }, + "wrong-type": { + self: knownvalue.ObjectExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + other: 1.234, + expectedError: fmt.Errorf("expected map[string]any value for ObjectExact check, got: float64"), + }, + "empty": { + self: knownvalue.ObjectExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{}, + expectedError: fmt.Errorf("expected 3 attribute(s) for ObjectExact check, got 0 attribute(s): actual value is missing attribute(s): \"one\", \"three\", \"two\""), + }, + "missing-one-attribute": { + self: knownvalue.ObjectExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "one": json.Number("1.23"), + "two": json.Number("4.56"), + }, + expectedError: fmt.Errorf("expected 3 attribute(s) for ObjectExact check, got 2 attribute(s): actual value is missing attribute(s): \"three\""), + }, + "missing-multiple-attributes": { + self: knownvalue.ObjectExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + "four": knownvalue.Float64Exact(0.12), + "five": knownvalue.Float64Exact(3.45), + }), + other: map[string]any{ + "one": json.Number("1.23"), + "two": json.Number("4.56"), + }, + expectedError: fmt.Errorf("expected 5 attribute(s) for ObjectExact check, got 2 attribute(s): actual value is missing attribute(s): \"five\", \"four\", \"three\""), + }, + "extra-one-attribute": { + self: knownvalue.ObjectExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + }), + other: map[string]any{ + "one": json.Number("1.23"), + "two": json.Number("4.56"), + "three": json.Number("7.89"), + }, + expectedError: fmt.Errorf("expected 2 attribute(s) for ObjectExact check, got 3 attribute(s): actual value has extra attribute(s): \"three\""), + }, + "extra-multiple-attributes": { + self: knownvalue.ObjectExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + }), + other: map[string]any{ + "one": json.Number("1.23"), + "two": json.Number("4.56"), + "three": json.Number("7.89"), + "four": json.Number("0.12"), + "five": json.Number("3.45"), + }, + expectedError: fmt.Errorf("expected 2 attribute(s) for ObjectExact check, got 5 attribute(s): actual value has extra attribute(s): \"five\", \"four\", \"three\""), + }, + "not-equal": { + self: knownvalue.ObjectExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "one": json.Number("1.23"), + "two": json.Number("4.56"), + "three": json.Number("6.54"), + }, + expectedError: fmt.Errorf("three object attribute: expected value 7.89 for Float64Exact check, got: 6.54"), + }, + "wrong-order": { + self: knownvalue.ObjectExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "one": json.Number("1.23"), + "two": json.Number("7.89"), + "three": json.Number("4.56"), + }, + expectedError: fmt.Errorf("three object attribute: expected value 7.89 for Float64Exact check, got: 4.56"), + }, + "key-not-found": { + self: knownvalue.ObjectExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "four": json.Number("1.23"), + "five": json.Number("7.89"), + "six": json.Number("4.56"), + }, + expectedError: fmt.Errorf("missing attribute one for ObjectExact check"), + }, + "equal": { + self: knownvalue.ObjectExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "one": json.Number("1.23"), + "two": json.Number("4.56"), + "three": json.Number("7.89"), + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectValue_String(t *testing.T) { + t.Parallel() + + got := knownvalue.ObjectExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }).String() + + if diff := cmp.Diff(got, "map[one:1.23 three:7.89 two:4.56]"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/set.go b/knownvalue/set.go new file mode 100644 index 000000000..206f26698 --- /dev/null +++ b/knownvalue/set.go @@ -0,0 +1,86 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "fmt" +) + +var _ Check = setExact{} + +type setExact struct { + value []Check +} + +// CheckValue determines whether the passed value is of type []any, and +// contains matching slice entries independent of the sequence. +func (v setExact) CheckValue(other any) error { + otherVal, ok := other.([]any) + + if !ok { + return fmt.Errorf("expected []any value for SetExact check, got: %T", other) + } + + if len(otherVal) != len(v.value) { + expectedElements := "elements" + actualElements := "elements" + + if len(v.value) == 1 { + expectedElements = "element" + } + + if len(otherVal) == 1 { + actualElements = "element" + } + + return fmt.Errorf("expected %d %s for SetExact check, got %d %s", len(v.value), expectedElements, len(otherVal), actualElements) + } + + otherValCopy := make([]any, len(otherVal)) + + copy(otherValCopy, otherVal) + + for i := 0; i < len(v.value); i++ { + err := fmt.Errorf("missing value %s for SetExact check", v.value[i].String()) + + for j := 0; j < len(otherValCopy); j++ { + checkValueErr := v.value[i].CheckValue(otherValCopy[j]) + + if checkValueErr == nil { + otherValCopy[j] = otherValCopy[len(otherValCopy)-1] + otherValCopy = otherValCopy[:len(otherValCopy)-1] + + err = nil + + break + } + } + + if err != nil { + return err + } + } + + return nil +} + +// String returns the string representation of the value. +func (v setExact) String() string { + var setVals []string + + for _, val := range v.value { + setVals = append(setVals, val.String()) + } + + return fmt.Sprintf("%s", setVals) +} + +// SetExact returns a Check for asserting equality between the +// supplied []Check and the value passed to the CheckValue method. +// This is an order-independent check. +func SetExact(value []Check) setExact { + return setExact{ + value: value, + } +} diff --git a/knownvalue/set_partial.go b/knownvalue/set_partial.go new file mode 100644 index 000000000..dcbcd2ff1 --- /dev/null +++ b/knownvalue/set_partial.go @@ -0,0 +1,72 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "fmt" +) + +var _ Check = setPartial{} + +type setPartial struct { + value []Check +} + +// CheckValue determines whether the passed value is of type []any, and +// contains matching slice entries in any sequence. +func (v setPartial) CheckValue(other any) error { + otherVal, ok := other.([]any) + + if !ok { + return fmt.Errorf("expected []any value for SetPartial check, got: %T", other) + } + + otherValCopy := make([]any, len(otherVal)) + + copy(otherValCopy, otherVal) + + for i := 0; i < len(v.value); i++ { + err := fmt.Errorf("missing value %s for SetPartial check", v.value[i].String()) + + for j := 0; j < len(otherValCopy); j++ { + checkValueErr := v.value[i].CheckValue(otherValCopy[j]) + + if checkValueErr == nil { + otherValCopy[j] = otherValCopy[len(otherValCopy)-1] + otherValCopy = otherValCopy[:len(otherValCopy)-1] + + err = nil + + break + } + } + + if err != nil { + return err + } + } + + return nil +} + +// String returns the string representation of the value. +func (v setPartial) String() string { + var setVals []string + + for _, val := range v.value { + setVals = append(setVals, val.String()) + } + + return fmt.Sprintf("%s", setVals) +} + +// SetPartial returns a Check for asserting partial equality between the +// supplied []Check and the value passed to the CheckValue method. Only the +// elements defined within the supplied []Check are checked. This is an +// order-independent check. +func SetPartial(value []Check) setPartial { + return setPartial{ + value: value, + } +} diff --git a/knownvalue/set_partial_test.go b/knownvalue/set_partial_test.go new file mode 100644 index 000000000..b936c4c97 --- /dev/null +++ b/knownvalue/set_partial_test.go @@ -0,0 +1,125 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestSetValuePartial_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.SetPartial([]knownvalue.Check{}), + expectedError: fmt.Errorf("expected []any value for SetPartial check, got: "), + }, + "zero-other": { + self: knownvalue.SetPartial([]knownvalue.Check{}), + other: []any{}, // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.Float64Exact(1.23), + knownvalue.Float64Exact(4.56), + knownvalue.Float64Exact(7.89), + }), + expectedError: fmt.Errorf("expected []any value for SetPartial check, got: "), + }, + "wrong-type": { + self: knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.Float64Exact(1.23), + knownvalue.Float64Exact(4.56), + knownvalue.Float64Exact(7.89), + }), + other: 1.234, + expectedError: fmt.Errorf("expected []any value for SetPartial check, got: float64"), + }, + "equal-empty": { + self: knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.Float64Exact(1.23), + knownvalue.Float64Exact(4.56), + knownvalue.Float64Exact(7.89), + }), + other: []any{}, + expectedError: fmt.Errorf("missing value 1.23 for SetPartial check"), + }, + "not-equal": { + self: knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.Float64Exact(1.23), + knownvalue.Float64Exact(4.56), + knownvalue.Float64Exact(7.89), + }), + other: []any{ + json.Number("1.23"), + json.Number("4.56"), + json.Number("6.54"), + json.Number("5.46"), + }, + expectedError: fmt.Errorf("missing value 7.89 for SetPartial check"), + }, + "equal-different-order": { + self: knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.Float64Exact(1.23), + knownvalue.Float64Exact(4.56), + knownvalue.Float64Exact(7.89), + }), + other: []any{ + json.Number("1.23"), + json.Number("0.00"), + json.Number("7.89"), + json.Number("4.56"), + }, + }, + "equal-same-order": { + self: knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.Float64Exact(1.23), + knownvalue.Float64Exact(4.56), + knownvalue.Float64Exact(7.89), + }), + other: []any{ + json.Number("1.23"), + json.Number("0.00"), + json.Number("4.56"), + json.Number("7.89"), + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetValuePartial_String(t *testing.T) { + t.Parallel() + + got := knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.Float64Exact(1.23), + knownvalue.Float64Exact(4.56), + knownvalue.Float64Exact(7.89), + }).String() + + if diff := cmp.Diff(got, "[1.23 4.56 7.89]"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/set_size.go b/knownvalue/set_size.go new file mode 100644 index 000000000..aa3cce170 --- /dev/null +++ b/knownvalue/set_size.go @@ -0,0 +1,55 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "fmt" + "strconv" +) + +var _ Check = setSizeExact{} + +type setSizeExact struct { + size int +} + +// CheckValue verifies that the passed value is a list, map, object, +// or set, and contains a matching number of elements. +func (v setSizeExact) CheckValue(other any) error { + otherVal, ok := other.([]any) + + if !ok { + return fmt.Errorf("expected []any value for SetElementExact check, got: %T", other) + } + + if len(otherVal) != v.size { + expectedElements := "elements" + actualElements := "elements" + + if v.size == 1 { + expectedElements = "element" + } + + if len(otherVal) == 1 { + actualElements = "element" + } + + return fmt.Errorf("expected %d %s for SetElementExact check, got %d %s", v.size, expectedElements, len(otherVal), actualElements) + } + + return nil +} + +// String returns the string representation of the value. +func (v setSizeExact) String() string { + return strconv.FormatInt(int64(v.size), 10) +} + +// SetSizeExact returns a Check for asserting that +// a set has size elements. +func SetSizeExact(size int) setSizeExact { + return setSizeExact{ + size: size, + } +} diff --git a/knownvalue/set_size_test.go b/knownvalue/set_size_test.go new file mode 100644 index 000000000..b45dcdf32 --- /dev/null +++ b/knownvalue/set_size_test.go @@ -0,0 +1,84 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestSetElements_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.SetSizeExact(0), + expectedError: fmt.Errorf("expected []any value for SetElementExact check, got: "), + }, + "zero-other": { + self: knownvalue.SetSizeExact(0), + other: []any{}, // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.SetSizeExact(3), + expectedError: fmt.Errorf("expected []any value for SetElementExact check, got: "), + }, + "wrong-type": { + self: knownvalue.SetSizeExact(3), + other: 1.234, + expectedError: fmt.Errorf("expected []any value for SetElementExact check, got: float64"), + }, + "empty": { + self: knownvalue.SetSizeExact(3), + other: []any{}, + expectedError: fmt.Errorf("expected 3 elements for SetElementExact check, got 0 elements"), + }, + "wrong-length": { + self: knownvalue.SetSizeExact(3), + other: []any{ + int64(123), + int64(456), + }, + expectedError: fmt.Errorf("expected 3 elements for SetElementExact check, got 2 elements"), + }, + "equal": { + self: knownvalue.SetSizeExact(3), + other: []any{ + int64(123), + int64(456), + int64(789), + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetElements_String(t *testing.T) { + t.Parallel() + + got := knownvalue.SetSizeExact(2).String() + + if diff := cmp.Diff(got, "2"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/set_test.go b/knownvalue/set_test.go new file mode 100644 index 000000000..125cd19cf --- /dev/null +++ b/knownvalue/set_test.go @@ -0,0 +1,134 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestSetValue_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.SetExact([]knownvalue.Check{}), + expectedError: fmt.Errorf("expected []any value for SetExact check, got: "), + }, + "zero-other": { + self: knownvalue.SetExact([]knownvalue.Check{}), + other: []any{}, // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.SetExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Int64Exact(456), + knownvalue.Int64Exact(789), + }), + expectedError: fmt.Errorf("expected []any value for SetExact check, got: "), + }, + "wrong-type": { + self: knownvalue.SetExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Int64Exact(456), + knownvalue.Int64Exact(789), + }), + other: 1.234, + expectedError: fmt.Errorf("expected []any value for SetExact check, got: float64"), + }, + "empty": { + self: knownvalue.SetExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Int64Exact(456), + knownvalue.Int64Exact(789), + }), + other: []any{}, + expectedError: fmt.Errorf("expected 3 elements for SetExact check, got 0 elements"), + }, + "wrong-length": { + self: knownvalue.SetExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Int64Exact(456), + knownvalue.Int64Exact(789), + }), + other: []any{ + json.Number("123"), + json.Number("456"), + }, + expectedError: fmt.Errorf("expected 3 elements for SetExact check, got 2 elements"), + }, + "not-equal": { + self: knownvalue.SetExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Int64Exact(456), + knownvalue.Int64Exact(789), + }), + other: []any{ + json.Number("123"), + json.Number("456"), + json.Number("654"), + }, + expectedError: fmt.Errorf("missing value 789 for SetExact check"), + }, + "equal-different-order": { + self: knownvalue.SetExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Int64Exact(456), + knownvalue.Int64Exact(789), + }), + other: []any{ + json.Number("123"), + json.Number("789"), + json.Number("456"), + }, + }, + "equal-same-order": { + self: knownvalue.SetExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Int64Exact(456), + knownvalue.Int64Exact(789), + }), + other: []any{ + json.Number("123"), + json.Number("456"), + json.Number("789"), + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetValue_String(t *testing.T) { + t.Parallel() + + got := knownvalue.SetExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Int64Exact(456), + knownvalue.Int64Exact(789), + }).String() + + if diff := cmp.Diff(got, "[123 456 789]"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/string.go b/knownvalue/string.go new file mode 100644 index 000000000..63d03a507 --- /dev/null +++ b/knownvalue/string.go @@ -0,0 +1,41 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import "fmt" + +var _ Check = stringExact{} + +type stringExact struct { + value string +} + +// CheckValue determines whether the passed value is of type string, and +// contains a matching sequence of bytes. +func (v stringExact) CheckValue(other any) error { + otherVal, ok := other.(string) + + if !ok { + return fmt.Errorf("expected string value for StringExact check, got: %T", other) + } + + if otherVal != v.value { + return fmt.Errorf("expected value %s for StringExact check, got: %s", v.value, otherVal) + } + + return nil +} + +// String returns the string representation of the value. +func (v stringExact) String() string { + return v.value +} + +// StringExact returns a Check for asserting equality between the +// supplied string and a value passed to the CheckValue method. +func StringExact(value string) stringExact { + return stringExact{ + value: value, + } +} diff --git a/knownvalue/string_func.go b/knownvalue/string_func.go new file mode 100644 index 000000000..e1dc3f0b9 --- /dev/null +++ b/knownvalue/string_func.go @@ -0,0 +1,39 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import "fmt" + +var _ Check = stringFunc{} + +type stringFunc struct { + checkFunc func(v string) error +} + +// CheckValue determines whether the passed value is of type string, and +// returns no error from the provided check function +func (v stringFunc) CheckValue(value any) error { + val, ok := value.(string) + + if !ok { + return fmt.Errorf("expected string value for StringFunc check, got: %T", value) + } + + return v.checkFunc(val) +} + +// String returns the string representation of the value. +func (v stringFunc) String() string { + // Validation is up the the implementer of the function, so there are no + // string literal or regex comparers to print here + return "StringFunc" +} + +// StringFunc returns a Check for passing the string value in state +// to the provided check function +func StringFunc(fn func(v string) error) stringFunc { + return stringFunc{ + checkFunc: fn, + } +} diff --git a/knownvalue/string_func_test.go b/knownvalue/string_func_test.go new file mode 100644 index 000000000..7e83fdc94 --- /dev/null +++ b/knownvalue/string_func_test.go @@ -0,0 +1,74 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestStringFunc_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "nil": { + self: knownvalue.StringFunc(func(string) error { return nil }), + expectedError: fmt.Errorf("expected string value for StringFunc check, got: "), + }, + "wrong-type": { + self: knownvalue.StringFunc(func(string) error { return nil }), + other: 1.234, + expectedError: fmt.Errorf("expected string value for StringFunc check, got: float64"), + }, + "failure": { + self: knownvalue.StringFunc(func(s string) error { + if s != "foo" { + return fmt.Errorf("%s was not foo", s) + } + return nil + }), + other: "bar", + expectedError: fmt.Errorf("bar was not foo"), + }, + "success": { + self: knownvalue.StringFunc(func(s string) error { + if s != "foo" { + return fmt.Errorf("%s was not foo", s) + } + return nil + }), + other: "foo", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringFunc_String(t *testing.T) { + t.Parallel() + + got := knownvalue.StringFunc(func(string) error { return nil }).String() + + if diff := cmp.Diff(got, "StringFunc"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/string_regexp.go b/knownvalue/string_regexp.go new file mode 100644 index 000000000..782e29747 --- /dev/null +++ b/knownvalue/string_regexp.go @@ -0,0 +1,45 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "fmt" + "regexp" +) + +var _ Check = stringRegexp{} + +type stringRegexp struct { + regex *regexp.Regexp +} + +// CheckValue determines whether the passed value is of type string, and +// contains a sequence of bytes that match the regular expression supplied +// to StringRegexp. +func (v stringRegexp) CheckValue(other any) error { + otherVal, ok := other.(string) + + if !ok { + return fmt.Errorf("expected string value for StringRegexp check, got: %T", other) + } + + if !v.regex.MatchString(otherVal) { + return fmt.Errorf("expected regex match %s for StringRegexp check, got: %s", v.regex.String(), otherVal) + } + + return nil +} + +// String returns the string representation of the value. +func (v stringRegexp) String() string { + return v.regex.String() +} + +// StringRegexp returns a Check for asserting equality between the +// supplied regular expression and a value passed to the CheckValue method. +func StringRegexp(regex *regexp.Regexp) stringRegexp { + return stringRegexp{ + regex: regex, + } +} diff --git a/knownvalue/string_regexp_test.go b/knownvalue/string_regexp_test.go new file mode 100644 index 000000000..86d5d4834 --- /dev/null +++ b/knownvalue/string_regexp_test.go @@ -0,0 +1,73 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "fmt" + "regexp" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestStringRegexp_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.StringRegexp(regexp.MustCompile("")), + expectedError: fmt.Errorf("expected string value for StringRegexp check, got: "), + }, + "zero-other": { + self: knownvalue.StringRegexp(regexp.MustCompile("")), + other: "", // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.StringRegexp(regexp.MustCompile("str")), + expectedError: fmt.Errorf("expected string value for StringRegexp check, got: "), + }, + "wrong-type": { + self: knownvalue.StringRegexp(regexp.MustCompile("str")), + other: 1.234, + expectedError: fmt.Errorf("expected string value for StringRegexp check, got: float64"), + }, + "not-equal": { + self: knownvalue.StringRegexp(regexp.MustCompile("str")), + other: "rts", + expectedError: fmt.Errorf("expected regex match str for StringRegexp check, got: rts"), + }, + "equal": { + self: knownvalue.StringRegexp(regexp.MustCompile("str")), + other: "str", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringRegexp_String(t *testing.T) { + t.Parallel() + + got := knownvalue.StringRegexp(regexp.MustCompile("^str[0-9a-z]")).String() + + if diff := cmp.Diff(got, "^str[0-9a-z]"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/string_test.go b/knownvalue/string_test.go new file mode 100644 index 000000000..2bab25739 --- /dev/null +++ b/knownvalue/string_test.go @@ -0,0 +1,72 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestStringValue_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.StringExact(""), + expectedError: fmt.Errorf("expected string value for StringExact check, got: "), + }, + "zero-other": { + self: knownvalue.StringExact(""), + other: "", // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.StringExact("str"), + expectedError: fmt.Errorf("expected string value for StringExact check, got: "), + }, + "wrong-type": { + self: knownvalue.StringExact("str"), + other: 1.234, + expectedError: fmt.Errorf("expected string value for StringExact check, got: float64"), + }, + "not-equal": { + self: knownvalue.StringExact("str"), + other: "rts", + expectedError: fmt.Errorf("expected value str for StringExact check, got: rts"), + }, + "equal": { + self: knownvalue.StringExact("str"), + other: "str", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringValue_String(t *testing.T) { + t.Parallel() + + got := knownvalue.StringExact("str").String() + + if diff := cmp.Diff(got, "str"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/tuple.go b/knownvalue/tuple.go new file mode 100644 index 000000000..c034bba73 --- /dev/null +++ b/knownvalue/tuple.go @@ -0,0 +1,67 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "fmt" +) + +var _ Check = tupleExact{} + +type tupleExact struct { + value []Check +} + +// CheckValue determines whether the passed value is of type []any, and +// contains matching slice entries in the same sequence. +func (v tupleExact) CheckValue(other any) error { + otherVal, ok := other.([]any) + + if !ok { + return fmt.Errorf("expected []any value for TupleExact check, got: %T", other) + } + + if len(otherVal) != len(v.value) { + expectedElements := "elements" + actualElements := "elements" + + if len(v.value) == 1 { + expectedElements = "element" + } + + if len(otherVal) == 1 { + actualElements = "element" + } + + return fmt.Errorf("expected %d %s for TupleExact check, got %d %s", len(v.value), expectedElements, len(otherVal), actualElements) + } + + for i := 0; i < len(v.value); i++ { + if err := v.value[i].CheckValue(otherVal[i]); err != nil { + return fmt.Errorf("tuple element index %d: %s", i, err) + } + } + + return nil +} + +// String returns the string representation of the value. +func (v tupleExact) String() string { + var tupleVals []string + + for _, val := range v.value { + tupleVals = append(tupleVals, val.String()) + } + + return fmt.Sprintf("%s", tupleVals) +} + +// TupleExact returns a Check for asserting equality between the +// supplied []Check and the value passed to the CheckValue method. +// This is an order-dependent check. +func TupleExact(value []Check) tupleExact { + return tupleExact{ + value: value, + } +} diff --git a/knownvalue/tuple_partial.go b/knownvalue/tuple_partial.go new file mode 100644 index 000000000..cfd73b2d9 --- /dev/null +++ b/knownvalue/tuple_partial.go @@ -0,0 +1,89 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "bytes" + "fmt" + "sort" + "strings" +) + +var _ Check = tuplePartial{} + +type tuplePartial struct { + value map[int]Check +} + +// CheckValue determines whether the passed value is of type []any, and +// contains matching slice entries in the same sequence. +func (v tuplePartial) CheckValue(other any) error { + otherVal, ok := other.([]any) + + if !ok { + return fmt.Errorf("expected []any value for TuplePartial check, got: %T", other) + } + + var keys []int + + for k := range v.value { + keys = append(keys, k) + } + + sort.SliceStable(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + for _, k := range keys { + if len(otherVal) <= k { + return fmt.Errorf("missing element index %d for TuplePartial check", k) + } + + if err := v.value[k].CheckValue(otherVal[k]); err != nil { + return fmt.Errorf("tuple element %d: %s", k, err) + } + } + + return nil +} + +// String returns the string representation of the value. +func (v tuplePartial) String() string { + var b bytes.Buffer + + b.WriteString("[") + + var keys []int + + var tupleVals []string + + for k := range v.value { + keys = append(keys, k) + } + + sort.SliceStable(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + for _, k := range keys { + tupleVals = append(tupleVals, fmt.Sprintf("%d:%s", k, v.value[k])) + } + + b.WriteString(strings.Join(tupleVals, " ")) + + b.WriteString("]") + + return b.String() +} + +// TuplePartial returns a Check for asserting partial equality between the +// supplied map[int]Check and the value passed to the CheckValue method. The +// map keys represent the zero-ordered element indices within the tuple that is +// being checked. Only the elements at the indices defined within the +// supplied map[int]Check are checked. +func TuplePartial(value map[int]Check) tuplePartial { + return tuplePartial{ + value: value, + } +} diff --git a/knownvalue/tuple_partial_test.go b/knownvalue/tuple_partial_test.go new file mode 100644 index 000000000..56eb34e19 --- /dev/null +++ b/knownvalue/tuple_partial_test.go @@ -0,0 +1,136 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestTuplePartial_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.TuplePartial(map[int]knownvalue.Check{}), + expectedError: fmt.Errorf("expected []any value for TuplePartial check, got: "), + }, + "zero-other": { + self: knownvalue.TuplePartial(map[int]knownvalue.Check{}), + other: []any{}, // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.TuplePartial(map[int]knownvalue.Check{ + 0: knownvalue.Float64Exact(1.23), + 2: knownvalue.StringExact("world"), + 3: knownvalue.Bool(true), + }), + expectedError: fmt.Errorf("expected []any value for TuplePartial check, got: "), + }, + "wrong-type": { + self: knownvalue.TuplePartial(map[int]knownvalue.Check{ + 0: knownvalue.Float64Exact(1.23), + 2: knownvalue.StringExact("world"), + 3: knownvalue.Bool(true), + }), + other: 1.234, + expectedError: fmt.Errorf("expected []any value for TuplePartial check, got: float64"), + }, + "empty": { + self: knownvalue.TuplePartial(map[int]knownvalue.Check{ + 0: knownvalue.Float64Exact(1.23), + 2: knownvalue.StringExact("world"), + 3: knownvalue.Bool(true), + }), + other: []any{}, + expectedError: fmt.Errorf("missing element index 0 for TuplePartial check"), + }, + "wrong-length": { + self: knownvalue.TuplePartial(map[int]knownvalue.Check{ + 0: knownvalue.Float64Exact(1.23), + 2: knownvalue.StringExact("world"), + 3: knownvalue.Bool(true), + }), + other: []any{ + json.Number("1.23"), + "hello", + }, + expectedError: fmt.Errorf("missing element index 2 for TuplePartial check"), + }, + "not-equal": { + self: knownvalue.TuplePartial(map[int]knownvalue.Check{ + 0: knownvalue.Float64Exact(1.23), + 2: knownvalue.StringExact("world"), + 3: knownvalue.Bool(true), + }), + other: []any{ + json.Number("1.23"), + "world", + "hello", + }, + expectedError: fmt.Errorf("tuple element 2: expected value world for StringExact check, got: hello"), + }, + "wrong-order": { + self: knownvalue.TuplePartial(map[int]knownvalue.Check{ + 0: knownvalue.Float64Exact(1.23), + 2: knownvalue.StringExact("world"), + 3: knownvalue.Bool(true), + }), + other: []any{ + json.Number("1.23"), + "world", + true, + }, + expectedError: fmt.Errorf("tuple element 2: expected string value for StringExact check, got: bool"), + }, + "equal": { + self: knownvalue.TuplePartial(map[int]knownvalue.Check{ + 0: knownvalue.Float64Exact(1.23), + 2: knownvalue.StringExact("world"), + 3: knownvalue.Bool(true), + }), + other: []any{ + json.Number("1.23"), + "hello", + "world", + true, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestTuplePartial_String(t *testing.T) { + t.Parallel() + + got := knownvalue.TuplePartial(map[int]knownvalue.Check{ + 0: knownvalue.Float64Exact(1.23), + 2: knownvalue.StringExact("world"), + 3: knownvalue.Bool(true), + }).String() + + if diff := cmp.Diff(got, "[0:1.23 2:world 3:true]"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/tuple_size.go b/knownvalue/tuple_size.go new file mode 100644 index 000000000..a2c2dce3d --- /dev/null +++ b/knownvalue/tuple_size.go @@ -0,0 +1,55 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "fmt" + "strconv" +) + +var _ Check = tupleSizeExact{} + +type tupleSizeExact struct { + size int +} + +// CheckValue verifies that the passed value is a tuple, map, object, +// or set, and contains a matching number of elements. +func (v tupleSizeExact) CheckValue(other any) error { + otherVal, ok := other.([]any) + + if !ok { + return fmt.Errorf("expected []any value for TupleSizeExact check, got: %T", other) + } + + if len(otherVal) != v.size { + expectedElements := "elements" + actualElements := "elements" + + if v.size == 1 { + expectedElements = "element" + } + + if len(otherVal) == 1 { + actualElements = "element" + } + + return fmt.Errorf("expected %d %s for TupleSizeExact check, got %d %s", v.size, expectedElements, len(otherVal), actualElements) + } + + return nil +} + +// String returns the string representation of the value. +func (v tupleSizeExact) String() string { + return strconv.FormatInt(int64(v.size), 10) +} + +// TupleSizeExact returns a Check for asserting that +// a tuple has size elements. +func TupleSizeExact(size int) tupleSizeExact { + return tupleSizeExact{ + size: size, + } +} diff --git a/knownvalue/tuple_size_test.go b/knownvalue/tuple_size_test.go new file mode 100644 index 000000000..bd5954f2f --- /dev/null +++ b/knownvalue/tuple_size_test.go @@ -0,0 +1,85 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestTupleSizeExact_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.TupleSizeExact(0), + expectedError: fmt.Errorf("expected []any value for TupleSizeExact check, got: "), + }, + "zero-other": { + self: knownvalue.TupleSizeExact(0), + other: []any{}, // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.TupleSizeExact(3), + expectedError: fmt.Errorf("expected []any value for TupleSizeExact check, got: "), + }, + "wrong-type": { + self: knownvalue.TupleSizeExact(3), + other: 1.234, + expectedError: fmt.Errorf("expected []any value for TupleSizeExact check, got: float64"), + }, + "empty": { + self: knownvalue.TupleSizeExact(3), + other: []any{}, + expectedError: fmt.Errorf("expected 3 elements for TupleSizeExact check, got 0 elements"), + }, + "wrong-length": { + self: knownvalue.TupleSizeExact(4), + other: []any{ + int64(123), + "hello", + true, + }, + expectedError: fmt.Errorf("expected 4 elements for TupleSizeExact check, got 3 elements"), + }, + "equal": { + self: knownvalue.TupleSizeExact(3), + other: []any{ + int64(123), + "hello", + true, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestTupleSizeExact_String(t *testing.T) { + t.Parallel() + + got := knownvalue.TupleSizeExact(2).String() + + if diff := cmp.Diff(got, "2"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/tuple_test.go b/knownvalue/tuple_test.go new file mode 100644 index 000000000..f5a82d76c --- /dev/null +++ b/knownvalue/tuple_test.go @@ -0,0 +1,135 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestTupleExact_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.TupleExact([]knownvalue.Check{}), + expectedError: fmt.Errorf("expected []any value for TupleExact check, got: "), + }, + "zero-other": { + self: knownvalue.TupleExact([]knownvalue.Check{}), + other: []any{}, // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.TupleExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Bool(true), + knownvalue.StringExact("hello"), + }), + expectedError: fmt.Errorf("expected []any value for TupleExact check, got: "), + }, + "wrong-type": { + self: knownvalue.TupleExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Bool(true), + knownvalue.StringExact("hello"), + }), + other: 1.234, + expectedError: fmt.Errorf("expected []any value for TupleExact check, got: float64"), + }, + "empty": { + self: knownvalue.TupleExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Bool(true), + knownvalue.StringExact("hello"), + }), + other: []any{}, + expectedError: fmt.Errorf("expected 3 elements for TupleExact check, got 0 elements"), + }, + "wrong-length": { + self: knownvalue.TupleExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Bool(true), + knownvalue.StringExact("hello"), + }), + other: []any{ + json.Number("123"), + true, + }, + expectedError: fmt.Errorf("expected 3 elements for TupleExact check, got 2 elements"), + }, + "not-equal": { + self: knownvalue.TupleExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Bool(true), + knownvalue.StringExact("hello"), + }), + other: []any{ + json.Number("123"), + true, + "goodbye", + }, + expectedError: fmt.Errorf("tuple element index 2: expected value hello for StringExact check, got: goodbye"), + }, + "wrong-order": { + self: knownvalue.TupleExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Bool(true), + knownvalue.StringExact("hello"), + }), + other: []any{ + json.Number("123"), + "hello", + true, + }, + expectedError: fmt.Errorf("tuple element index 1: expected bool value for Bool check, got: string"), + }, + "equal": { + self: knownvalue.TupleExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Bool(true), + knownvalue.StringExact("hello"), + }), + other: []any{ + json.Number("123"), + true, + "hello", + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestTupleExact_String(t *testing.T) { + t.Parallel() + + got := knownvalue.TupleExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Bool(true), + knownvalue.StringExact("hello"), + }).String() + + if diff := cmp.Diff(got, "[123 true hello]"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/plancheck/deferred_reason.go b/plancheck/deferred_reason.go new file mode 100644 index 000000000..4787a8c3e --- /dev/null +++ b/plancheck/deferred_reason.go @@ -0,0 +1,21 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package plancheck + +// DeferredReason is a string stored in the plan file which indicates why Terraform +// is deferring a change for a resource. +type DeferredReason string + +const ( + // DeferredReasonResourceConfigUnknown is used to indicate that the resource configuration + // is partially unknown and the real values need to be known before the change can be planned. + DeferredReasonResourceConfigUnknown DeferredReason = "resource_config_unknown" + + // DeferredReasonProviderConfigUnknown is used to indicate that the provider configuration + // is partially unknown and the real values need to be known before the change can be planned. + DeferredReasonProviderConfigUnknown DeferredReason = "provider_config_unknown" + + // DeferredReasonAbsentPrereq is used to indicate that a hard dependency has not been satisfied. + DeferredReasonAbsentPrereq DeferredReason = "absent_prereq" +) diff --git a/plancheck/expect_deferred_change.go b/plancheck/expect_deferred_change.go new file mode 100644 index 000000000..14310ca31 --- /dev/null +++ b/plancheck/expect_deferred_change.go @@ -0,0 +1,49 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package plancheck + +import ( + "context" + "fmt" +) + +var _ PlanCheck = expectDeferredChange{} + +type expectDeferredChange struct { + resourceAddress string + reason DeferredReason +} + +// CheckPlan implements the plan check logic. +func (e expectDeferredChange) CheckPlan(ctx context.Context, req CheckPlanRequest, resp *CheckPlanResponse) { + foundResource := false + + for _, dc := range req.Plan.DeferredChanges { + if dc.ResourceChange == nil || e.resourceAddress != dc.ResourceChange.Address { + continue + } + + if e.reason != DeferredReason(dc.Reason) { + resp.Error = fmt.Errorf("'%s' - expected %q, got deferred reason: %q", dc.ResourceChange.Address, e.reason, dc.Reason) + return + } + + foundResource = true + break + } + + if !foundResource { + resp.Error = fmt.Errorf("%s - No deferred changes found for resource", e.resourceAddress) + return + } +} + +// ExpectDeferredChange returns a plan check that asserts that a given resource will have a +// deferred change in the plan with the given reason. +func ExpectDeferredChange(resourceAddress string, reason DeferredReason) PlanCheck { + return expectDeferredChange{ + resourceAddress: resourceAddress, + reason: reason, + } +} diff --git a/plancheck/expect_deferred_change_test.go b/plancheck/expect_deferred_change_test.go new file mode 100644 index 000000000..c32200f62 --- /dev/null +++ b/plancheck/expect_deferred_change_test.go @@ -0,0 +1,169 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package plancheck_test + +import ( + "context" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func Test_ExpectDeferredChange_Reason_Match(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_9_0), + tfversion.SkipIfNotAlpha(), + }, + AdditionalCLIOptions: &r.AdditionalCLIOptions{ + Plan: r.PlanOptions{AllowDeferral: true}, + Apply: r.ApplyOptions{AllowDeferral: true}, + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + PlanChangeFunc: func(ctx context.Context, req resource.PlanChangeRequest, resp *resource.PlanChangeResponse) { + resp.Deferred = &tfprotov6.Deferred{ + Reason: tfprotov6.DeferredReasonResourceConfigUnknown, + } + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" {}`, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectDeferredChange("test_resource.test", plancheck.DeferredReasonResourceConfigUnknown), + }, + }, + }, + }, + }) +} + +func Test_ExpectDeferredChange_Reason_NoMatch(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_9_0), + tfversion.SkipIfNotAlpha(), + }, + AdditionalCLIOptions: &r.AdditionalCLIOptions{ + Plan: r.PlanOptions{AllowDeferral: true}, + Apply: r.ApplyOptions{AllowDeferral: true}, + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + PlanChangeFunc: func(ctx context.Context, req resource.PlanChangeRequest, resp *resource.PlanChangeResponse) { + resp.Deferred = &tfprotov6.Deferred{ + Reason: tfprotov6.DeferredReasonAbsentPrereq, + } + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" {}`, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectDeferredChange("test_resource.test", plancheck.DeferredReasonProviderConfigUnknown), + }, + }, + ExpectError: regexp.MustCompile(`expected "provider_config_unknown", got deferred reason: "absent_prereq"`), + }, + }, + }) +} + +func Test_ExpectDeferredChange_NoDeferredChanges(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_9_0), + tfversion.SkipIfNotAlpha(), + }, + AdditionalCLIOptions: &r.AdditionalCLIOptions{ + Plan: r.PlanOptions{AllowDeferral: true}, + Apply: r.ApplyOptions{AllowDeferral: true}, + }, + 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: "id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" {}`, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectDeferredChange("test_resource.test", plancheck.DeferredReasonProviderConfigUnknown), + }, + }, + ExpectError: regexp.MustCompile(`No deferred changes found for resource`), + }, + }, + }) +} diff --git a/plancheck/expect_empty_plan.go b/plancheck/expect_empty_plan.go index 92d55f465..8df2e281e 100644 --- a/plancheck/expect_empty_plan.go +++ b/plancheck/expect_empty_plan.go @@ -5,9 +5,8 @@ package plancheck import ( "context" + "errors" "fmt" - - "github.com/hashicorp/terraform-plugin-testing/internal/errorshim" ) var _ PlanCheck = expectEmptyPlan{} @@ -16,21 +15,25 @@ type expectEmptyPlan struct{} // CheckPlan implements the plan check logic. func (e expectEmptyPlan) CheckPlan(ctx context.Context, req CheckPlanRequest, resp *CheckPlanResponse) { - var result error + var result []error + + for output, change := range req.Plan.OutputChanges { + if !change.Actions.NoOp() { + result = append(result, fmt.Errorf("expected empty plan, but output %q has planned action(s): %v", output, change.Actions)) + } + } for _, rc := range req.Plan.ResourceChanges { if !rc.Change.Actions.NoOp() { - // TODO: Once Go 1.20 is the minimum supported version for this module, replace with `errors.Join` function - // - https://github.com/hashicorp/terraform-plugin-testing/issues/99 - result = errorshim.Join(result, fmt.Errorf("expected empty plan, but %s has planned action(s): %v", rc.Address, rc.Change.Actions)) + result = append(result, fmt.Errorf("expected empty plan, but %s has planned action(s): %v", rc.Address, rc.Change.Actions)) } } - resp.Error = result + resp.Error = errors.Join(result...) } -// ExpectEmptyPlan returns a plan check that asserts that there are no resource changes in the plan. -// All resource changes found will be aggregated and returned in a plan check error. +// ExpectEmptyPlan returns a plan check that asserts that there are no output or resource changes in the plan. +// All output and resource changes found will be aggregated and returned in a plan check error. func ExpectEmptyPlan() PlanCheck { return expectEmptyPlan{} } diff --git a/plancheck/expect_empty_plan_test.go b/plancheck/expect_empty_plan_test.go index 516b2a106..8a223c148 100644 --- a/plancheck/expect_empty_plan_test.go +++ b/plancheck/expect_empty_plan_test.go @@ -9,74 +9,128 @@ import ( r "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/tfversion" ) -func Test_ExpectEmptyPlan(t *testing.T) { +func Test_ExpectEmptyPlan_OutputChanges_NoChanges(t *testing.T) { t.Parallel() - r.Test(t, r.TestCase{ + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version0_14_0), // outputs before 0.14 always show as created + }, + // Avoid our own validation that requires at least one provider config. ExternalProviders: map[string]r.ExternalProvider{ - "random": { - Source: "registry.terraform.io/hashicorp/random", + "terraform": {Source: "terraform.io/builtin/terraform"}, + }, + Steps: []r.TestStep{ + { + Config: `output "test" { value = "original" }`, + }, + { + Config: `output "test" { value = "original" }`, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + }, + }, }, }, + }) +} + +func Test_ExpectEmptyPlan_OutputChanges_Error(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version0_14_0), // outputs before 0.14 always show as created + }, + // Avoid our own validation that requires at least one provider config. + ExternalProviders: map[string]r.ExternalProvider{ + "terraform": {Source: "terraform.io/builtin/terraform"}, + }, Steps: []r.TestStep{ { - Config: `resource "random_string" "one" { - length = 16 - }`, + Config: `output "test" { value = "original" }`, }, { - Config: `resource "random_string" "one" { - length = 16 - }`, + Config: `output "test" { value = "new" }`, ConfigPlanChecks: r.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ plancheck.ExpectEmptyPlan(), }, }, + ExpectError: regexp.MustCompile(`output \"test\" has planned action\(s\): \[update\]`), }, }, }) } -func Test_ExpectEmptyPlan_Error(t *testing.T) { +func Test_ExpectEmptyPlan_ResourceChanges_NoChanges(t *testing.T) { t.Parallel() - r.Test(t, r.TestCase{ + r.UnitTest(t, r.TestCase{ ExternalProviders: map[string]r.ExternalProvider{ - "random": { - Source: "registry.terraform.io/hashicorp/random", + "terraform": {Source: "terraform.io/builtin/terraform"}, + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_4_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "terraform_data" "test" {}`, + }, + { + Config: `resource "terraform_data" "test" {}`, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + }, + }, }, }, + }) +} + +func Test_ExpectEmptyPlan_ResourceChanges_Error(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + ExternalProviders: map[string]r.ExternalProvider{ + "terraform": {Source: "terraform.io/builtin/terraform"}, + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_4_0), + }, Steps: []r.TestStep{ { - Config: `resource "random_string" "one" { - length = 16 + Config: `resource "terraform_data" "one" { + triggers_replace = ["original"] } - resource "random_string" "two" { - length = 16 + resource "terraform_data" "two" { + triggers_replace = ["original"] } - resource "random_string" "three" { - length = 16 + resource "terraform_data" "three" { + triggers_replace = ["original"] }`, }, { - Config: `resource "random_string" "one" { - length = 12 + Config: `resource "terraform_data" "one" { + triggers_replace = ["new"] } - resource "random_string" "two" { - length = 16 + resource "terraform_data" "two" { + triggers_replace = ["original"] } - resource "random_string" "three" { - length = 12 + resource "terraform_data" "three" { + triggers_replace = ["new"] }`, ConfigPlanChecks: r.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ plancheck.ExpectEmptyPlan(), }, }, - ExpectError: regexp.MustCompile(`.*?(random_string.one has planned action\(s\): \[delete create\])\n.*?(random_string.three has planned action\(s\): \[delete create\])`), + ExpectError: regexp.MustCompile(`.*?(terraform_data.one has planned action\(s\): \[delete create\])\n.*?(terraform_data.three has planned action\(s\): \[delete create\])`), }, }, }) diff --git a/plancheck/expect_known_output_value.go b/plancheck/expect_known_output_value.go new file mode 100644 index 000000000..bc954c8ed --- /dev/null +++ b/plancheck/expect_known_output_value.go @@ -0,0 +1,68 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package plancheck + +import ( + "context" + "fmt" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +// Resource Plan Check +var _ PlanCheck = expectKnownOutputValue{} + +type expectKnownOutputValue struct { + outputAddress string + knownValue knownvalue.Check +} + +// CheckPlan implements the plan check logic. +func (e expectKnownOutputValue) CheckPlan(ctx context.Context, req CheckPlanRequest, resp *CheckPlanResponse) { + var change *tfjson.Change + + if req.Plan == nil { + resp.Error = fmt.Errorf("plan is nil") + } + + for address, oc := range req.Plan.OutputChanges { + if e.outputAddress == address { + change = oc + + break + } + } + + if change == nil { + resp.Error = fmt.Errorf("%s - Output not found in plan", e.outputAddress) + + return + } + + result, err := tfjsonpath.Traverse(change.After, tfjsonpath.Path{}) + + if err != nil { + resp.Error = err + + return + } + + if err := e.knownValue.CheckValue(result); err != nil { + resp.Error = fmt.Errorf("error checking value for output at path: %s, err: %s", e.outputAddress, err) + + return + } +} + +// ExpectKnownOutputValue returns a plan check that asserts that the specified value +// has a known type, and value. +func ExpectKnownOutputValue(outputAddress string, knownValue knownvalue.Check) PlanCheck { + return expectKnownOutputValue{ + outputAddress: outputAddress, + knownValue: knownValue, + } +} diff --git a/plancheck/expect_known_output_value_at_path.go b/plancheck/expect_known_output_value_at_path.go new file mode 100644 index 000000000..42f07a151 --- /dev/null +++ b/plancheck/expect_known_output_value_at_path.go @@ -0,0 +1,71 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package plancheck + +import ( + "context" + "fmt" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +// Resource Plan Check +var _ PlanCheck = expectKnownOutputValueAtPath{} + +type expectKnownOutputValueAtPath struct { + outputAddress string + outputPath tfjsonpath.Path + knownValue knownvalue.Check +} + +// CheckPlan implements the plan check logic. +func (e expectKnownOutputValueAtPath) CheckPlan(ctx context.Context, req CheckPlanRequest, resp *CheckPlanResponse) { + var change *tfjson.Change + + if req.Plan == nil { + resp.Error = fmt.Errorf("plan is nil") + } + + for address, oc := range req.Plan.OutputChanges { + if e.outputAddress == address { + change = oc + + break + } + } + + if change == nil { + resp.Error = fmt.Errorf("%s - Output not found in plan", e.outputAddress) + + return + } + + result, err := tfjsonpath.Traverse(change.After, e.outputPath) + + if err != nil { + resp.Error = err + + return + } + + if err := e.knownValue.CheckValue(result); err != nil { + resp.Error = fmt.Errorf("error checking value for output at path: %s.%s, err: %s", e.outputAddress, e.outputPath.String(), err) + + return + } +} + +// ExpectKnownOutputValueAtPath returns a plan check that asserts that the specified output at the given path +// has a known type and value. Prior to Terraform v1.3.0 a planned output is marked as fully unknown +// if any attribute is unknown. +func ExpectKnownOutputValueAtPath(outputAddress string, outputPath tfjsonpath.Path, knownValue knownvalue.Check) PlanCheck { + return expectKnownOutputValueAtPath{ + outputAddress: outputAddress, + outputPath: outputPath, + knownValue: knownValue, + } +} diff --git a/plancheck/expect_known_output_value_at_path_test.go b/plancheck/expect_known_output_value_at_path_test.go new file mode 100644 index 000000000..0d61e57dd --- /dev/null +++ b/plancheck/expect_known_output_value_at_path_test.go @@ -0,0 +1,1964 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package plancheck_test + +import ( + "context" + "fmt" + "math/big" + "regexp" + "testing" + + "github.com/google/go-cmp/cmp" + tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestExpectKnownOutputValueAtPath_CheckPlan_ResourceNotFound(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_two_output", + tfjsonpath.New("bool_attribute"), + knownvalue.Bool(true), + ), + }, + }, + ExpectError: regexp.MustCompile("test_resource_two_output - Output not found in plan"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_AttributeValueNull(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" {} + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("bool_attribute"), + knownvalue.Null(), + ), + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("float_attribute"), + knownvalue.Null(), + ), + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("int_attribute"), + knownvalue.Null(), + ), + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_attribute"), + knownvalue.Null(), + ), + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_nested_block"), + knownvalue.ListExact([]knownvalue.Check{}), + ), + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("map_attribute"), + knownvalue.Null(), + ), + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_attribute"), + knownvalue.Null(), + ), + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_nested_block"), + knownvalue.SetExact([]knownvalue.Check{}), + ), + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("string_attribute"), + knownvalue.Null(), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_AttributeValueNotNull(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + float_attribute = 1.23 + int_attribute = 123 + list_attribute = ["value1", "value2"] + list_nested_block { + list_nested_block_attribute = "str" + } + map_attribute = { + key1 = "value1" + } + set_attribute = ["value1", "value2"] + set_nested_block { + set_nested_block_attribute = "str" + } + string_attribute = "str" + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("bool_attribute"), + knownvalue.NotNull(), + ), + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("float_attribute"), + knownvalue.NotNull(), + ), + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("int_attribute"), + knownvalue.NotNull(), + ), + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_attribute"), + knownvalue.NotNull(), + ), + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_nested_block"), + knownvalue.ListSizeExact(1), + ), + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("map_attribute"), + knownvalue.NotNull(), + ), + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_attribute"), + knownvalue.NotNull(), + ), + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_nested_block"), + knownvalue.SetSizeExact(1), + ), + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("string_attribute"), + knownvalue.NotNull(), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_Bool(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("bool_attribute"), + knownvalue.Bool(true), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_Bool_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("bool_attribute"), + knownvalue.Float64Exact(1.23), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: test_resource_one_output.bool_attribute, err: expected json\.Number value for Float64Exact check, got: bool`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_Bool_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("bool_attribute"), + knownvalue.Bool(false), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for output at path: test_resource_one_output.bool_attribute, err: expected value false for Bool check, got: true"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_Float64(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + float_attribute = 1.23 + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("float_attribute"), + knownvalue.Float64Exact(1.23), + ), + }, + }, + }, + }, + }) +} + +// We do not need equivalent tests for Int64 and Number as they all test the same logic. +func TestExpectKnownOutputValueAtPath_CheckPlan_Float64_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + float_attribute = 1.23 + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("float_attribute"), + knownvalue.StringExact("str"), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: test_resource_one_output.float_attribute, err: expected string value for StringExact check, got: json\.Number`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_Float64_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + float_attribute = 1.23 + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("float_attribute"), + knownvalue.Float64Exact(3.21), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for output at path: test_resource_one_output.float_attribute, err: expected value 3.21 for Float64Exact check, got: 1.23"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_Int64(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("int_attribute"), + knownvalue.Int64Exact(123), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_Int64_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("int_attribute"), + knownvalue.Int64Exact(321), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for output at path: test_resource_one_output.int_attribute, err: expected value 321 for Int64Exact check, got: 123"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_List(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_attribute"), + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("value1"), + knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_List_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_attribute"), + knownvalue.MapExact(map[string]knownvalue.Check{}), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: test_resource_one_output.list_attribute, err: expected map\[string\]any value for MapExact check, got: \[\]interface {}`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_List_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_attribute"), + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("value3"), + knownvalue.StringExact("value4"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: test_resource_one_output.list_attribute, err: list element index 0: expected value value3 for StringExact check, got: value1`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_ListPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_attribute"), + knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.StringExact("value1"), + }), + ), + }, + }, + }, + }, + }) +} + +// No need to check KnownValueWrongType for ListPartial as all lists, and sets are []any in +// tfjson.Plan. +func TestExpectKnownOutputValueAtPath_CheckPlan_ListPartial_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_attribute"), + knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.StringExact("value3"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: test_resource_one_output.list_attribute, err: list element 0: expected value value3 for StringExact check, got: value1`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_ListElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_attribute"), + knownvalue.ListSizeExact(2), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_ListElements_WrongNum(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_attribute"), + knownvalue.ListSizeExact(3), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for output at path: test_resource_one_output.list_attribute, err: expected 3 elements for ListSizeExact check, got 2 elements"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_ListNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_nested_block"), + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("str"), + }), + knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_ListNestedBlockPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_nested_block"), + knownvalue.ListPartial(map[int]knownvalue.Check{ + 1: knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_ListNestedBlockElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_nested_block"), + knownvalue.ListSizeExact(2), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_Map(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("map_attribute"), + knownvalue.MapExact(map[string]knownvalue.Check{ + "key1": knownvalue.StringExact("value1"), + "key2": knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_Map_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("map_attribute"), + knownvalue.ListExact([]knownvalue.Check{}), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: test_resource_one_output.map_attribute, err: expected \[\]any value for ListExact check, got: map\[string\]interface {}`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_Map_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("map_attribute"), + knownvalue.MapExact(map[string]knownvalue.Check{ + "key3": knownvalue.StringExact("value3"), + "key4": knownvalue.StringExact("value4"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: test_resource_one_output.map_attribute, err: missing element key3 for MapExact check`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_MapPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("map_attribute"), + knownvalue.MapPartial(map[string]knownvalue.Check{ + "key1": knownvalue.StringExact("value1"), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_MapPartial_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("map_attribute"), + knownvalue.MapPartial(map[string]knownvalue.Check{ + "key3": knownvalue.StringExact("value1"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: test_resource_one_output.map_attribute, err: missing element key3 for MapPartial check`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_MapElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("map_attribute"), + knownvalue.MapSizeExact(2), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_MapElements_WrongNum(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("map_attribute"), + knownvalue.MapSizeExact(3), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for output at path: test_resource_one_output.map_attribute, err: expected 3 elements for MapSizeExact check, got 2 elements"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_Number(t *testing.T) { + t.Parallel() + + f, _, err := big.ParseFloat("123", 10, 512, big.ToNearestEven) + + if err != nil { + t.Errorf("%s", err) + } + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("int_attribute"), + knownvalue.NumberExact(f), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_Number_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + f, _, err := big.ParseFloat("321", 10, 512, big.ToNearestEven) + + if err != nil { + t.Errorf("%s", err) + } + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("int_attribute"), + knownvalue.NumberExact(f), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for output at path: test_resource_one_output.int_attribute, err: expected value 321 for NumberExact check, got: 123"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_Set(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_attribute"), + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.StringExact("value1"), + knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_Set_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_attribute"), + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.StringExact("value1"), + knownvalue.StringExact("value3"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: test_resource_one_output.set_attribute, err: missing value value3 for SetExact check`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_SetPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_attribute"), + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_SetPartial_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_attribute"), + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.StringExact("value3"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: test_resource_one_output.set_attribute, err: missing value value3 for SetPartial check`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_SetElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_attribute"), + knownvalue.SetSizeExact(2), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_SetNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_nested_block"), + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("str"), + }), + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_SetNestedBlockPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_nested_block"), + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_SetNestedBlockElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_nested_block"), + knownvalue.SetSizeExact(2), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_String(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("string_attribute"), + knownvalue.StringExact("str")), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_String_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("string_attribute"), + knownvalue.Bool(true)), + }, + }, + ExpectError: regexp.MustCompile("error checking value for output at path: test_resource_one_output.string_attribute, err: expected bool value for Bool check, got: string"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_String_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("string_attribute"), + knownvalue.StringExact("rts")), + }, + }, + ExpectError: regexp.MustCompile("error checking value for output at path: test_resource_one_output.string_attribute, err: expected value rts for StringExact check, got: str"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_UnknownAttributeType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + knownValue knownvalue.Check + req plancheck.CheckPlanRequest + expectedErr error + }{ + "unrecognised-type": { + knownValue: knownvalue.Int64Exact(123), + req: plancheck.CheckPlanRequest{ + Plan: &tfjson.Plan{ + OutputChanges: map[string]*tfjson.Change{ + "float32_output": { + After: float32(123), + }, + }, + }, + }, + expectedErr: fmt.Errorf("error checking value for output at path: float32_output., err: expected json.Number value for Int64Exact check, got: float32"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + e := plancheck.ExpectKnownOutputValueAtPath("float32_output", tfjsonpath.Path{}, testCase.knownValue) + + resp := plancheck.CheckPlanResponse{} + + e.CheckPlan(context.Background(), testCase.req, &resp) + + if diff := cmp.Diff(resp.Error, testCase.expectedErr, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/plancheck/expect_known_output_value_test.go b/plancheck/expect_known_output_value_test.go new file mode 100644 index 000000000..2065e20ba --- /dev/null +++ b/plancheck/expect_known_output_value_test.go @@ -0,0 +1,1664 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package plancheck_test + +import ( + "context" + "fmt" + "math/big" + "regexp" + "testing" + + "github.com/google/go-cmp/cmp" + tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" +) + +func TestExpectKnownOutputValue_CheckPlan_OutputNotFound(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + output bool_output { + value = test_resource.one.bool_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "bool_not_found", + knownvalue.Bool(true), + ), + }, + }, + ExpectError: regexp.MustCompile("bool_not_found - Output not found in plan"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_AttributeValueNull(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" {} + output bool_output { + value = test_resource.one.bool_attribute + } + output float_output { + value = test_resource.one.float_attribute + } + output int_output { + value = test_resource.one.int_attribute + } + output list_output { + value = test_resource.one.list_attribute + } + output list_nested_block_output { + value = test_resource.one.list_nested_block + } + output map_output { + value = test_resource.one.map_attribute + } + output set_output { + value = test_resource.one.set_attribute + } + output set_nested_block_output { + value = test_resource.one.set_nested_block + } + output string_output { + value = test_resource.one.string_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "bool_output", + knownvalue.Null(), + ), + plancheck.ExpectKnownOutputValue( + "float_output", + knownvalue.Null(), + ), + plancheck.ExpectKnownOutputValue( + "int_output", + knownvalue.Null(), + ), + plancheck.ExpectKnownOutputValue( + "list_output", + knownvalue.Null(), + ), + plancheck.ExpectKnownOutputValue( + "list_nested_block_output", + knownvalue.ListExact([]knownvalue.Check{}), + ), + plancheck.ExpectKnownOutputValue( + "map_output", + knownvalue.Null(), + ), + plancheck.ExpectKnownOutputValue( + "set_output", + knownvalue.Null(), + ), + plancheck.ExpectKnownOutputValue( + "set_nested_block_output", + knownvalue.SetExact([]knownvalue.Check{}), + ), + plancheck.ExpectKnownOutputValue( + "string_output", + knownvalue.Null(), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_AttributeValueNotNull(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + float_attribute = 1.23 + int_attribute = 123 + list_attribute = ["value1", "value2"] + list_nested_block { + list_nested_block_attribute = "str" + } + map_attribute = { + key1 = "value1" + } + set_attribute = ["value1", "value2"] + set_nested_block { + set_nested_block_attribute = "str" + } + string_attribute = "str" + } + output bool_output { + value = test_resource.one.bool_attribute + } + output float64_output { + value = test_resource.one.float_attribute + } + output int64_output { + value = test_resource.one.int_attribute + } + output list_output { + value = test_resource.one.list_attribute + } + output list_nested_block_output { + value = test_resource.one.list_nested_block + } + output map_output { + value = test_resource.one.map_attribute + } + output set_output { + value = test_resource.one.set_attribute + } + output set_nested_block_output { + value = test_resource.one.set_nested_block + } + output string_output { + value = test_resource.one.string_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "bool_output", + knownvalue.NotNull(), + ), + plancheck.ExpectKnownOutputValue( + "float64_output", + knownvalue.NotNull(), + ), + plancheck.ExpectKnownOutputValue( + "int64_output", + knownvalue.NotNull(), + ), + plancheck.ExpectKnownOutputValue( + "list_output", + knownvalue.NotNull(), + ), + plancheck.ExpectKnownOutputValue( + "list_nested_block_output", + knownvalue.ListSizeExact(1), + ), + plancheck.ExpectKnownOutputValue( + "map_output", + knownvalue.NotNull(), + ), + plancheck.ExpectKnownOutputValue( + "set_output", + knownvalue.NotNull(), + ), + plancheck.ExpectKnownOutputValue( + "set_nested_block_output", + knownvalue.SetSizeExact(1), + ), + plancheck.ExpectKnownOutputValue( + "string_output", + knownvalue.NotNull(), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_Bool(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + output bool_output { + value = test_resource.one.bool_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "bool_output", + knownvalue.Bool(true), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_Bool_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + output bool_output { + value = test_resource.one.bool_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "bool_output", + knownvalue.Float64Exact(1.23), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: bool_output, err: expected json\.Number value for Float64Exact check, got: bool`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_Bool_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + output bool_output { + value = test_resource.one.bool_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "bool_output", + knownvalue.Bool(false), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for output at path: bool_output, err: expected value false for Bool check, got: true"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_Float64(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + float_attribute = 1.23 + } + + output float64_output { + value = test_resource.one.float_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "float64_output", + knownvalue.Float64Exact(1.23), + ), + }, + }, + }, + }, + }) +} + +// We do not need equivalent tests for Int64 and Number as they all test the same logic. +func TestExpectKnownOutputValue_CheckPlan_Float64_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + float_attribute = 1.23 + } + + output float64_output { + value = test_resource.one.float_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "float64_output", + knownvalue.StringExact("str"), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: float64_output, err: expected string value for StringExact check, got: json\.Number`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_Float64_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + float_attribute = 1.23 + } + + output float64_output { + value = test_resource.one.float_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "float64_output", + knownvalue.Float64Exact(3.21), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for output at path: float64_output, err: expected value 3.21 for Float64Exact check, got: 1.23"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_Int64(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + + output int64_output { + value = test_resource.one.int_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "int64_output", + knownvalue.Int64Exact(123), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_Int64_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + + output int64_output { + value = test_resource.one.int_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "int64_output", + knownvalue.Int64Exact(321), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for output at path: int64_output, err: expected value 321 for Int64Exact check, got: 123"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_List(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output list_output { + value = test_resource.one.list_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "list_output", + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("value1"), + knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_List_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output list_output { + value = test_resource.one.list_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "list_output", + knownvalue.MapExact(map[string]knownvalue.Check{}), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: list_output, err: expected map\[string\]any value for MapExact check, got: \[\]interface {}`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_List_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output list_output { + value = test_resource.one.list_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "list_output", + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("value3"), + knownvalue.StringExact("value4"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: list_output, err: list element index 0: expected value value3 for StringExact check, got: value1`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_ListPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output list_output { + value = test_resource.one.list_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "list_output", + knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.StringExact("value1"), + }), + ), + }, + }, + }, + }, + }) +} + +// No need to check KnownValueWrongType for ListPartial as all lists, and sets are []any in +// tfjson.Plan. +func TestExpectKnownOutputValue_CheckPlan_ListPartial_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output list_output { + value = test_resource.one.list_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "list_output", + knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.StringExact("value3"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: list_output, err: list element 0: expected value value3 for StringExact check, got: value1`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_ListElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output list_output { + value = test_resource.one.list_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "list_output", + knownvalue.ListSizeExact(2), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_ListElements_WrongNum(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output list_output { + value = test_resource.one.list_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "list_output", + knownvalue.ListSizeExact(3), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for output at path: list_output, err: expected 3 elements for ListSizeExact check, got 2 elements"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_ListNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + + output list_nested_block_output { + value = test_resource.one.list_nested_block + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "list_nested_block_output", + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("str"), + }), + knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_ListNestedBlockPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + + output list_nested_block_output { + value = test_resource.one.list_nested_block + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "list_nested_block_output", + knownvalue.ListPartial(map[int]knownvalue.Check{ + 1: knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_ListNestedBlockElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + + output list_nested_block_output { + value = test_resource.one.list_nested_block + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "list_nested_block_output", + knownvalue.ListSizeExact(2), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_Map(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output map_output { + value = test_resource.one.map_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "map_output", + knownvalue.MapExact(map[string]knownvalue.Check{ + "key1": knownvalue.StringExact("value1"), + "key2": knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_Map_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output map_output { + value = test_resource.one.map_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "map_output", + knownvalue.ListExact([]knownvalue.Check{}), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: map_output, err: expected \[\]any value for ListExact check, got: map\[string\]interface {}`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_Map_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output map_output { + value = test_resource.one.map_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "map_output", + knownvalue.MapExact(map[string]knownvalue.Check{ + "key3": knownvalue.StringExact("value3"), + "key4": knownvalue.StringExact("value4"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: map_output, err: missing element key3 for MapExact check`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_MapPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output map_output { + value = test_resource.one.map_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "map_output", + knownvalue.MapPartial(map[string]knownvalue.Check{ + "key1": knownvalue.StringExact("value1"), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_MapPartial_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output map_output { + value = test_resource.one.map_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "map_output", + knownvalue.MapPartial(map[string]knownvalue.Check{ + "key3": knownvalue.StringExact("value1"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: map_output, err: missing element key3 for MapPartial check`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_MapElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output map_output { + value = test_resource.one.map_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "map_output", + knownvalue.MapSizeExact(2), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_MapElements_WrongNum(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output map_output { + value = test_resource.one.map_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "map_output", + knownvalue.MapSizeExact(3), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for output at path: map_output, err: expected 3 elements for MapSizeExact check, got 2 elements"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_Number(t *testing.T) { + t.Parallel() + + f, _, err := big.ParseFloat("123", 10, 512, big.ToNearestEven) + + if err != nil { + t.Errorf("%s", err) + } + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + + output int64_output { + value = test_resource.one.int_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "int64_output", + knownvalue.NumberExact(f), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_Number_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + f, _, err := big.ParseFloat("321", 10, 512, big.ToNearestEven) + + if err != nil { + t.Errorf("%s", err) + } + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + + output int64_output { + value = test_resource.one.int_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "int64_output", + knownvalue.NumberExact(f), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for output at path: int64_output, err: expected value 321 for NumberExact check, got: 123"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_Set(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + + output set_output { + value = test_resource.one.set_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "set_output", + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.StringExact("value1"), + knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_Set_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + + output set_output { + value = test_resource.one.set_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "set_output", + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.StringExact("value1"), + knownvalue.StringExact("value3"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: set_output, err: missing value value3 for SetExact check`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_SetPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + + output set_output { + value = test_resource.one.set_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "set_output", + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_SetPartial_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + + output set_output { + value = test_resource.one.set_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "set_output", + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.StringExact("value3"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: set_output, err: missing value value3 for SetPartial check`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_SetElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + + output set_output { + value = test_resource.one.set_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "set_output", + knownvalue.SetSizeExact(2), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_SetNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + + output set_nested_block_output { + value = test_resource.one.set_nested_block + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "set_nested_block_output", + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("str"), + }), + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_SetNestedBlockPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + + output set_nested_block_output { + value = test_resource.one.set_nested_block + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "set_nested_block_output", + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_SetNestedBlockElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + + output set_nested_block_output { + value = test_resource.one.set_nested_block + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "set_nested_block_output", + knownvalue.SetSizeExact(2), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_String(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + output string_output { + value = test_resource.one.string_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "string_output", + knownvalue.StringExact("str")), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_String_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + output string_output { + value = test_resource.one.string_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "string_output", + knownvalue.Bool(true)), + }, + }, + ExpectError: regexp.MustCompile("error checking value for output at path: string_output, err: expected bool value for Bool check, got: string"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_String_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + output string_output { + value = test_resource.one.string_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "string_output", + knownvalue.StringExact("rts")), + }, + }, + ExpectError: regexp.MustCompile("error checking value for output at path: string_output, err: expected value rts for StringExact check, got: str"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_UnknownAttributeType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + knownValue knownvalue.Check + req plancheck.CheckPlanRequest + expectedErr error + }{ + "unrecognised-type": { + knownValue: knownvalue.Int64Exact(123), + req: plancheck.CheckPlanRequest{ + Plan: &tfjson.Plan{ + OutputChanges: map[string]*tfjson.Change{ + "float32_output": { + After: float32(123), + }, + }, + }, + }, + expectedErr: fmt.Errorf("error checking value for output at path: float32_output, err: expected json.Number value for Int64Exact check, got: float32"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + e := plancheck.ExpectKnownOutputValue("float32_output", testCase.knownValue) + + resp := plancheck.CheckPlanResponse{} + + e.CheckPlan(context.Background(), testCase.req, &resp) + + if diff := cmp.Diff(resp.Error, testCase.expectedErr, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/plancheck/expect_known_value.go b/plancheck/expect_known_value.go new file mode 100644 index 000000000..ae6ea6d8d --- /dev/null +++ b/plancheck/expect_known_value.go @@ -0,0 +1,70 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package plancheck + +import ( + "context" + "fmt" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +// Resource Plan Check +var _ PlanCheck = expectKnownValue{} + +type expectKnownValue struct { + resourceAddress string + attributePath tfjsonpath.Path + knownValue knownvalue.Check +} + +// CheckPlan implements the plan check logic. +func (e expectKnownValue) CheckPlan(ctx context.Context, req CheckPlanRequest, resp *CheckPlanResponse) { + var rc *tfjson.ResourceChange + + if req.Plan == nil { + resp.Error = fmt.Errorf("plan is nil") + } + + for _, resourceChange := range req.Plan.ResourceChanges { + if e.resourceAddress == resourceChange.Address { + rc = resourceChange + + break + } + } + + if rc == nil { + resp.Error = fmt.Errorf("%s - Resource not found in plan", e.resourceAddress) + + return + } + + result, err := tfjsonpath.Traverse(rc.Change.After, e.attributePath) + + if err != nil { + resp.Error = err + + return + } + + if err := e.knownValue.CheckValue(result); err != nil { + resp.Error = fmt.Errorf("error checking value for attribute at path: %s.%s, err: %s", e.resourceAddress, e.attributePath.String(), err) + + return + } +} + +// ExpectKnownValue returns a plan check that asserts that the specified attribute at the given resource +// has a known type and value. +func ExpectKnownValue(resourceAddress string, attributePath tfjsonpath.Path, knownValue knownvalue.Check) PlanCheck { + return expectKnownValue{ + resourceAddress: resourceAddress, + attributePath: attributePath, + knownValue: knownValue, + } +} diff --git a/plancheck/expect_known_value_test.go b/plancheck/expect_known_value_test.go new file mode 100644 index 000000000..5256892a9 --- /dev/null +++ b/plancheck/expect_known_value_test.go @@ -0,0 +1,1523 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package plancheck_test + +import ( + "context" + "fmt" + "math/big" + "regexp" + "testing" + + "github.com/google/go-cmp/cmp" + tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestExpectKnownValue_CheckPlan_ResourceNotFound(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.two", + tfjsonpath.New("bool_attribute"), + knownvalue.Bool(true), + ), + }, + }, + ExpectError: regexp.MustCompile("test_resource.two - Resource not found in plan"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_AttributeValueNull(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" {}`, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + knownvalue.Null(), + ), + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("float_attribute"), + knownvalue.Null(), + ), + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("int_attribute"), + knownvalue.Null(), + ), + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.Null(), + ), + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_nested_block"), + knownvalue.ListExact([]knownvalue.Check{}), + ), + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.Null(), + ), + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_attribute"), + knownvalue.Null(), + ), + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_nested_block"), + knownvalue.SetExact([]knownvalue.Check{}), + ), + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("string_attribute"), + knownvalue.Null(), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_AttributeValueNotNull(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + float_attribute = 1.23 + int_attribute = 123 + list_attribute = ["value1", "value2"] + list_nested_block { + list_nested_block_attribute = "str" + } + map_attribute = { + key1 = "value1" + } + set_attribute = ["value1", "value2"] + set_nested_block { + set_nested_block_attribute = "str" + } + string_attribute = "str" + }`, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + knownvalue.NotNull(), + ), + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("float_attribute"), + knownvalue.NotNull(), + ), + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("int_attribute"), + knownvalue.NotNull(), + ), + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.NotNull(), + ), + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_nested_block"), + knownvalue.ListSizeExact(1), + ), + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.NotNull(), + ), + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_attribute"), + knownvalue.NotNull(), + ), + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_nested_block"), + knownvalue.SetSizeExact(1), + ), + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("string_attribute"), + knownvalue.NotNull(), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_Bool(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + knownvalue.Bool(true), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_Bool_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + knownvalue.Float64Exact(1.23), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for attribute at path: test_resource.one.bool_attribute, err: expected json\.Number value for Float64Exact check, got: bool`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_Bool_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + knownvalue.Bool(false), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for attribute at path: test_resource.one.bool_attribute, err: expected value false for Bool check, got: true"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_Float64(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + float_attribute = 1.23 + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("float_attribute"), + knownvalue.Float64Exact(1.23), + ), + }, + }, + }, + }, + }) +} + +// We do not need equivalent tests for Int64 and Number as they all test the same logic. +func TestExpectKnownValue_CheckPlan_Float64_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + float_attribute = 1.23 + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("float_attribute"), + knownvalue.StringExact("str"), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for attribute at path: test_resource.one.float_attribute, err: expected string value for StringExact check, got: json\.Number`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_Float64_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + float_attribute = 1.23 + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("float_attribute"), + knownvalue.Float64Exact(3.21), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for attribute at path: test_resource.one.float_attribute, err: expected value 3.21 for Float64Exact check, got: 1.23"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_Int64(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("int_attribute"), + knownvalue.Int64Exact(123), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_Int64_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("int_attribute"), + knownvalue.Int64Exact(321), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for attribute at path: test_resource.one.int_attribute, err: expected value 321 for Int64Exact check, got: 123"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_List(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("value1"), + knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_List_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.MapExact(map[string]knownvalue.Check{}), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for attribute at path: test_resource.one.list_attribute, err: expected map\[string\]any value for MapExact check, got: \[\]interface {}`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_List_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("value3"), + knownvalue.StringExact("value4"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for attribute at path: test_resource.one.list_attribute, err: list element index 0: expected value value3 for StringExact check, got: value1`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_ListPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.StringExact("value1"), + }), + ), + }, + }, + }, + }, + }) +} + +// No need to check KnownValueWrongType for ListPartial as all lists, and sets are []any in +// tfjson.Plan. +func TestExpectKnownValue_CheckPlan_ListPartial_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.StringExact("value3"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for attribute at path: test_resource.one.list_attribute, err: list element 0: expected value value3 for StringExact check, got: value1`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_ListElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.ListSizeExact(2), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_ListElements_WrongNum(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.ListSizeExact(3), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for attribute at path: test_resource.one.list_attribute, err: expected 3 elements for ListSizeExact check, got 2 elements"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_ListNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_nested_block"), + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("str"), + }), + knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_ListNestedBlockPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_nested_block"), + knownvalue.ListPartial(map[int]knownvalue.Check{ + 1: knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_ListNestedBlockElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_nested_block"), + knownvalue.ListSizeExact(2), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_Map(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.MapExact(map[string]knownvalue.Check{ + "key1": knownvalue.StringExact("value1"), + "key2": knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_Map_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.ListExact([]knownvalue.Check{}), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for attribute at path: test_resource.one.map_attribute, err: expected \[\]any value for ListExact check, got: map\[string\]interface {}`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_Map_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.MapExact(map[string]knownvalue.Check{ + "key3": knownvalue.StringExact("value3"), + "key4": knownvalue.StringExact("value4"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for attribute at path: test_resource.one.map_attribute, err: missing element key3 for MapExact check`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_MapPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.MapPartial(map[string]knownvalue.Check{ + "key1": knownvalue.StringExact("value1"), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_MapPartial_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.MapPartial(map[string]knownvalue.Check{ + "key3": knownvalue.StringExact("value1"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for attribute at path: test_resource.one.map_attribute, err: missing element key3 for MapPartial check`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_MapElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.MapSizeExact(2), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_MapElements_WrongNum(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.MapSizeExact(3), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for attribute at path: test_resource.one.map_attribute, err: expected 3 elements for MapSizeExact check, got 2 elements"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_Number(t *testing.T) { + t.Parallel() + + f, _, err := big.ParseFloat("123", 10, 512, big.ToNearestEven) + + if err != nil { + t.Errorf("%s", err) + } + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("int_attribute"), + knownvalue.NumberExact(f), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_Number_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + f, _, err := big.ParseFloat("321", 10, 512, big.ToNearestEven) + + if err != nil { + t.Errorf("%s", err) + } + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("int_attribute"), + knownvalue.NumberExact(f), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for attribute at path: test_resource.one.int_attribute, err: expected value 321 for NumberExact check, got: 123"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_Set(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_attribute"), + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.StringExact("value2"), + knownvalue.StringExact("value1"), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_Set_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_attribute"), + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.StringExact("value1"), + knownvalue.StringExact("value3"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for attribute at path: test_resource.one.set_attribute, err: missing value value3 for SetExact check`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_SetPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_attribute"), + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_SetPartial_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_attribute"), + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.StringExact("value3"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for attribute at path: test_resource.one.set_attribute, err: missing value value3 for SetPartial check`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_SetElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_attribute"), + knownvalue.SetSizeExact(2), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_SetNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_nested_block"), + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("str"), + }), + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_SetNestedBlockPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_nested_block"), + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_SetNestedBlockElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_nested_block"), + knownvalue.SetSizeExact(2), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_String(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("string_attribute"), + knownvalue.StringExact("str")), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_String_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("string_attribute"), + knownvalue.Bool(true)), + }, + }, + ExpectError: regexp.MustCompile("error checking value for attribute at path: test_resource.one.string_attribute, err: expected bool value for Bool check, got: string"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_String_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("string_attribute"), + knownvalue.StringExact("rts")), + }, + }, + ExpectError: regexp.MustCompile("error checking value for attribute at path: test_resource.one.string_attribute, err: expected value rts for StringExact check, got: str"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_UnknownAttributeType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + knownValue knownvalue.Check + req plancheck.CheckPlanRequest + expectedErr error + }{ + "unrecognised-type": { + knownValue: knownvalue.Int64Exact(123), + req: plancheck.CheckPlanRequest{ + Plan: &tfjson.Plan{ + ResourceChanges: []*tfjson.ResourceChange{ + { + Address: "example_resource.test", + Change: &tfjson.Change{ + After: map[string]any{ + "attribute": float32(123), + }, + }, + }, + }, + }, + }, + expectedErr: fmt.Errorf("error checking value for attribute at path: example_resource.test.attribute, err: expected json.Number value for Int64Exact check, got: float32"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + e := plancheck.ExpectKnownValue("example_resource.test", tfjsonpath.New("attribute"), testCase.knownValue) + + resp := plancheck.CheckPlanResponse{} + + e.CheckPlan(context.Background(), testCase.req, &resp) + + if diff := cmp.Diff(resp.Error, testCase.expectedErr, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +var equateErrorMessage = cmp.Comparer(func(x, y error) bool { + if x == nil || y == nil { + return x == nil && y == nil + } + + return x.Error() == y.Error() +}) diff --git a/plancheck/expect_no_deferred_changes.go b/plancheck/expect_no_deferred_changes.go new file mode 100644 index 000000000..726ea6802 --- /dev/null +++ b/plancheck/expect_no_deferred_changes.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package plancheck + +import ( + "context" + "errors" + "fmt" +) + +var _ PlanCheck = expectNoDeferredChanges{} + +type expectNoDeferredChanges struct{} + +// CheckPlan implements the plan check logic. +func (e expectNoDeferredChanges) CheckPlan(ctx context.Context, req CheckPlanRequest, resp *CheckPlanResponse) { + if len(req.Plan.DeferredChanges) == 0 { + return + } + + var result []error + for _, deferred := range req.Plan.DeferredChanges { + resourceAddress := "unknown" + if deferred.ResourceChange != nil { + resourceAddress = deferred.ResourceChange.Address + } + + result = append(result, fmt.Errorf("expected no deferred changes, but resource %q is deferred with reason: %q", resourceAddress, deferred.Reason)) + } + + resp.Error = errors.Join(result...) + if resp.Error != nil { + return + } + + if req.Plan.Complete == nil { + resp.Error = errors.New("expected plan to be marked as complete, but complete field was not set in plan (nil). This indicates that the plan was created with a version of Terraform older than 1.8, which does not support the complete field.") + return + } + + if !*req.Plan.Complete { + resp.Error = errors.New("expected plan to be marked as complete, but complete was \"false\", indicating that at least one more plan/apply round is needed to converge.") + return + } +} + +// ExpectNoDeferredChanges returns a plan check that asserts that there are no deferred changes +// for any resources in the plan. +func ExpectNoDeferredChanges() PlanCheck { + return expectNoDeferredChanges{} +} diff --git a/plancheck/expect_no_deferred_changes_test.go b/plancheck/expect_no_deferred_changes_test.go new file mode 100644 index 000000000..5c047d0c7 --- /dev/null +++ b/plancheck/expect_no_deferred_changes_test.go @@ -0,0 +1,198 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package plancheck_test + +import ( + "context" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func Test_ExpectNoDeferredChange(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_9_0), + tfversion.SkipIfNotAlpha(), + }, + AdditionalCLIOptions: &r.AdditionalCLIOptions{ + Plan: r.PlanOptions{AllowDeferral: true}, + Apply: r.ApplyOptions{AllowDeferral: true}, + }, + 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: "id", + Type: tftypes.String, + Required: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" { + id = "hello" + }`, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNoDeferredChanges(), + }, + }, + }, + }, + }) +} + +func Test_ExpectNoDeferredChange_OneDeferral(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_9_0), + tfversion.SkipIfNotAlpha(), + }, + AdditionalCLIOptions: &r.AdditionalCLIOptions{ + Plan: r.PlanOptions{AllowDeferral: true}, + Apply: r.ApplyOptions{AllowDeferral: true}, + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + PlanChangeFunc: func(ctx context.Context, req resource.PlanChangeRequest, resp *resource.PlanChangeResponse) { + resp.Deferred = &tfprotov6.Deferred{ + Reason: tfprotov6.DeferredReasonResourceConfigUnknown, + } + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" {}`, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNoDeferredChanges(), + }, + }, + ExpectError: regexp.MustCompile(`expected no deferred changes, but resource "test_resource.test" is deferred with reason: "resource_config_unknown"`), + }, + }, + }) +} + +func Test_ExpectNoDeferredChange_MultipleDeferrals(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_9_0), + tfversion.SkipIfNotAlpha(), + }, + AdditionalCLIOptions: &r.AdditionalCLIOptions{ + Plan: r.PlanOptions{AllowDeferral: true}, + Apply: r.ApplyOptions{AllowDeferral: true}, + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource_one": { + PlanChangeFunc: func(ctx context.Context, req resource.PlanChangeRequest, resp *resource.PlanChangeResponse) { + resp.Deferred = &tfprotov6.Deferred{ + Reason: tfprotov6.DeferredReasonResourceConfigUnknown, + } + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + "test_resource_two": { + PlanChangeFunc: func(ctx context.Context, req resource.PlanChangeRequest, resp *resource.PlanChangeResponse) { + resp.Deferred = &tfprotov6.Deferred{ + Reason: tfprotov6.DeferredReasonAbsentPrereq, + } + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource_one" "test" {} + + resource "test_resource_two" "test" {} + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNoDeferredChanges(), + }, + }, + ExpectError: regexp.MustCompile( + `expected no deferred changes, but resource "test_resource_one.test" is deferred with reason: "resource_config_unknown"\n` + + `expected no deferred changes, but resource "test_resource_two.test" is deferred with reason: "absent_prereq"`, + ), + }, + }, + }) +} diff --git a/plancheck/expect_non_empty_plan.go b/plancheck/expect_non_empty_plan.go index 74acf034d..482321ece 100644 --- a/plancheck/expect_non_empty_plan.go +++ b/plancheck/expect_non_empty_plan.go @@ -14,6 +14,12 @@ type expectNonEmptyPlan struct{} // CheckPlan implements the plan check logic. func (e expectNonEmptyPlan) CheckPlan(ctx context.Context, req CheckPlanRequest, resp *CheckPlanResponse) { + for _, change := range req.Plan.OutputChanges { + if !change.Actions.NoOp() { + return + } + } + for _, rc := range req.Plan.ResourceChanges { if !rc.Change.Actions.NoOp() { return @@ -23,7 +29,7 @@ func (e expectNonEmptyPlan) CheckPlan(ctx context.Context, req CheckPlanRequest, resp.Error = errors.New("expected a non-empty plan, but got an empty plan") } -// ExpectNonEmptyPlan returns a plan check that asserts there is at least one resource change in the plan. +// ExpectNonEmptyPlan returns a plan check that asserts there is at least one output or resource change in the plan. func ExpectNonEmptyPlan() PlanCheck { return expectNonEmptyPlan{} } diff --git a/plancheck/expect_non_empty_plan_test.go b/plancheck/expect_non_empty_plan_test.go index 9c6afae4b..ffe919ce3 100644 --- a/plancheck/expect_non_empty_plan_test.go +++ b/plancheck/expect_non_empty_plan_test.go @@ -9,26 +9,83 @@ import ( r "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/tfversion" ) -func Test_ExpectNonEmptyPlan(t *testing.T) { +func Test_ExpectNonEmptyPlan_OutputChanges(t *testing.T) { t.Parallel() - r.Test(t, r.TestCase{ + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version0_14_0), // outputs before 0.14 always show as created + }, + // Avoid our own validation that requires at least one provider config. ExternalProviders: map[string]r.ExternalProvider{ - "random": { - Source: "registry.terraform.io/hashicorp/random", + "terraform": {Source: "terraform.io/builtin/terraform"}, + }, + Steps: []r.TestStep{ + { + Config: `output "test" { value = "original" }`, + }, + { + Config: `output "test" { value = "new" }`, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNonEmptyPlan(), + }, + }, }, }, + }) +} + +func Test_ExpectNonEmptyPlan_OutputChanges_Error(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version0_14_0), // outputs before 0.14 always show as created + }, + // Avoid our own validation that requires at least one provider config. + ExternalProviders: map[string]r.ExternalProvider{ + "terraform": {Source: "terraform.io/builtin/terraform"}, + }, Steps: []r.TestStep{ { - Config: `resource "random_string" "one" { - length = 16 + Config: `output "test" { value = "original" }`, + }, + { + Config: `output "test" { value = "original" }`, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNonEmptyPlan(), + }, + }, + ExpectError: regexp.MustCompile(`expected a non-empty plan, but got an empty plan`), + }, + }, + }) +} + +func Test_ExpectNonEmptyPlan_ResourceChanges(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_4_0), + }, + ExternalProviders: map[string]r.ExternalProvider{ + "terraform": {Source: "terraform.io/builtin/terraform"}, + }, + Steps: []r.TestStep{ + { + Config: `resource "terraform_data" "one" { + triggers_replace = ["original"] }`, }, { - Config: `resource "random_string" "one" { - length = 12 + Config: `resource "terraform_data" "one" { + triggers_replace = ["new"] }`, ConfigPlanChecks: r.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ @@ -40,24 +97,25 @@ func Test_ExpectNonEmptyPlan(t *testing.T) { }) } -func Test_ExpectNonEmptyPlan_Error(t *testing.T) { +func Test_ExpectNonEmptyPlan_ResourceChanges_Error(t *testing.T) { t.Parallel() - r.Test(t, r.TestCase{ + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_4_0), + }, ExternalProviders: map[string]r.ExternalProvider{ - "random": { - Source: "registry.terraform.io/hashicorp/random", - }, + "terraform": {Source: "terraform.io/builtin/terraform"}, }, Steps: []r.TestStep{ { - Config: `resource "random_string" "one" { - length = 16 + Config: `resource "terraform_data" "one" { + triggers_replace = ["original"] }`, }, { - Config: `resource "random_string" "one" { - length = 16 + Config: `resource "terraform_data" "one" { + triggers_replace = ["original"] }`, ConfigPlanChecks: r.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ diff --git a/plancheck/expect_null_output_value.go b/plancheck/expect_null_output_value.go new file mode 100644 index 000000000..92fbf89dd --- /dev/null +++ b/plancheck/expect_null_output_value.go @@ -0,0 +1,74 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package plancheck + +import ( + "context" + "fmt" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +var _ PlanCheck = expectNullOutputValue{} + +type expectNullOutputValue struct { + outputAddress string +} + +// CheckPlan implements the plan check logic. +func (e expectNullOutputValue) CheckPlan(ctx context.Context, req CheckPlanRequest, resp *CheckPlanResponse) { + var change *tfjson.Change + + for address, oc := range req.Plan.OutputChanges { + if e.outputAddress == address { + change = oc + + break + } + } + + if change == nil { + resp.Error = fmt.Errorf("%s - Output not found in plan OutputChanges", e.outputAddress) + + return + } + + var result any + var err error + + switch { + case change.Actions.Create(): + result, err = tfjsonpath.Traverse(change.After, tfjsonpath.Path{}) + default: + result, err = tfjsonpath.Traverse(change.Before, tfjsonpath.Path{}) + } + + if err != nil { + resp.Error = err + + return + } + + if result != nil { + resp.Error = fmt.Errorf("attribute at path is not null") + + return + } +} + +// ExpectNullOutputValue returns a plan check that asserts that the specified output has a null value. +// +// Due to implementation differences between the terraform-plugin-sdk and the terraform-plugin-framework, representation of null +// values may differ. For example, terraform-plugin-sdk based providers may have less precise representations of null values, such +// as marking whole maps as null rather than individual element values. +// +// Deprecated: Use [plancheck.ExpectKnownOutputValue] with [knownvalue.Null] instead. +// ExpectNullOutputValue will be removed in the next major version release. +func ExpectNullOutputValue(outputAddress string) PlanCheck { + return expectNullOutputValue{ + outputAddress: outputAddress, + } +} diff --git a/plancheck/expect_null_output_value_at_path.go b/plancheck/expect_null_output_value_at_path.go new file mode 100644 index 000000000..69f237d5f --- /dev/null +++ b/plancheck/expect_null_output_value_at_path.go @@ -0,0 +1,76 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package plancheck + +import ( + "context" + "fmt" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +var _ PlanCheck = expectNullOutputValueAtPath{} + +type expectNullOutputValueAtPath struct { + outputAddress string + valuePath tfjsonpath.Path +} + +// CheckPlan implements the plan check logic. +func (e expectNullOutputValueAtPath) CheckPlan(ctx context.Context, req CheckPlanRequest, resp *CheckPlanResponse) { + var change *tfjson.Change + + for address, oc := range req.Plan.OutputChanges { + if e.outputAddress == address { + change = oc + + break + } + } + + if change == nil { + resp.Error = fmt.Errorf("%s - Output not found in plan OutputChanges", e.outputAddress) + + return + } + + var result any + var err error + + switch { + case change.Actions.Create(): + result, err = tfjsonpath.Traverse(change.After, e.valuePath) + default: + result, err = tfjsonpath.Traverse(change.Before, e.valuePath) + } + + if err != nil { + resp.Error = err + + return + } + + if result != nil { + resp.Error = fmt.Errorf("attribute at path is not null") + + return + } +} + +// ExpectNullOutputValueAtPath returns a plan check that asserts that the specified output has a null value. +// +// Due to implementation differences between the terraform-plugin-sdk and the terraform-plugin-framework, representation of null +// values may differ. For example, terraform-plugin-sdk based providers may have less precise representations of null values, such +// as marking whole maps as null rather than individual element values. +// +// Deprecated: Use [plancheck.ExpectKnownOutputValueAtPath] with [knownvalue.Null] instead. +// ExpectNullOutputValueAtPath will be removed in the next major version release. +func ExpectNullOutputValueAtPath(outputAddress string, valuePath tfjsonpath.Path) PlanCheck { + return expectNullOutputValueAtPath{ + outputAddress: outputAddress, + valuePath: valuePath, + } +} diff --git a/plancheck/expect_null_output_value_at_path_test.go b/plancheck/expect_null_output_value_at_path_test.go new file mode 100644 index 000000000..6d183b626 --- /dev/null +++ b/plancheck/expect_null_output_value_at_path_test.go @@ -0,0 +1,643 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package plancheck_test + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func Test_ExpectNullOutputValueAtPath_StringAttribute_EmptyConfig(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Config: `resource "test_resource" "test" { + } + + output "resource" { + value = test_resource.test + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNullOutputValueAtPath("resource", tfjsonpath.New("string_attribute")), + }, + }, + }, + }, + }) +} + +func Test_ExpectNullOutputValueAtPath_StringAttribute_NullConfig(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Config: `resource "test_resource" "test" { + string_attribute = null + } + + output "resource" { + value = test_resource.test + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNullOutputValueAtPath("resource", tfjsonpath.New("string_attribute")), + }, + }, + }, + }, + }) +} + +func Test_ExpectNullOutputValueAtPath_StringAttribute_ExpectErrorNotNull(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Config: `resource "test_resource" "test" { + string_attribute = "str" + } + + output "resource" { + value = test_resource.test + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNullOutputValueAtPath("resource", tfjsonpath.New("string_attribute")), + }, + }, + ExpectError: regexp.MustCompile(`attribute at path is not null`), + }, + }, + }) +} + +func Test_ExpectNullOutputValueAtPath_ListAttribute_EmptyConfig(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" { + } + + output "resource" { + value = test_resource.test + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNullOutputValueAtPath("resource", tfjsonpath.New("list_attribute")), + }, + }, + }, + }, + }) +} + +func Test_ExpectNullOutputValueAtPath_ListAttribute_NullConfig(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" { + list_attribute = null + } + + output "resource" { + value = test_resource.test + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNullOutputValueAtPath("resource", tfjsonpath.New("list_attribute")), + }, + }, + }, + }, + }) +} + +func Test_ExpectNullOutputValueAtPath_ListAttribute_ExpectErrorNotNull(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" { + list_attribute = ["one", "two"] + } + + output "resource" { + value = test_resource.test + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNullOutputValueAtPath("resource", tfjsonpath.New("list_attribute")), + }, + }, + ExpectError: regexp.MustCompile(`attribute at path is not null`), + }, + }, + }) +} + +func Test_ExpectNullOutputValueAtPath_SetAttribute_EmptyConfig(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" { + } + + output "resource" { + value = test_resource.test + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNullOutputValueAtPath("resource", tfjsonpath.New("set_attribute")), + }, + }, + }, + }, + }) +} + +func Test_ExpectNullOutputValueAtPath_SetAttribute_NullConfig(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" { + set_attribute = null + } + + output "resource" { + value = test_resource.test + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNullOutputValueAtPath("resource", tfjsonpath.New("set_attribute")), + }, + }, + }, + }, + }) +} + +func Test_ExpectNullOutputValueAtPath_SetAttribute_ExpectErrorNotNull(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" { + set_attribute = ["one", "two"] + } + + output "resource" { + value = test_resource.test + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNullOutputValueAtPath("resource", tfjsonpath.New("set_attribute")), + }, + }, + ExpectError: regexp.MustCompile(`attribute at path is not null`), + }, + }, + }) +} + +func Test_ExpectNullOutputValueAtPath_MapAttribute_EmptyConfig(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" { + } + + output "resource" { + value = test_resource.test + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNullOutputValueAtPath("resource", tfjsonpath.New("map_attribute")), + }, + }, + }, + }, + }) +} + +func Test_ExpectNullOutputValueAtPath_MapAttribute_NullConfig(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" { + map_attribute = null + } + + output "resource" { + value = test_resource.test + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNullOutputValueAtPath("resource", tfjsonpath.New("map_attribute")), + }, + }, + }, + }, + }) +} + +func Test_ExpectNullOutputValueAtPath_MapAttribute_ExpectErrorNotNull(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" { + map_attribute = { + "one": "str", + "two": "str" + } + } + + output "resource" { + value = test_resource.test + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNullOutputValueAtPath("resource", tfjsonpath.New("map_attribute")), + }, + }, + ExpectError: regexp.MustCompile(`attribute at path is not null`), + }, + }, + }) +} + +func Test_ExpectNullOutputValueAtPath_MapAttribute_PartiallyNullConfig_ExpectError(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" { + map_attribute = { + key1 = "value1", + key2 = null + } + } + + output "resource" { + value = test_resource.test + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNullOutputValueAtPath("resource", tfjsonpath.New("map_attribute").AtMapKey("key2")), + }, + }, + ExpectError: regexp.MustCompile(`path not found: specified key key2 not found in map at map_attribute.key2`), + }, + }, + }) +} + +func Test_ExpectNullOutputValueAtPath_ListNestedBlock_EmptyConfig(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" { + list_nested_block {} + } + + output "resource" { + value = test_resource.test + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNullOutputValueAtPath("resource", tfjsonpath.New("list_nested_block").AtSliceIndex(0).AtMapKey("list_nested_block_attribute")), + }, + }, + }, + }, + }) +} + +func Test_ExpectNullOutputValueAtPath_ListNestedBlock_NullConfig(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" { + list_nested_block { + list_nested_block_attribute = null + } + } + + output "resource" { + value = test_resource.test + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNullOutputValueAtPath("resource", tfjsonpath.New("list_nested_block").AtSliceIndex(0).AtMapKey("list_nested_block_attribute")), + }, + }, + }, + }, + }) +} + +func Test_ExpectNullOutputValueAtPath_ListNestedBlock_ExpectErrorNotNull(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" { + list_nested_block { + list_nested_block_attribute = "str" + } + } + + output "resource" { + value = test_resource.test + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNullOutputValueAtPath("resource", tfjsonpath.New("list_nested_block").AtSliceIndex(0).AtMapKey("list_nested_block_attribute")), + }, + }, + ExpectError: regexp.MustCompile(`attribute at path is not null`), + }, + }, + }) +} + +func Test_ExpectNullOutputValueAtPath_SetNestedBlock_NullConfig_ExpectErrorNotNull(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" { + set_nested_block { + set_nested_block_attribute = null + } + } + + output "resource" { + value = test_resource.test + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNullOutputValueAtPath("resource", tfjsonpath.New("set_nested_block")), + }, + }, + ExpectError: regexp.MustCompile(`attribute at path is not null`), + }, + }, + }) +} diff --git a/plancheck/expect_null_output_value_test.go b/plancheck/expect_null_output_value_test.go new file mode 100644 index 000000000..b4e85dcbf --- /dev/null +++ b/plancheck/expect_null_output_value_test.go @@ -0,0 +1,522 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package plancheck_test + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" +) + +func Test_ExpectNullOutputValue_StringAttribute_EmptyConfig(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + Steps: []r.TestStep{ + { + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Config: `resource "test_resource" "test" { + } + + output "string_attribute" { + value = test_resource.test.string_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNullOutputValue("string_attribute"), + }, + }, + }, + }, + }) +} + +func Test_ExpectNullOutputValue_StringAttribute_NullConfig(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + Steps: []r.TestStep{ + { + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Config: `resource "test_resource" "test" { + string_attribute = null + } + + output "string_attribute" { + value = test_resource.test.string_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNullOutputValue("string_attribute"), + }, + }, + }, + }, + }) +} + +func Test_ExpectNullOutputValue_StringAttribute_ExpectErrorNotNull(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + Steps: []r.TestStep{ + { + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Config: `resource "test_resource" "test" { + string_attribute = "str" + } + + output "string_attribute" { + value = test_resource.test.string_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNullOutputValue("string_attribute"), + }, + }, + ExpectError: regexp.MustCompile(`attribute at path is not null`), + }, + }, + }) +} + +func Test_ExpectNullOutputValue_ListAttribute_EmptyConfig(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" { + } + + output "list_attribute" { + value = test_resource.test.list_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNullOutputValue("list_attribute"), + }, + }, + }, + }, + }) +} + +func Test_ExpectNullOutputValue_ListAttribute_NullConfig(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" { + list_attribute = null + } + + output "list_attribute" { + value = test_resource.test.list_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNullOutputValue("list_attribute"), + }, + }, + }, + }, + }) +} + +func Test_ExpectNullOutputValue_ListAttribute_ExpectErrorNotNull(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" { + list_attribute = ["one", "two"] + } + + output "list_attribute" { + value = test_resource.test.list_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNullOutputValue("list_attribute"), + }, + }, + ExpectError: regexp.MustCompile(`attribute at path is not null`), + }, + }, + }) +} + +func Test_ExpectNullOutputValue_SetAttribute_EmptyConfig(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" { + } + + output "set_attribute" { + value = test_resource.test.set_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNullOutputValue("set_attribute"), + }, + }, + }, + }, + }) +} + +func Test_ExpectNullOutputValue_SetAttribute_NullConfig(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" { + set_attribute = null + } + + output "set_attribute" { + value = test_resource.test.set_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNullOutputValue("set_attribute"), + }, + }, + }, + }, + }) +} + +func Test_ExpectNullOutputValue_SetAttribute_ExpectErrorNotNull(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" { + set_attribute = ["one", "two"] + } + + output "set_attribute" { + value = test_resource.test.set_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNullOutputValue("set_attribute"), + }, + }, + ExpectError: regexp.MustCompile(`attribute at path is not null`), + }, + }, + }) +} + +func Test_ExpectNullOutputValue_MapAttribute_EmptyConfig(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" { + } + + output "map_attribute" { + value = test_resource.test.map_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNullOutputValue("map_attribute"), + }, + }, + }, + }, + }) +} + +func Test_ExpectNullOutputValue_MapAttribute_NullConfig(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" { + map_attribute = null + } + + output "map_attribute" { + value = test_resource.test.map_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNullOutputValue("map_attribute"), + }, + }, + }, + }, + }) +} + +func Test_ExpectNullOutputValue_MapAttribute_ExpectErrorNotNull(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" { + map_attribute = { + "one": "str", + "two": "str" + } + } + + output "map_attribute" { + value = test_resource.test.map_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNullOutputValue("map_attribute"), + }, + }, + ExpectError: regexp.MustCompile(`attribute at path is not null`), + }, + }, + }) +} + +func Test_ExpectNullOutputValue_MapAttribute_PartiallyNullConfig_ExpectError(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" { + map_attribute = { + key1 = "value1", + key2 = null + } + } + + output "map_attribute" { + value = test_resource.test.map_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNullOutputValue("key2"), + }, + }, + ExpectError: regexp.MustCompile(`key2 - Output not found in plan OutputChanges`), + }, + }, + }) +} + +func Test_ExpectNullOutputValue_ListNestedBlock_EmptyConfig(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" { + list_nested_block {} + } + + output "list_nested_block_attribute" { + value = test_resource.test.list_nested_block.0.list_nested_block_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNullOutputValue("list_nested_block_attribute"), + }, + }, + }, + }, + }) +} + +func Test_ExpectNullOutputValue_ListNestedBlock_NullConfig(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" { + list_nested_block { + list_nested_block_attribute = null + } + } + + output "list_nested_block_attribute" { + value = test_resource.test.list_nested_block.0.list_nested_block_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNullOutputValue("list_nested_block_attribute"), + }, + }, + }, + }, + }) +} + +func Test_ExpectNullOutputValue_ListNestedBlock_ExpectErrorNotNull(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" { + list_nested_block { + list_nested_block_attribute = "str" + } + } + + output "list_nested_block_attribute" { + value = test_resource.test.list_nested_block.0.list_nested_block_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNullOutputValue("list_nested_block_attribute"), + }, + }, + ExpectError: regexp.MustCompile(`attribute at path is not null`), + }, + }, + }) +} + +func Test_ExpectNullOutputValue_SetNestedBlock_NullConfig_ExpectErrorNotNull(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" { + set_nested_block { + set_nested_block_attribute = null + } + } + + output "set_nested_block" { + value = test_resource.test.set_nested_block + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNullOutputValue("set_nested_block"), + }, + }, + ExpectError: regexp.MustCompile(`attribute at path is not null`), + }, + }, + }) +} diff --git a/plancheck/expect_sensitive_value.go b/plancheck/expect_sensitive_value.go new file mode 100644 index 000000000..b6c3a5194 --- /dev/null +++ b/plancheck/expect_sensitive_value.go @@ -0,0 +1,61 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package plancheck + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +var _ PlanCheck = expectSensitiveValue{} + +type expectSensitiveValue struct { + resourceAddress string + attributePath tfjsonpath.Path +} + +// CheckPlan implements the plan check logic. +func (e expectSensitiveValue) CheckPlan(ctx context.Context, req CheckPlanRequest, resp *CheckPlanResponse) { + + for _, rc := range req.Plan.ResourceChanges { + if e.resourceAddress != rc.Address { + continue + } + + result, err := tfjsonpath.Traverse(rc.Change.AfterSensitive, e.attributePath) + if err != nil { + resp.Error = err + return + } + + isSensitive, ok := result.(bool) + if !ok { + resp.Error = fmt.Errorf("invalid path: the path value cannot be asserted as bool") + return + } + + if !isSensitive { + resp.Error = fmt.Errorf("attribute at path is not sensitive") + return + } + + return + } + + resp.Error = fmt.Errorf("%s - Resource not found in plan ResourceChanges", e.resourceAddress) +} + +// ExpectSensitiveValue returns a plan check that asserts that the specified attribute at the given resource has a sensitive value. +// +// Due to implementation differences between the terraform-plugin-sdk and the terraform-plugin-framework, representation of sensitive +// values may differ. For example, terraform-plugin-sdk based providers may have less precise representations of sensitive values, such +// as marking whole maps as sensitive rather than individual element values. +func ExpectSensitiveValue(resourceAddress string, attributePath tfjsonpath.Path) PlanCheck { + return expectSensitiveValue{ + resourceAddress: resourceAddress, + attributePath: attributePath, + } +} diff --git a/plancheck/expect_sensitive_value_test.go b/plancheck/expect_sensitive_value_test.go new file mode 100644 index 000000000..1976d5b89 --- /dev/null +++ b/plancheck/expect_sensitive_value_test.go @@ -0,0 +1,322 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package plancheck_test + +import ( + "context" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func Test_ExpectSensitiveValue_SensitiveStringAttribute(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // Change.AfterSensitive + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProviderSensitive(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "one" { + sensitive_string_attribute = "test" + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectSensitiveValue("test_resource.one", + tfjsonpath.New("sensitive_string_attribute")), + }, + }, + }, + }, + }) +} + +func Test_ExpectSensitiveValue_SensitiveListAttribute(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // Change.AfterSensitive + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProviderSensitive(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "one" { + sensitive_list_attribute = ["value1"] + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectSensitiveValue("test_resource.one", + tfjsonpath.New("sensitive_list_attribute")), + }, + }, + }, + }, + }) +} + +func Test_ExpectSensitiveValue_SensitiveSetAttribute(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // Change.AfterSensitive + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProviderSensitive(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "one" { + sensitive_set_attribute = ["value1"] + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectSensitiveValue("test_resource.one", + tfjsonpath.New("sensitive_set_attribute")), + }, + }, + }, + }, + }) +} + +func Test_ExpectSensitiveValue_SensitiveMapAttribute(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // Change.AfterSensitive + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProviderSensitive(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "one" { + sensitive_map_attribute = { + key1 = "value1", + } + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectSensitiveValue("test_resource.one", + tfjsonpath.New("sensitive_map_attribute")), + }, + }, + }, + }, + }) +} + +func Test_ExpectSensitiveValue_ListNestedBlock_SensitiveAttribute(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // Change.AfterSensitive + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProviderSensitive(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "one" { + list_nested_block_sensitive_attribute { + sensitive_list_nested_block_attribute = "sensitive-test" + list_nested_block_attribute = "test" + } + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectSensitiveValue("test_resource.one", + tfjsonpath.New("list_nested_block_sensitive_attribute").AtSliceIndex(0). + AtMapKey("sensitive_list_nested_block_attribute")), + }, + }, + }, + }, + }) +} + +func Test_ExpectSensitiveValue_SetNestedBlock_SensitiveAttribute(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // Change.AfterSensitive + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProviderSensitive(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "one" { + set_nested_block_sensitive_attribute { + sensitive_set_nested_block_attribute = "sensitive-test" + set_nested_block_attribute = "test" + } + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectSensitiveValue("test_resource.one", + tfjsonpath.New("set_nested_block_sensitive_attribute")), + }, + }, + }, + }, + }) +} + +func Test_ExpectSensitiveValue_ExpectError_ResourceNotFound(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // Change.AfterSensitive + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProviderSensitive(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "one" {} + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectSensitiveValue("test_resource.two", tfjsonpath.New("set_attribute")), + }, + }, + ExpectError: regexp.MustCompile(`test_resource.two - Resource not found in plan ResourceChanges`), + }, + }, + }) +} + +func testProviderSensitive() *schema.Provider { + return &schema.Provider{ + ResourcesMap: map[string]*schema.Resource{ + "test_resource": { + CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + d.SetId("test") + return nil + }, + UpdateContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + Schema: map[string]*schema.Schema{ + "sensitive_string_attribute": { + Sensitive: true, + Optional: true, + Type: schema.TypeString, + }, + "sensitive_list_attribute": { + Sensitive: true, + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + }, + "sensitive_set_attribute": { + Sensitive: true, + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + }, + "sensitive_map_attribute": { + Sensitive: true, + Type: schema.TypeMap, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + }, + "list_nested_block_sensitive_attribute": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "list_nested_block_attribute": { + Type: schema.TypeString, + Optional: true, + }, + "sensitive_list_nested_block_attribute": { + Sensitive: true, + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "set_nested_block_sensitive_attribute": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "set_nested_block_attribute": { + Type: schema.TypeString, + Optional: true, + }, + "sensitive_set_nested_block_attribute": { + Sensitive: true, + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + } +} diff --git a/plancheck/expect_unknown_output_value.go b/plancheck/expect_unknown_output_value.go new file mode 100644 index 000000000..a104b4887 --- /dev/null +++ b/plancheck/expect_unknown_output_value.go @@ -0,0 +1,82 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package plancheck + +import ( + "context" + "fmt" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +var _ PlanCheck = expectUnknownOutputValue{} + +type expectUnknownOutputValue struct { + outputAddress string +} + +// CheckPlan implements the plan check logic. +func (e expectUnknownOutputValue) CheckPlan(ctx context.Context, req CheckPlanRequest, resp *CheckPlanResponse) { + var change *tfjson.Change + + for address, oc := range req.Plan.OutputChanges { + if e.outputAddress == address { + change = oc + + break + } + } + + if change == nil { + resp.Error = fmt.Errorf("%s - Output not found in plan OutputChanges", e.outputAddress) + + return + } + + result, err := tfjsonpath.Traverse(change.AfterUnknown, tfjsonpath.Path{}) + if err != nil { + // If we find the output in the known values, return a more explicit message + knownVal, knownErr := tfjsonpath.Traverse(change.After, tfjsonpath.Path{}) + if knownErr == nil { + resp.Error = fmt.Errorf("Expected unknown value at output %q, but found known value: \"%v\"", e.outputAddress, knownVal) + return + } + + resp.Error = err + return + } + + isUnknown, ok := result.(bool) + + if !ok { + resp.Error = fmt.Errorf("invalid path: the path value cannot be asserted as bool") + + return + } + + if !isUnknown { + // The output should have a known value, look first to return a more explicit message + knownVal, knownErr := tfjsonpath.Traverse(change.After, tfjsonpath.Path{}) + if knownErr == nil { + resp.Error = fmt.Errorf("Expected unknown value at output %q, but found known value: \"%v\"", e.outputAddress, knownVal) + return + } + resp.Error = fmt.Errorf("Expected unknown value at output %q, but found known value", e.outputAddress) + + return + } +} + +// ExpectUnknownOutputValue returns a plan check that asserts that the specified output has an unknown value. +// +// Due to implementation differences between the terraform-plugin-sdk and the terraform-plugin-framework, representation of unknown +// values may differ. For example, terraform-plugin-sdk based providers may have less precise representations of unknown values, such +// as marking whole maps as unknown rather than individual element values. +func ExpectUnknownOutputValue(outputAddress string) PlanCheck { + return expectUnknownOutputValue{ + outputAddress: outputAddress, + } +} diff --git a/plancheck/expect_unknown_output_value_at_path.go b/plancheck/expect_unknown_output_value_at_path.go new file mode 100644 index 000000000..9c54e09b8 --- /dev/null +++ b/plancheck/expect_unknown_output_value_at_path.go @@ -0,0 +1,85 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package plancheck + +import ( + "context" + "fmt" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +var _ PlanCheck = expectUnknownOutputValueAtPath{} + +type expectUnknownOutputValueAtPath struct { + outputAddress string + valuePath tfjsonpath.Path +} + +// CheckPlan implements the plan check logic. +func (e expectUnknownOutputValueAtPath) CheckPlan(ctx context.Context, req CheckPlanRequest, resp *CheckPlanResponse) { + var change *tfjson.Change + + for address, oc := range req.Plan.OutputChanges { + if e.outputAddress == address { + change = oc + + break + } + } + + if change == nil { + resp.Error = fmt.Errorf("%s - Output not found in plan OutputChanges", e.outputAddress) + + return + } + + result, err := tfjsonpath.Traverse(change.AfterUnknown, e.valuePath) + if err != nil { + // If we find the output in the known values, return a more explicit message + knownVal, knownErr := tfjsonpath.Traverse(change.After, e.valuePath) + if knownErr == nil { + resp.Error = fmt.Errorf("Expected unknown value at output %q path %q, but found known value: \"%v\"", e.outputAddress, e.valuePath.String(), knownVal) + return + } + + resp.Error = err + + return + } + + isUnknown, ok := result.(bool) + + if !ok { + resp.Error = fmt.Errorf("invalid path: the path value cannot be asserted as bool") + + return + } + + if !isUnknown { + // The output should have a known value, look first to return a more explicit message + knownVal, knownErr := tfjsonpath.Traverse(change.After, e.valuePath) + if knownErr == nil { + resp.Error = fmt.Errorf("Expected unknown value at output %q path %q, but found known value: \"%v\"", e.outputAddress, e.valuePath.String(), knownVal) + return + } + resp.Error = fmt.Errorf("Expected unknown value at output %q path %q, but found known value", e.outputAddress, e.valuePath.String()) + + return + } +} + +// ExpectUnknownOutputValueAtPath returns a plan check that asserts that the specified output has an unknown value. +// +// Due to implementation differences between the terraform-plugin-sdk and the terraform-plugin-framework, representation of unknown +// values may differ. For example, terraform-plugin-sdk based providers may have less precise representations of unknown values, such +// as marking whole maps as unknown rather than individual element values. +func ExpectUnknownOutputValueAtPath(outputAddress string, valuePath tfjsonpath.Path) PlanCheck { + return expectUnknownOutputValueAtPath{ + outputAddress: outputAddress, + valuePath: valuePath, + } +} diff --git a/plancheck/expect_unknown_output_value_at_path_test.go b/plancheck/expect_unknown_output_value_at_path_test.go new file mode 100644 index 000000000..b2b9c0c49 --- /dev/null +++ b/plancheck/expect_unknown_output_value_at_path_test.go @@ -0,0 +1,625 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package plancheck_test + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func Test_ExpectUnknownOutputValueAtPath_StringAttribute(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ExternalProviders: map[string]r.ExternalProvider{ + "data": { + Source: "terraform.io/builtin/terraform", + }, + }, + // The terraform_data resource is not available prior to Terraform v1.4.0 + // Reference: https://github.com/hashicorp/terraform/blob/v1.4/CHANGELOG.md#140-march-08-2023 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_4_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "terraform_data" "one" { + input = "string" + } + + output "resource" { + value = terraform_data.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownOutputValueAtPath("resource", tfjsonpath.New("output")), + }, + }, + }, + }, + }) +} + +func Test_ExpectUnknownOutputValueAtPath_ListAttribute(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ExternalProviders: map[string]r.ExternalProvider{ + "data": { + Source: "terraform.io/builtin/terraform", + }, + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // The terraform_data resource is not available prior to Terraform v1.4.0 + // Reference: https://github.com/hashicorp/terraform/blob/v1.4/CHANGELOG.md#140-march-08-2023 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_4_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "terraform_data" "one" { + input = "string" + } + + resource "test_resource" "two" { + list_attribute = ["value1", terraform_data.one.output] + } + + output "resource" { + value = test_resource.two + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownOutputValueAtPath("resource", tfjsonpath.New("list_attribute")), + }, + }, + }, + }, + }) +} + +func Test_ExpectUnknownOutputValueAtPath_SetAttribute(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ExternalProviders: map[string]r.ExternalProvider{ + "data": { + Source: "terraform.io/builtin/terraform", + }, + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // The terraform_data resource is not available prior to Terraform v1.4.0 + // Reference: https://github.com/hashicorp/terraform/blob/v1.4/CHANGELOG.md#140-march-08-2023 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_4_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "terraform_data" "one" { + input = "string" + } + + resource "test_resource" "two" { + set_attribute = ["value1", terraform_data.one.output] + } + + output "resource" { + value = test_resource.two + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownOutputValueAtPath("resource", tfjsonpath.New("set_attribute")), + }, + }, + }, + }, + }) +} + +func Test_ExpectUnknownOutputValueAtPath_MapAttribute(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ExternalProviders: map[string]r.ExternalProvider{ + "data": { + Source: "terraform.io/builtin/terraform", + }, + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // The terraform_data resource is not available prior to Terraform v1.4.0 + // Reference: https://github.com/hashicorp/terraform/blob/v1.4/CHANGELOG.md#140-march-08-2023 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_4_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "terraform_data" "one" { + input = "string" + } + + resource "test_resource" "two" { + map_attribute = { + key1 = "value1", + key2 = terraform_data.one.output + } + } + + output "resource" { + value = test_resource.two + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownOutputValueAtPath("resource", tfjsonpath.New("map_attribute")), + }, + }, + }, + }, + }) +} + +func Test_ExpectUnknownOutputValueAtPath_ListNestedBlock_Resource(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ExternalProviders: map[string]r.ExternalProvider{ + "data": { + Source: "terraform.io/builtin/terraform", + }, + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // The terraform_data resource is not available prior to Terraform v1.4.0 + // Reference: https://github.com/hashicorp/terraform/blob/v1.4/CHANGELOG.md#140-march-08-2023 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_4_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "terraform_data" "one" { + input = "string" + } + + resource "test_resource" "two" { + list_nested_block { + list_nested_block_attribute = terraform_data.one.output + } + } + + output "resource" { + value = test_resource.two + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownOutputValueAtPath("resource", tfjsonpath.New("list_nested_block").AtSliceIndex(0).AtMapKey("list_nested_block_attribute")), + }, + }, + }, + }, + }) +} + +func Test_ExpectUnknownOutputValueAtPath_ListNestedBlock_ResourceBlocks(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ExternalProviders: map[string]r.ExternalProvider{ + "data": { + Source: "terraform.io/builtin/terraform", + }, + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // The terraform_data resource is not available prior to Terraform v1.4.0 + // Reference: https://github.com/hashicorp/terraform/blob/v1.4/CHANGELOG.md#140-march-08-2023 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_4_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "terraform_data" "one" { + input = "string" + } + + resource "test_resource" "two" { + list_nested_block { + list_nested_block_attribute = terraform_data.one.output + } + } + + output "resource_blocks" { + value = test_resource.two.list_nested_block + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownOutputValueAtPath("resource_blocks", tfjsonpath.New(0).AtMapKey("list_nested_block_attribute")), + }, + }, + }, + }, + }) +} + +func Test_ExpectUnknownOutputValueAtPath_ListNestedBlock_ObjectBlockIndex(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ExternalProviders: map[string]r.ExternalProvider{ + "data": { + Source: "terraform.io/builtin/terraform", + }, + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // The terraform_data resource is not available prior to Terraform v1.4.0 + // Reference: https://github.com/hashicorp/terraform/blob/v1.4/CHANGELOG.md#140-march-08-2023 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_4_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "terraform_data" "one" { + input = "string" + } + + resource "test_resource" "two" { + list_nested_block { + list_nested_block_attribute = terraform_data.one.output + } + } + + output "resource_blocks_index" { + value = test_resource.two.list_nested_block.0 + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownOutputValueAtPath("resource_blocks_index", tfjsonpath.New("list_nested_block_attribute")), + }, + }, + }, + }, + }) +} + +func Test_ExpectUnknownOutputValueAtPath_SetNestedBlock_Object(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ExternalProviders: map[string]r.ExternalProvider{ + "data": { + Source: "terraform.io/builtin/terraform", + }, + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // The terraform_data resource is not available prior to Terraform v1.4.0 + // Reference: https://github.com/hashicorp/terraform/blob/v1.4/CHANGELOG.md#140-march-08-2023 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_4_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "terraform_data" "one" { + input = "string" + } + + resource "test_resource" "two" { + set_nested_block { + set_nested_block_attribute = terraform_data.one.output + } + } + + output "resource" { + value = test_resource.two + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownOutputValueAtPath("resource", tfjsonpath.New("set_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block_attribute")), + }, + }, + }, + }, + }) +} + +func Test_ExpectUnknownOutputValueAtPath_ExpectError_KnownValue_PathNotFound(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "one" { + list_nested_block { + list_nested_block_attribute = "value 1" + } + } + + output "resource" { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownOutputValueAtPath("resource", tfjsonpath.New("list_nested_block").AtSliceIndex(0).AtMapKey("not_correct_attr")), + }, + }, + ExpectError: regexp.MustCompile(`path not found: specified key not_correct_attr not found in map at list_nested_block.0.not_correct_attr`), + }, + }, + }) +} + +func Test_ExpectUnknownOutputValueAtPath_ExpectError_KnownValue_ListAttribute(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "one" { + list_attribute = ["value1"] + } + + output "resource" { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownOutputValueAtPath("resource", tfjsonpath.New("list_attribute").AtSliceIndex(0)), + }, + }, + ExpectError: regexp.MustCompile(`Expected unknown value at output "resource" path "list_attribute.0", but found known value: "value1"`), + }, + }, + }) +} + +func Test_ExpectUnknownOutputValueAtPath_ExpectError_KnownValue_StringAttribute(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "one" { + string_attribute = "hello world!" + } + + output "resource" { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownOutputValueAtPath("resource", tfjsonpath.New("string_attribute")), + }, + }, + ExpectError: regexp.MustCompile(`Expected unknown value at output "resource" path "string_attribute", but found known value: "hello world!"`), + }, + }, + }) +} + +func Test_ExpectUnknownOutputValueAtPath_ExpectError_KnownValue_BoolAttribute(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "one" { + bool_attribute = true + } + + output "resource" { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownOutputValueAtPath("resource", tfjsonpath.New("bool_attribute")), + }, + }, + ExpectError: regexp.MustCompile(`Expected unknown value at output "resource" path "bool_attribute", but found known value: "true"`), + }, + }, + }) +} + +func Test_ExpectUnknownOutputValueAtPath_ExpectError_KnownValue_FloatAttribute(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "one" { + float_attribute = 1.234 + } + + output "resource" { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownOutputValueAtPath("resource", tfjsonpath.New("float_attribute")), + }, + }, + ExpectError: regexp.MustCompile(`Expected unknown value at output "resource" path "float_attribute", but found known value: "1.234"`), + }, + }, + }) +} + +func Test_ExpectUnknownOutputValueAtPath_ExpectError_KnownValue_ListNestedBlock(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "one" { + list_nested_block { + list_nested_block_attribute = "value 1" + } + } + + output "resource" { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownOutputValueAtPath("resource", tfjsonpath.New("list_nested_block").AtSliceIndex(0).AtMapKey("list_nested_block_attribute")), + }, + }, + ExpectError: regexp.MustCompile(`Expected unknown value at output "resource" path "list_nested_block.0.list_nested_block_attribute", but found known value: "value 1"`), + }, + }, + }) +} + +func Test_ExpectUnknownOutputValueAtPath_ExpectError_OutputNotFound(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "one" { + set_attribute = ["value1"] + } + + output "output_one" { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownOutputValueAtPath("output_two", tfjsonpath.New("set_attribute")), + }, + }, + ExpectError: regexp.MustCompile(`output_two - Output not found in plan OutputChanges`), + }, + }, + }) +} diff --git a/plancheck/expect_unknown_output_value_test.go b/plancheck/expect_unknown_output_value_test.go new file mode 100644 index 000000000..1e0c15770 --- /dev/null +++ b/plancheck/expect_unknown_output_value_test.go @@ -0,0 +1,413 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package plancheck_test + +import ( + "regexp" + "testing" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func Test_ExpectUnknownOutputValue_StringAttribute(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ExternalProviders: map[string]r.ExternalProvider{ + "data": { + Source: "terraform.io/builtin/terraform", + }, + }, + // The terraform_data resource is not available prior to Terraform v1.4.0 + // Reference: https://github.com/hashicorp/terraform/blob/v1.4/CHANGELOG.md#140-march-08-2023 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.4.0"))), + }, + Steps: []r.TestStep{ + { + Config: `resource "terraform_data" "one" { + input = "string" + } + + output "string_attribute" { + value = terraform_data.one.output + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownOutputValue("string_attribute"), + }, + }, + }, + }, + }) +} + +func Test_ExpectUnknownOutputValue_ListAttribute(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ExternalProviders: map[string]r.ExternalProvider{ + "data": { + Source: "terraform.io/builtin/terraform", + }, + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // The terraform_data resource is not available prior to Terraform v1.4.0 + // Reference: https://github.com/hashicorp/terraform/blob/v1.4/CHANGELOG.md#140-march-08-2023 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.4.0"))), + }, + Steps: []r.TestStep{ + { + Config: `resource "terraform_data" "one" { + input = "string" + } + + resource "test_resource" "two" { + list_attribute = ["value1", terraform_data.one.output] + } + + output "list_attribute" { + value = test_resource.two.list_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownOutputValue("list_attribute"), + }, + }, + }, + }, + }) +} + +func Test_ExpectUnknownOutputValue_SetAttribute(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ExternalProviders: map[string]r.ExternalProvider{ + "data": { + Source: "terraform.io/builtin/terraform", + }, + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // The terraform_data resource is not available prior to Terraform v1.4.0 + // Reference: https://github.com/hashicorp/terraform/blob/v1.4/CHANGELOG.md#140-march-08-2023 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.4.0"))), + }, + Steps: []r.TestStep{ + { + Config: `resource "terraform_data" "one" { + input = "string" + } + + resource "test_resource" "two" { + set_attribute = ["value1", terraform_data.one.output] + } + + output "set_attribute" { + value = test_resource.two.set_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownOutputValue("set_attribute"), + }, + }, + }, + }, + }) +} + +func Test_ExpectUnknownOutputValue_MapAttribute(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ExternalProviders: map[string]r.ExternalProvider{ + "data": { + Source: "terraform.io/builtin/terraform", + }, + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // The terraform_data resource is not available prior to Terraform v1.4.0 + // Reference: https://github.com/hashicorp/terraform/blob/v1.4/CHANGELOG.md#140-march-08-2023 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.4.0"))), + }, + Steps: []r.TestStep{ + { + Config: `resource "terraform_data" "one" { + input = "string" + } + + resource "test_resource" "two" { + map_attribute = { + key1 = "value1", + key2 = terraform_data.one.output + } + } + + output "map_attribute" { + value = test_resource.two.map_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownOutputValue("map_attribute"), + }, + }, + }, + }, + }) +} + +func Test_ExpectUnknownOutputValue_ListNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ExternalProviders: map[string]r.ExternalProvider{ + "data": { + Source: "terraform.io/builtin/terraform", + }, + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // The terraform_data resource is not available prior to Terraform v1.4.0 + // Reference: https://github.com/hashicorp/terraform/blob/v1.4/CHANGELOG.md#140-march-08-2023 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.4.0"))), + }, + Steps: []r.TestStep{ + { + Config: `resource "terraform_data" "one" { + input = "string" + } + + resource "test_resource" "two" { + list_nested_block { + list_nested_block_attribute = terraform_data.one.output + } + } + + output "list_nested_block_attribute" { + value = test_resource.two.list_nested_block.0.list_nested_block_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownOutputValue("list_nested_block_attribute"), + }, + }, + }, + }, + }) +} + +func Test_ExpectUnknownOutputValue_ExpectError_KnownValue_ListAttribute(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "one" { + list_attribute = ["value1"] + } + + output "list_attribute" { + value = test_resource.one.list_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownOutputValue("list_attribute"), + }, + }, + ExpectError: regexp.MustCompile(`Expected unknown value at output "list_attribute", but found known value: "\[value1\]"`), + }, + }, + }) +} + +func Test_ExpectUnknownOutputValue_ExpectError_KnownValue_StringAttribute(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + ProviderFactories: map[string]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 = "hello world!" + } + + output "string_attribute" { + value = test_resource.one.string_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownOutputValue("string_attribute"), + }, + }, + ExpectError: regexp.MustCompile(`Expected unknown value at output "string_attribute", but found known value: "hello world!"`), + }, + }, + }) +} + +func Test_ExpectUnknownOutputValue_ExpectError_KnownValue_BoolAttribute(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "one" { + bool_attribute = true + } + + output "bool_attribute" { + value = test_resource.one.bool_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownOutputValue("bool_attribute"), + }, + }, + ExpectError: regexp.MustCompile(`Expected unknown value at output "bool_attribute", but found known value: "true"`), + }, + }, + }) +} + +func Test_ExpectUnknownOutputValue_ExpectError_KnownValue_FloatAttribute(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + ProviderFactories: map[string]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 + } + + output "float_attribute" { + value = test_resource.one.float_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownOutputValue("float_attribute"), + }, + }, + ExpectError: regexp.MustCompile(`Expected unknown value at output "float_attribute", but found known value: "1.234"`), + }, + }, + }) +} + +func Test_ExpectUnknownOutputValue_ExpectError_KnownValue_ListNestedBlock(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "one" { + list_nested_block { + list_nested_block_attribute = "value 1" + } + } + + output "list_nested_block" { + value = test_resource.one.list_nested_block + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownOutputValue("list_nested_block"), + }, + }, + ExpectError: regexp.MustCompile(`Expected unknown value at output "list_nested_block", but found known value: "\[map\[list_nested_block_attribute:value 1\]\]"`), + }, + }, + }) +} + +func Test_ExpectUnknownOutputValue_ExpectError_OutputNotFound(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "one" {} + + output "output_one" { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownOutputValue("output_two"), + }, + }, + ExpectError: regexp.MustCompile(`output_two - Output not found in plan OutputChanges`), + }, + }, + }) +} diff --git a/plancheck/expect_unknown_value.go b/plancheck/expect_unknown_value.go new file mode 100644 index 000000000..91ec3beb2 --- /dev/null +++ b/plancheck/expect_unknown_value.go @@ -0,0 +1,75 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package plancheck + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +var _ PlanCheck = expectUnknownValue{} + +type expectUnknownValue struct { + resourceAddress string + attributePath tfjsonpath.Path +} + +// CheckPlan implements the plan check logic. +func (e expectUnknownValue) CheckPlan(ctx context.Context, req CheckPlanRequest, resp *CheckPlanResponse) { + + for _, rc := range req.Plan.ResourceChanges { + if e.resourceAddress != rc.Address { + continue + } + + result, err := tfjsonpath.Traverse(rc.Change.AfterUnknown, e.attributePath) + if err != nil { + // If we find the attribute in the known values, return a more explicit message + knownVal, knownErr := tfjsonpath.Traverse(rc.Change.After, e.attributePath) + if knownErr == nil { + resp.Error = fmt.Errorf("Expected unknown value at %q, but found known value: \"%v\"", e.attributePath.String(), knownVal) + return + } + + resp.Error = err + return + } + + isUnknown, ok := result.(bool) + if !ok { + resp.Error = fmt.Errorf("invalid path: the path value cannot be asserted as bool") + return + } + + if !isUnknown { + // The attribute should have a known value, look first to return a more explicit message + knownVal, knownErr := tfjsonpath.Traverse(rc.Change.After, e.attributePath) + if knownErr == nil { + resp.Error = fmt.Errorf("Expected unknown value at %q, but found known value: \"%v\"", e.attributePath.String(), knownVal) + return + } + + resp.Error = fmt.Errorf("Expected unknown value at %q, but found known value", e.attributePath.String()) + return + } + + return + } + + resp.Error = fmt.Errorf("%s - Resource not found in plan ResourceChanges", e.resourceAddress) +} + +// ExpectUnknownValue returns a plan check that asserts that the specified attribute at the given resource has an unknown value. +// +// Due to implementation differences between the terraform-plugin-sdk and the terraform-plugin-framework, representation of unknown +// values may differ. For example, terraform-plugin-sdk based providers may have less precise representations of unknown values, such +// as marking whole maps as unknown rather than individual element values. +func ExpectUnknownValue(resourceAddress string, attributePath tfjsonpath.Path) PlanCheck { + return expectUnknownValue{ + resourceAddress: resourceAddress, + attributePath: attributePath, + } +} diff --git a/plancheck/expect_unknown_value_test.go b/plancheck/expect_unknown_value_test.go new file mode 100644 index 000000000..fd2b1c789 --- /dev/null +++ b/plancheck/expect_unknown_value_test.go @@ -0,0 +1,496 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package plancheck_test + +import ( + "context" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func Test_ExpectUnknownValue_StringAttribute(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ExternalProviders: map[string]r.ExternalProvider{ + "time": { + Source: "registry.terraform.io/hashicorp/time", + }, + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "time_static" "one" {} + + resource "test_resource" "two" { + string_attribute = time_static.one.rfc3339 + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownValue("test_resource.two", tfjsonpath.New("string_attribute")), + }, + }, + }, + }, + }) +} + +func Test_ExpectUnknownValue_ListAttribute(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ExternalProviders: map[string]r.ExternalProvider{ + "time": { + Source: "registry.terraform.io/hashicorp/time", + }, + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "time_static" "one" {} + + resource "test_resource" "two" { + list_attribute = ["value1", time_static.one.rfc3339] + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownValue("test_resource.two", tfjsonpath.New("list_attribute")), + }, + }, + }, + }, + }) +} + +func Test_ExpectUnknownValue_SetAttribute(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ExternalProviders: map[string]r.ExternalProvider{ + "time": { + Source: "registry.terraform.io/hashicorp/time", + }, + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "time_static" "one" {} + + resource "test_resource" "two" { + set_attribute = ["value1", time_static.one.rfc3339] + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownValue("test_resource.two", tfjsonpath.New("set_attribute")), + }, + }, + }, + }, + }) +} + +func Test_ExpectUnknownValue_MapAttribute(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ExternalProviders: map[string]r.ExternalProvider{ + "time": { + Source: "registry.terraform.io/hashicorp/time", + }, + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "time_static" "one" {} + + resource "test_resource" "two" { + map_attribute = { + key1 = "value1", + key2 = time_static.one.rfc3339 + } + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownValue("test_resource.two", tfjsonpath.New("map_attribute")), + }, + }, + }, + }, + }) +} + +func Test_ExpectUnknownValue_ListNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ExternalProviders: map[string]r.ExternalProvider{ + "time": { + Source: "registry.terraform.io/hashicorp/time", + }, + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "time_static" "one" {} + + resource "test_resource" "two" { + list_nested_block { + list_nested_block_attribute = time_static.one.rfc3339 + } + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownValue("test_resource.two", + tfjsonpath.New("list_nested_block").AtSliceIndex(0).AtMapKey("list_nested_block_attribute")), + }, + }, + }, + }, + }) +} + +func Test_ExpectUnknownValue_SetNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ExternalProviders: map[string]r.ExternalProvider{ + "time": { + Source: "registry.terraform.io/hashicorp/time", + }, + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "time_static" "one" {} + + resource "test_resource" "two" { + set_nested_block { + set_nested_block_attribute = time_static.one.rfc3339 + } + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownValue("test_resource.two", + tfjsonpath.New("set_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block_attribute")), + }, + }, + }, + }, + }) +} + +func Test_ExpectUnknownValue_ExpectError_KnownValue_PathNotFound(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "two" { + list_nested_block { + list_nested_block_attribute = "value 1" + } + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownValue("test_resource.two", + tfjsonpath.New("list_nested_block").AtSliceIndex(0).AtMapKey("not_correct_attr")), + }, + }, + ExpectError: regexp.MustCompile(`path not found: specified key not_correct_attr not found in map at list_nested_block.0.not_correct_attr`), + }, + }, + }) +} + +func Test_ExpectUnknownValue_ExpectError_KnownValue_ListAttribute(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "one" { + list_attribute = ["value1"] + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownValue("test_resource.one", tfjsonpath.New("list_attribute").AtSliceIndex(0)), + }, + }, + ExpectError: regexp.MustCompile(`Expected unknown value at "list_attribute.0", but found known value: "value1"`), + }, + }, + }) +} + +func Test_ExpectUnknownValue_ExpectError_KnownValue_StringAttribute(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + ProviderFactories: map[string]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 = "hello world!" + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownValue("test_resource.one", tfjsonpath.New("string_attribute")), + }, + }, + ExpectError: regexp.MustCompile(`Expected unknown value at "string_attribute", but found known value: "hello world!"`), + }, + }, + }) +} + +func Test_ExpectUnknownValue_ExpectError_KnownValue_BoolAttribute(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + ProviderFactories: map[string]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 + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownValue("test_resource.one", tfjsonpath.New("bool_attribute")), + }, + }, + ExpectError: regexp.MustCompile(`Expected unknown value at "bool_attribute", but found known value: "true"`), + }, + }, + }) +} + +func Test_ExpectUnknownValue_ExpectError_KnownValue_FloatAttribute(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + ProviderFactories: map[string]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 + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownValue("test_resource.one", tfjsonpath.New("float_attribute")), + }, + }, + ExpectError: regexp.MustCompile(`Expected unknown value at "float_attribute", but found known value: "1.234"`), + }, + }, + }) +} + +func Test_ExpectUnknownValue_ExpectError_KnownValue_ListNestedBlock(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "two" { + list_nested_block { + list_nested_block_attribute = "value 1" + } + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownValue("test_resource.two", + tfjsonpath.New("list_nested_block").AtSliceIndex(0).AtMapKey("list_nested_block_attribute")), + }, + }, + ExpectError: regexp.MustCompile(`Expected unknown value at "list_nested_block.0.list_nested_block_attribute", but found known value: "value 1"`), + }, + }, + }) +} + +func Test_ExpectUnknownValue_ExpectError_ResourceNotFound(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "one" {} + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownValue("test_resource.two", tfjsonpath.New("set_attribute")), + }, + }, + ExpectError: regexp.MustCompile(`test_resource.two - Resource not found in plan ResourceChanges`), + }, + }, + }) +} + +func testProvider() *schema.Provider { + return &schema.Provider{ + ResourcesMap: map[string]*schema.Resource{ + "test_resource": { + CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + d.SetId("test") + return nil + }, + UpdateContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + Schema: map[string]*schema.Schema{ + "bool_attribute": { + Optional: true, + Type: schema.TypeBool, + }, + "float_attribute": { + Optional: true, + Type: schema.TypeFloat, + }, + "int_attribute": { + Optional: true, + Type: schema.TypeInt, + }, + "list_attribute": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + }, + "list_nested_block": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "list_nested_block_attribute": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "map_attribute": { + Type: schema.TypeMap, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + }, + "set_attribute": { + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + }, + "set_nested_block": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "set_nested_block_attribute": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "string_attribute": { + Optional: true, + Type: schema.TypeString, + }, + }, + }, + }, + } +} diff --git a/querycheck/contains.go b/querycheck/contains.go new file mode 100644 index 000000000..6cef57012 --- /dev/null +++ b/querycheck/contains.go @@ -0,0 +1,38 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "fmt" + "strings" +) + +var _ QueryResultCheck = contains{} + +type contains struct { + resourceAddress string + check string +} + +func (c contains) CheckQuery(_ context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { + for _, res := range req.Query { + if strings.EqualFold(c.check, res.DisplayName) { + return + } + } + + resp.Error = fmt.Errorf("expected to find resource with display name %q in results but resource was not found", c.check) + +} + +// ContainsResourceWithName returns a query check that asserts that a resource with a given display name exists within the returned results of the query. +// +// This query check can only be used with managed resources that support query. Query is only supported in Terraform v1.14+ +func ContainsResourceWithName(resourceAddress string, displayName string) QueryResultCheck { + return contains{ + resourceAddress: resourceAddress, + check: displayName, + } +} diff --git a/querycheck/doc.go b/querycheck/doc.go new file mode 100644 index 000000000..67aa0cede --- /dev/null +++ b/querycheck/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package querycheck contains the query check interface, request/response structs, and common query check implementations. +package querycheck diff --git a/querycheck/expect_identity.go b/querycheck/expect_identity.go new file mode 100644 index 000000000..2d8777f7c --- /dev/null +++ b/querycheck/expect_identity.go @@ -0,0 +1,93 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "errors" + "fmt" + "sort" + "strings" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" +) + +var _ QueryResultCheck = expectIdentity{} + +type expectIdentity struct { + listResourceAddress string + check map[string]knownvalue.Check +} + +// CheckQuery implements the query check logic. +func (e expectIdentity) CheckQuery(_ context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { + for _, res := range req.Query { + var errCollection []error + + if e.listResourceAddress != strings.TrimPrefix(res.Address, "list.") { + continue + } + + if len(res.Identity) != len(e.check) { + deltaMsg := "" + if len(res.Identity) > len(e.check) { + deltaMsg = statecheck.CreateDeltaString(res.Identity, e.check, "actual identity has extra attribute(s): ") + } else { + deltaMsg = statecheck.CreateDeltaString(e.check, res.Identity, "actual identity is missing attribute(s): ") + } + + resp.Error = fmt.Errorf("%s - Expected %d attribute(s) in the actual identity object, got %d attribute(s): %s", e.listResourceAddress, len(e.check), len(res.Identity), deltaMsg) + return + } + + var keys []string + + for k := range e.check { + keys = append(keys, k) + } + + sort.SliceStable(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + for _, k := range keys { + actualIdentityVal, ok := res.Identity[k] + + if !ok { + resp.Error = fmt.Errorf("%s - missing attribute %q in actual identity object", e.listResourceAddress, k) + return + } + + if err := e.check[k].CheckValue(actualIdentityVal); err != nil { + errCollection = append(errCollection, fmt.Errorf("%s - %q identity attribute: %s", e.listResourceAddress, k, err)) + } + } + + if errCollection == nil { + return + } + } + + var errCollection []error + errCollection = append(errCollection, fmt.Errorf("an identity with the following attributes was not found")) + + // wrap errors for each check + for attr, check := range e.check { + errCollection = append(errCollection, fmt.Errorf("attribute %q: %s", attr, check)) + } + errCollection = append(errCollection, fmt.Errorf("address: %s\n", e.listResourceAddress)) + resp.Error = errors.Join(errCollection...) +} + +// ExpectIdentity returns a query check that asserts that the identity at the given resource matches a known object, where each +// map key represents an identity attribute name. The identity in query must exactly match the given object. +// +// This query check can only be used with managed resources that support resource identity and query. Query is only supported in Terraform v1.14+ +func ExpectIdentity(resourceAddress string, identity map[string]knownvalue.Check) QueryResultCheck { + return expectIdentity{ + listResourceAddress: resourceAddress, + check: identity, + } +} diff --git a/querycheck/expect_known_value.go b/querycheck/expect_known_value.go new file mode 100644 index 000000000..faa046eb5 --- /dev/null +++ b/querycheck/expect_known_value.go @@ -0,0 +1,74 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +var _ QueryResultCheck = expectKnownValue{} + +type expectKnownValue struct { + listResourceAddress string + resourceName string + attributePath tfjsonpath.Path + knownValue knownvalue.Check +} + +func (e expectKnownValue) CheckQuery(_ context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { + for _, res := range req.Query { + var diags []error + + if e.listResourceAddress == strings.TrimPrefix(res.Address, "list.") && e.resourceName == res.DisplayName { + if res.ResourceObject == nil { + resp.Error = fmt.Errorf("%s - no resource object was returned, ensure `include_resource` has been set to `true` in the list resource config`", e.listResourceAddress) + return + } + + resource, err := tfjsonpath.Traverse(res.ResourceObject, e.attributePath) + if err != nil { + resp.Error = err + return + } + + if err := e.knownValue.CheckValue(resource); err != nil { + diags = append(diags, fmt.Errorf("error checking value for attribute at path: %s for resource %s, err: %s", e.attributePath.String(), e.resourceName, err)) + } + + if diags == nil { + return + } + } + + if diags != nil { + var diagsStr string + for _, diag := range diags { + diagsStr += diag.Error() + "; " + } + resp.Error = fmt.Errorf("the following errors were found while checking values: %s", diagsStr) + return + } + } + + resp.Error = fmt.Errorf("%s - the resource %s was not found", e.listResourceAddress, e.resourceName) +} + +// ExpectKnownValue returns a query check that asserts the specified attribute values are present for a given resource object +// returned by a list query. The resource object can only be identified by providing the list resource address as well as the +// resource name (display name). +// +// This query check can only be used with managed resources that support resource identity and query. Query is only supported in Terraform v1.14+ +func ExpectKnownValue(listResourceAddress string, resourceName string, attributePath tfjsonpath.Path, knownValue knownvalue.Check) QueryResultCheck { + return expectKnownValue{ + listResourceAddress: listResourceAddress, + resourceName: resourceName, + attributePath: attributePath, + knownValue: knownValue, + } +} diff --git a/querycheck/expect_no_identity.go b/querycheck/expect_no_identity.go new file mode 100644 index 000000000..f5d92f1f3 --- /dev/null +++ b/querycheck/expect_no_identity.go @@ -0,0 +1,89 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "errors" + "fmt" + "sort" + "strings" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" +) + +var _ QueryResultCheck = expectNoIdentity{} + +type expectNoIdentity struct { + listResourceAddress string + check map[string]knownvalue.Check +} + +// CheckQuery implements the query check logic. +func (e expectNoIdentity) CheckQuery(_ context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { + for _, res := range req.Query { + var errCollection []error + + if e.listResourceAddress != strings.TrimPrefix(res.Address, "list.") { + continue + } + + if len(res.Identity) != len(e.check) { + deltaMsg := "" + if len(res.Identity) > len(e.check) { + deltaMsg = statecheck.CreateDeltaString(res.Identity, e.check, "actual identity has extra attribute(s): ") + } else { + deltaMsg = statecheck.CreateDeltaString(e.check, res.Identity, "actual identity is missing attribute(s): ") + } + + resp.Error = fmt.Errorf("%s - Expected %d attribute(s) in the actual identity object, got %d attribute(s): %s", e.listResourceAddress, len(e.check), len(res.Identity), deltaMsg) + return + } + + var keys []string + + for k := range e.check { + keys = append(keys, k) + } + + sort.SliceStable(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + for _, k := range keys { + actualIdentityVal, ok := res.Identity[k] + + if !ok { + resp.Error = fmt.Errorf("%s - missing attribute %q in actual identity object", e.listResourceAddress, k) + return + } + + if err := e.check[k].CheckValue(actualIdentityVal); err != nil { + errCollection = append(errCollection, fmt.Errorf("%s - %q identity attribute: %s", e.listResourceAddress, k, err)) + } + } + + if errCollection == nil { + errs := []error{fmt.Errorf("an unexpected identity matching the given attributes was found")} + // wrap errors for each check + for attr, check := range e.check { + errs = append(errs, fmt.Errorf("attribute %q: %s", attr, check)) + } + errs = append(errs, fmt.Errorf("address: %s\n", e.listResourceAddress)) + resp.Error = errors.Join(errs...) + } + } +} + +// ExpectNoIdentity returns a query check that asserts that the identity at the given resource does not match a known object, where each +// map key represents an identity attribute name. The identity in query must exactly match the given object. +// +// This query check can only be used with managed resources that support resource identity and query. Query is only supported in Terraform v1.14+ +func ExpectNoIdentity(resourceAddress string, identity map[string]knownvalue.Check) QueryResultCheck { + return expectNoIdentity{ + listResourceAddress: resourceAddress, + check: identity, + } +} diff --git a/querycheck/expect_result_length_atleast.go b/querycheck/expect_result_length_atleast.go new file mode 100644 index 000000000..6b3dbde8a --- /dev/null +++ b/querycheck/expect_result_length_atleast.go @@ -0,0 +1,39 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "fmt" +) + +var _ QueryResultCheck = expectLengthAtLeast{} + +type expectLengthAtLeast struct { + resourceAddress string + check int +} + +// CheckQuery implements the query check logic. +func (e expectLengthAtLeast) CheckQuery(_ context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { + if req.QuerySummary == nil { + resp.Error = fmt.Errorf("no completed query information available") + return + } + + if req.QuerySummary.Total < e.check { + resp.Error = fmt.Errorf("Query result of at least length %v - expected but got %v.", e.check, req.QuerySummary.Total) + return + } +} + +// ExpectLengthAtLeast returns a query check that asserts that the length of the query result is at least the given value. +// +// This query check can only be used with managed resources that support query. Query is only supported in Terraform v1.14+ +func ExpectLengthAtLeast(resourceAddress string, length int) QueryResultCheck { + return expectLengthAtLeast{ + resourceAddress: resourceAddress, + check: length, + } +} diff --git a/querycheck/expect_result_length_exact.go b/querycheck/expect_result_length_exact.go new file mode 100644 index 000000000..f10546ac2 --- /dev/null +++ b/querycheck/expect_result_length_exact.go @@ -0,0 +1,39 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + "fmt" +) + +var _ QueryResultCheck = expectLength{} + +type expectLength struct { + resourceAddress string + check int +} + +// CheckQuery implements the query check logic. +func (e expectLength) CheckQuery(_ context.Context, req CheckQueryRequest, resp *CheckQueryResponse) { + if req.QuerySummary == nil { + resp.Error = fmt.Errorf("no query summary information available") + return + } + + if e.check != req.QuerySummary.Total { + resp.Error = fmt.Errorf("number of found resources %v - expected but got %v.", e.check, req.QuerySummary.Total) + return + } +} + +// ExpectLength returns a query check that asserts that the length of the query result is exactly the given value. +// +// This query check can only be used with managed resources that support query. Query is only supported in Terraform v1.14+ +func ExpectLength(resourceAddress string, length int) QueryResultCheck { + return expectLength{ + resourceAddress: resourceAddress, + check: length, + } +} diff --git a/querycheck/query_check.go b/querycheck/query_check.go new file mode 100644 index 000000000..66d326287 --- /dev/null +++ b/querycheck/query_check.go @@ -0,0 +1,33 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package querycheck + +import ( + "context" + + tfjson "github.com/hashicorp/terraform-json" +) + +// QueryResultCheck defines an interface for implementing test logic to apply an assertion against a collection of found +// resources that were returned by a query. It returns an error if the query results do not match what is expected. +type QueryResultCheck interface { + // CheckQuery should perform the query check. + CheckQuery(context.Context, CheckQueryRequest, *CheckQueryResponse) +} + +// CheckQueryRequest is a request for an invoke of the CheckQuery function. +type CheckQueryRequest struct { + // Query represents the parsed log messages relating to found resources returned by the `terraform query -json` command. + Query []tfjson.ListResourceFoundData + + // QuerySummary contains a summary of the completed query operation + QuerySummary *tfjson.ListCompleteData +} + +// CheckQueryResponse is a response to an invoke of the CheckQuery function. +type CheckQueryResponse struct { + // Error is used to report the failure of a query check assertion and is combined with other QueryResultCheck errors + // to be reported as a test failure. + Error 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/doc.go b/statecheck/doc.go new file mode 100644 index 000000000..eba32447d --- /dev/null +++ b/statecheck/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package statecheck contains the state check interface, request/response structs, and common state check implementations. +package statecheck diff --git a/statecheck/expect_identity.go b/statecheck/expect_identity.go new file mode 100644 index 000000000..a89a06e6d --- /dev/null +++ b/statecheck/expect_identity.go @@ -0,0 +1,138 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck + +import ( + "context" + "fmt" + "maps" + "slices" + "sort" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +var _ StateCheck = expectIdentity{} + +type expectIdentity struct { + resourceAddress string + identity map[string]knownvalue.Check +} + +// CheckState implements the state check logic. +func (e expectIdentity) 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 + } + + for _, r := range req.State.Values.RootModule.Resources { + if e.resourceAddress == r.Address { + resource = r + + break + } + } + + if resource == nil { + resp.Error = fmt.Errorf("%s - Resource not found in state", e.resourceAddress) + + return + } + + if resource.IdentitySchemaVersion == nil || len(resource.IdentityValues) == 0 { + resp.Error = fmt.Errorf("%s - Identity not found in state. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.12+)", e.resourceAddress) + + return + } + + if len(resource.IdentityValues) != len(e.identity) { + deltaMsg := "" + if len(resource.IdentityValues) > len(e.identity) { + deltaMsg = CreateDeltaString(resource.IdentityValues, e.identity, "actual identity has extra attribute(s): ") + } else { + deltaMsg = CreateDeltaString(e.identity, resource.IdentityValues, "actual identity is missing attribute(s): ") + } + + resp.Error = fmt.Errorf("%s - Expected %d attribute(s) in the actual identity object, got %d attribute(s): %s", e.resourceAddress, len(e.identity), len(resource.IdentityValues), deltaMsg) + return + } + + var keys []string + + for k := range e.identity { + keys = append(keys, k) + } + + sort.SliceStable(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + for _, k := range keys { + actualIdentityVal, ok := resource.IdentityValues[k] + + if !ok { + resp.Error = fmt.Errorf("%s - missing attribute %q in actual identity object", e.resourceAddress, k) + return + } + + if err := e.identity[k].CheckValue(actualIdentityVal); err != nil { + resp.Error = fmt.Errorf("%s - %q identity attribute: %s", e.resourceAddress, k, err) + return + } + } +} + +// ExpectIdentity returns a state check that asserts that the identity at the given resource matches a known object, where each +// map key represents an identity attribute name. The identity in state must exactly match the given object and any missing/extra +// attributes will raise a diagnostic. +// +// This state check can only be used with managed resources that support resource identity. Resource identity is only supported in Terraform v1.12+ +func ExpectIdentity(resourceAddress string, identity map[string]knownvalue.Check) StateCheck { + return expectIdentity{ + resourceAddress: resourceAddress, + identity: identity, + } +} + +// CreateDeltaString prints the map keys that are present in mapA and not present in mapB +func CreateDeltaString[T any, V any](mapA map[string]T, mapB map[string]V, msgPrefix string) string { + deltaMsg := "" + + deltaMap := make(map[string]T, len(mapA)) + maps.Copy(deltaMap, mapA) + for key := range mapB { + delete(deltaMap, key) + } + + deltaKeys := slices.Sorted(maps.Keys(deltaMap)) + + for i, k := range deltaKeys { + if i == 0 { + deltaMsg += msgPrefix + } else { + deltaMsg += ", " + } + deltaMsg += fmt.Sprintf("%q", k) + } + + return deltaMsg +} diff --git a/statecheck/expect_identity_example_test.go b/statecheck/expect_identity_example_test.go new file mode 100644 index 000000000..1db3766c4 --- /dev/null +++ b/statecheck/expect_identity_example_test.go @@ -0,0 +1,41 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func ExampleExpectIdentity() { + // A typical test would accept *testing.T as a function parameter, for instance `func TestSomething(t *testing.T) { ... }`. + t := &testing.T{} + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Resource identity support is only available in Terraform v1.12+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + // Provider definition omitted. Assuming "test_resource" has an identity schema with "id" and "name" string attributes + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentity( + "test_resource.one", + map[string]knownvalue.Check{ + "id": knownvalue.StringExact("id-123"), + "name": knownvalue.StringExact("John Doe"), + }, + ), + }, + }, + }, + }) +} diff --git a/statecheck/expect_identity_test.go b/statecheck/expect_identity_test.go new file mode 100644 index 000000000..20032bfe0 --- /dev/null +++ b/statecheck/expect_identity_test.go @@ -0,0 +1,334 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck_test + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestExpectIdentity_CheckState_ResourceNotFound(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentity( + "examplecloud_thing.two", + map[string]knownvalue.Check{ + "id": knownvalue.StringExact("id-123"), + "list_of_numbers": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Int64Exact(1), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(4), + }, + ), + }, + ), + }, + ExpectError: regexp.MustCompile("examplecloud_thing.two - Resource not found in state"), + }, + }, + }) +} + +func TestExpectIdentity_CheckState_No_Terraform_Identity_Support(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + tfversion.SkipAbove(tfversion.Version1_11_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + // Resource support identity, but the Terraform versions running will not. + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentity( + "examplecloud_thing.one", + map[string]knownvalue.Check{ + "id": knownvalue.StringExact("id-123"), + "list_of_numbers": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Int64Exact(1), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(4), + }, + ), + }, + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in state. Either the resource ` + + `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, + ), + }, + }, + }) +} + +func TestExpectIdentity_CheckState_No_Identity(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + // Resource does not support identity + "examplecloud": examplecloudProviderNoIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentity( + "examplecloud_thing.one", + map[string]knownvalue.Check{ + "id": knownvalue.StringExact("id-123"), + "list_of_numbers": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Int64Exact(1), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(4), + }, + ), + }, + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in state. Either the resource ` + + `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, + ), + }, + }, + }) +} + +func TestExpectIdentity_CheckState(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentity( + "examplecloud_thing.one", + map[string]knownvalue.Check{ + "id": knownvalue.StringExact("id-123"), + "list_of_numbers": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Int64Exact(1), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(4), + }, + ), + }, + ), + }, + }, + }, + }) +} + +func TestExpectIdentity_CheckState_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + + statecheck.ExpectIdentity( + "examplecloud_thing.one", + map[string]knownvalue.Check{ + "id": knownvalue.Bool(true), + "list_of_numbers": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Int64Exact(1), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(4), + }, + ), + }, + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - "id" identity attribute: expected bool value for Bool check, got: string`), + }, + }, + }) +} + +func TestExpectIdentity_CheckState_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + + statecheck.ExpectIdentity( + "examplecloud_thing.one", + map[string]knownvalue.Check{ + "id": knownvalue.StringExact("321-id"), + "list_of_numbers": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Int64Exact(1), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(4), + }, + ), + }, + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - "id" identity attribute: expected value 321-id for StringExact check, got: id-123`), + }, + }, + }) +} + +func TestExpectIdentity_CheckState_ExtraAttribute(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + + statecheck.ExpectIdentity( + "examplecloud_thing.one", + map[string]knownvalue.Check{ + "id": knownvalue.StringExact("321-id"), + }, + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Expected 1 attribute\(s\) in the actual identity object, got 2 attribute\(s\): actual identity has extra attribute\(s\): "list_of_numbers"`), + }, + }, + }) +} + +func TestExpectIdentity_CheckState_MissingAttribute(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + + statecheck.ExpectIdentity( + "examplecloud_thing.one", + map[string]knownvalue.Check{ + "id": knownvalue.StringExact("id-123"), + "nonexistent_attr": knownvalue.StringExact("hello"), + "list_of_numbers": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Int64Exact(1), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(4), + }, + ), + }, + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Expected 3 attribute\(s\) in the actual identity object, got 2 attribute\(s\): actual identity is missing attribute\(s\): "nonexistent_attr"`), + }, + }, + }) +} + +func TestExpectIdentity_CheckState_MismatchedAttribute(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentity( + "examplecloud_thing.one", + map[string]knownvalue.Check{ + "not_id": knownvalue.StringExact("id-123"), + "list_of_numbers": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Int64Exact(1), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(4), + }, + ), + }, + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - missing attribute "not_id" in actual identity object`), + }, + }, + }) +} diff --git a/statecheck/expect_identity_value.go b/statecheck/expect_identity_value.go new file mode 100644 index 000000000..22da58ea8 --- /dev/null +++ b/statecheck/expect_identity_value.go @@ -0,0 +1,91 @@ +// 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/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +var _ StateCheck = expectIdentityValue{} + +type expectIdentityValue struct { + resourceAddress string + attributePath tfjsonpath.Path + identityValue knownvalue.Check +} + +// CheckState implements the state check logic. +func (e expectIdentityValue) 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 + } + + for _, r := range req.State.Values.RootModule.Resources { + if e.resourceAddress == r.Address { + resource = r + + break + } + } + + if resource == nil { + resp.Error = fmt.Errorf("%s - Resource not found in state", e.resourceAddress) + + return + } + + if resource.IdentitySchemaVersion == nil || len(resource.IdentityValues) == 0 { + resp.Error = fmt.Errorf("%s - Identity not found in state. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.12+)", e.resourceAddress) + + return + } + + result, err := tfjsonpath.Traverse(resource.IdentityValues, e.attributePath) + + if err != nil { + resp.Error = err + + return + } + + if err := e.identityValue.CheckValue(result); err != nil { + resp.Error = fmt.Errorf("error checking identity value for attribute at path: %s.%s, err: %s", e.resourceAddress, e.attributePath.String(), err) + + return + } +} + +// ExpectIdentityValue returns a state check that asserts that the specified identity attribute at the given resource +// matches a known value. This state check can only be used with managed resources that support resource identity. +// +// Resource identity is only supported in Terraform v1.12+ +func ExpectIdentityValue(resourceAddress string, attributePath tfjsonpath.Path, identityValue knownvalue.Check) StateCheck { + return expectIdentityValue{ + resourceAddress: resourceAddress, + attributePath: attributePath, + identityValue: identityValue, + } +} diff --git a/statecheck/expect_identity_value_example_test.go b/statecheck/expect_identity_value_example_test.go new file mode 100644 index 000000000..38aa506f2 --- /dev/null +++ b/statecheck/expect_identity_value_example_test.go @@ -0,0 +1,40 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func ExampleExpectIdentityValue() { + // A typical test would accept *testing.T as a function parameter, for instance `func TestSomething(t *testing.T) { ... }`. + t := &testing.T{} + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Resource identity support is only available in Terraform v1.12+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + // Provider definition omitted. Assuming "test_resource" has an identity schema with an "id" string attribute + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValue( + "test_resource.one", + tfjsonpath.New("id"), + knownvalue.StringExact("id-123"), + ), + }, + }, + }, + }) +} diff --git a/statecheck/expect_identity_value_matches_state.go b/statecheck/expect_identity_value_matches_state.go new file mode 100644 index 000000000..1e3c6ea14 --- /dev/null +++ b/statecheck/expect_identity_value_matches_state.go @@ -0,0 +1,97 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck + +import ( + "context" + "fmt" + "reflect" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +var _ StateCheck = expectIdentityValueMatchesState{} + +type expectIdentityValueMatchesState struct { + resourceAddress string + attributePath tfjsonpath.Path +} + +// CheckState implements the state check logic. +func (e expectIdentityValueMatchesState) 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 + } + + for _, r := range req.State.Values.RootModule.Resources { + if e.resourceAddress == r.Address { + resource = r + + break + } + } + + if resource == nil { + resp.Error = fmt.Errorf("%s - Resource not found in state", e.resourceAddress) + + return + } + + if resource.IdentitySchemaVersion == nil || len(resource.IdentityValues) == 0 { + resp.Error = fmt.Errorf("%s - Identity not found in state. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.12+)", e.resourceAddress) + + return + } + + identityResult, err := tfjsonpath.Traverse(resource.IdentityValues, e.attributePath) + + if err != nil { + resp.Error = err + + return + } + + stateResult, err := tfjsonpath.Traverse(resource.AttributeValues, e.attributePath) + + if err != nil { + resp.Error = err + + return + } + + if !reflect.DeepEqual(identityResult, stateResult) { + resp.Error = fmt.Errorf("expected identity and state value at path to match, but they differ: %s.%s, identity value: %v, state value: %v", e.resourceAddress, e.attributePath.String(), identityResult, stateResult) + + return + } +} + +// ExpectIdentityValueMatchesState returns a state check that asserts that the specified identity attribute at the given resource +// matches the same attribute in state. This is useful when an identity attribute is in sync with a state attribute of the same path. +// +// This state check can only be used with managed resources that support resource identity. Resource identity is only supported in Terraform v1.12+ +func ExpectIdentityValueMatchesState(resourceAddress string, attributePath tfjsonpath.Path) StateCheck { + return expectIdentityValueMatchesState{ + resourceAddress: resourceAddress, + attributePath: attributePath, + } +} diff --git a/statecheck/expect_identity_value_matches_state_at_path.go b/statecheck/expect_identity_value_matches_state_at_path.go new file mode 100644 index 000000000..257243998 --- /dev/null +++ b/statecheck/expect_identity_value_matches_state_at_path.go @@ -0,0 +1,106 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck + +import ( + "context" + "fmt" + "reflect" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +var _ StateCheck = expectIdentityValueMatchesStateAtPath{} + +type expectIdentityValueMatchesStateAtPath struct { + resourceAddress string + identityAttrPath tfjsonpath.Path + stateAttrPath tfjsonpath.Path +} + +// CheckState implements the state check logic. +func (e expectIdentityValueMatchesStateAtPath) 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 + } + + for _, r := range req.State.Values.RootModule.Resources { + if e.resourceAddress == r.Address { + resource = r + + break + } + } + + if resource == nil { + resp.Error = fmt.Errorf("%s - Resource not found in state", e.resourceAddress) + + return + } + + if resource.IdentitySchemaVersion == nil || len(resource.IdentityValues) == 0 { + resp.Error = fmt.Errorf("%s - Identity not found in state. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.12+)", e.resourceAddress) + + return + } + + identityResult, err := tfjsonpath.Traverse(resource.IdentityValues, e.identityAttrPath) + + if err != nil { + resp.Error = err + + return + } + + stateResult, err := tfjsonpath.Traverse(resource.AttributeValues, e.stateAttrPath) + + if err != nil { + resp.Error = err + + return + } + + if !reflect.DeepEqual(identityResult, stateResult) { + resp.Error = fmt.Errorf( + "expected identity (%[1]s.%[2]s) and state value (%[1]s.%[3]s) to match, but they differ: identity value: %[4]v, state value: %[5]v", + e.resourceAddress, + e.identityAttrPath.String(), + e.stateAttrPath.String(), + identityResult, + stateResult, + ) + + return + } +} + +// ExpectIdentityValueMatchesStateAtPath returns a state check that asserts that the specified identity attribute at the given resource +// matches the specified attribute in state. This is useful when an identity attribute is in sync with a state attribute of a different path. +// +// This state check can only be used with managed resources that support resource identity. Resource identity is only supported in Terraform v1.12+ +func ExpectIdentityValueMatchesStateAtPath(resourceAddress string, identityAttrPath, stateAttrPath tfjsonpath.Path) StateCheck { + return expectIdentityValueMatchesStateAtPath{ + resourceAddress: resourceAddress, + identityAttrPath: identityAttrPath, + stateAttrPath: stateAttrPath, + } +} diff --git a/statecheck/expect_identity_value_matches_state_at_path_example_test.go b/statecheck/expect_identity_value_matches_state_at_path_example_test.go new file mode 100644 index 000000000..474864631 --- /dev/null +++ b/statecheck/expect_identity_value_matches_state_at_path_example_test.go @@ -0,0 +1,42 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func ExampleExpectIdentityValueMatchesStateAtPath() { + // A typical test would accept *testing.T as a function parameter, for instance `func TestSomething(t *testing.T) { ... }`. + t := &testing.T{} + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Resource identity support is only available in Terraform v1.12+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + // Provider definition omitted. Assuming "test_resource": + // - Has an identity schema with an "identity_id" string attribute + // - Has a resource schema with an "state_id" string attribute + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + // The identity attribute at "identity_id" and state attribute at "state_id" must match + statecheck.ExpectIdentityValueMatchesStateAtPath( + "test_resource.one", + tfjsonpath.New("identity_id"), + tfjsonpath.New("state_id"), + ), + }, + }, + }, + }) +} diff --git a/statecheck/expect_identity_value_matches_state_at_path_test.go b/statecheck/expect_identity_value_matches_state_at_path_test.go new file mode 100644 index 000000000..3ee7a4a64 --- /dev/null +++ b/statecheck/expect_identity_value_matches_state_at_path_test.go @@ -0,0 +1,344 @@ +// 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" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestExpectIdentityValueMatchesStateAtPath_CheckState_ResourceNotFound(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValueMatchesStateAtPath( + "examplecloud_thing.two", + tfjsonpath.New("id"), + tfjsonpath.New("id"), + ), + }, + ExpectError: regexp.MustCompile("examplecloud_thing.two - Resource not found in state"), + }, + }, + }) +} + +func TestExpectIdentityValueMatchesStateAtPath_CheckState_No_Terraform_Identity_Support(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + tfversion.SkipAbove(tfversion.Version1_11_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + // Resource support identity, but the Terraform versions running will not. + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValueMatchesStateAtPath( + "examplecloud_thing.one", + tfjsonpath.New("id"), + tfjsonpath.New("id"), + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in state. Either the resource ` + + `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, + ), + }, + }, + }) +} + +func TestExpectIdentityValueMatchesStateAtPath_CheckState_No_Identity(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + // Resource does not support identity + "examplecloud": examplecloudProviderNoIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValueMatchesStateAtPath( + "examplecloud_thing.one", + tfjsonpath.New("id"), + tfjsonpath.New("id"), + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in state. Either the resource ` + + `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, + ), + }, + }, + }) +} + +func TestExpectIdentityValueMatchesStateAtPath_CheckState_String_Matches(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentityDifferentPaths(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValueMatchesStateAtPath( + "examplecloud_thing.one", + tfjsonpath.New("identity_id"), + tfjsonpath.New("state_id"), + ), + }, + }, + }, + }) +} + +func TestExpectIdentityValueMatchesStateAtPath_CheckState_String_DoesntMatch(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithMismatchedResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValueMatchesStateAtPath( + "examplecloud_thing.one", + tfjsonpath.New("id"), + tfjsonpath.New("id"), + ), + }, + ExpectError: regexp.MustCompile(`expected identity \(examplecloud_thing.one.id\) and state value \(examplecloud_thing.one.id\) to match, but they differ: identity value: id-123, state value: 321-di`), + }, + }, + }) +} + +func TestExpectIdentityValueMatchesStateAtPath_CheckState_List(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentityDifferentPaths(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValueMatchesStateAtPath( + "examplecloud_thing.one", + tfjsonpath.New("identity_list_of_numbers"), + tfjsonpath.New("state_list_of_numbers"), + ), + }, + }, + }, + }) +} + +func TestExpectIdentityValueMatchesStateAtPath_CheckState_List_DoesntMatch(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithMismatchedResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValueMatchesStateAtPath( + "examplecloud_thing.one", + tfjsonpath.New("list_of_numbers"), + tfjsonpath.New("list_of_numbers"), + ), + }, + ExpectError: regexp.MustCompile(`expected identity \(examplecloud_thing.one.list_of_numbers\) and state value \(examplecloud_thing.one.list_of_numbers\) to match, but they differ: identity value: \[1 2 3 4\], state value: \[4 3 2 1\]`), + }, + }, + }) +} + +func examplecloudProviderWithResourceIdentityDifferentPaths() func() (tfprotov6.ProviderServer, error) { + return providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_thing": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + "state_id": tftypes.String, + "state_list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "test value"), + "state_id": tftypes.NewValue(tftypes.String, "id-123"), + "state_list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 1), + tftypes.NewValue(tftypes.Number, 2), + tftypes.NewValue(tftypes.Number, 3), + tftypes.NewValue(tftypes.Number, 4), + }, + ), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "identity_id": tftypes.String, + "identity_list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "identity_id": tftypes.NewValue(tftypes.String, "id-123"), + "identity_list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 1), + tftypes.NewValue(tftypes.Number, 2), + tftypes.NewValue(tftypes.Number, 3), + tftypes.NewValue(tftypes.Number, 4), + }, + ), + }, + )), + }, + ReadResponse: &resource.ReadResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + "state_id": tftypes.String, + "state_list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "test value"), + "state_id": tftypes.NewValue(tftypes.String, "id-123"), + "state_list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 1), + tftypes.NewValue(tftypes.Number, 2), + tftypes.NewValue(tftypes.Number, 3), + tftypes.NewValue(tftypes.Number, 4), + }, + ), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "identity_id": tftypes.String, + "identity_list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "identity_id": tftypes.NewValue(tftypes.String, "id-123"), + "identity_list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 1), + tftypes.NewValue(tftypes.Number, 2), + tftypes.NewValue(tftypes.Number, 3), + tftypes.NewValue(tftypes.Number, 4), + }, + ), + }, + )), + }, + IdentitySchemaResponse: &resource.IdentitySchemaResponse{ + Schema: &tfprotov6.ResourceIdentitySchema{ + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "identity_id", + Type: tftypes.String, + RequiredForImport: true, + }, + { + Name: "identity_list_of_numbers", + Type: tftypes.List{ElementType: tftypes.Number}, + OptionalForImport: true, + }, + }, + }, + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "name", + Type: tftypes.String, + Computed: true, + }, + { + Name: "state_id", + Type: tftypes.String, + Computed: true, + }, + { + Name: "state_list_of_numbers", + Type: tftypes.List{ElementType: tftypes.Number}, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }) +} diff --git a/statecheck/expect_identity_value_matches_state_example_test.go b/statecheck/expect_identity_value_matches_state_example_test.go new file mode 100644 index 000000000..df0dd546c --- /dev/null +++ b/statecheck/expect_identity_value_matches_state_example_test.go @@ -0,0 +1,38 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func ExampleExpectIdentityValueMatchesState() { + // A typical test would accept *testing.T as a function parameter, for instance `func TestSomething(t *testing.T) { ... }`. + t := &testing.T{} + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Resource identity support is only available in Terraform v1.12+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + // Provider definition omitted. Assuming "test_resource": + // - Has an identity schema with an "id" string attribute + // - Has a resource schema with an "id" string attribute + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + // The identity attribute and state attribute at "id" must match + statecheck.ExpectIdentityValueMatchesState("test_resource.one", tfjsonpath.New("id")), + }, + }, + }, + }) +} diff --git a/statecheck/expect_identity_value_matches_state_test.go b/statecheck/expect_identity_value_matches_state_test.go new file mode 100644 index 000000000..d3248e15b --- /dev/null +++ b/statecheck/expect_identity_value_matches_state_test.go @@ -0,0 +1,337 @@ +// 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" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestExpectIdentityValueMatchesState_CheckState_ResourceNotFound(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValueMatchesState( + "examplecloud_thing.two", + tfjsonpath.New("id"), + ), + }, + ExpectError: regexp.MustCompile("examplecloud_thing.two - Resource not found in state"), + }, + }, + }) +} + +func TestExpectIdentityValueMatchesState_CheckState_No_Terraform_Identity_Support(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + tfversion.SkipAbove(tfversion.Version1_11_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + // Resource support identity, but the Terraform versions running will not. + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValueMatchesState( + "examplecloud_thing.one", + tfjsonpath.New("id"), + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in state. Either the resource ` + + `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, + ), + }, + }, + }) +} + +func TestExpectIdentityValueMatchesState_CheckState_No_Identity(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + // Resource does not support identity + "examplecloud": examplecloudProviderNoIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValueMatchesState( + "examplecloud_thing.one", + tfjsonpath.New("id"), + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in state. Either the resource ` + + `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, + ), + }, + }, + }) +} + +func TestExpectIdentityValueMatchesState_CheckState_String_Matches(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValueMatchesState( + "examplecloud_thing.one", + tfjsonpath.New("id"), + ), + }, + }, + }, + }) +} + +func TestExpectIdentityValueMatchesState_CheckState_String_DoesntMatch(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithMismatchedResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValueMatchesState( + "examplecloud_thing.one", + tfjsonpath.New("id"), + ), + }, + ExpectError: regexp.MustCompile("expected identity and state value at path to match, but they differ: examplecloud_thing.one.id, identity value: id-123, state value: 321-di"), + }, + }, + }) +} + +func TestExpectIdentityValueMatchesState_CheckState_List(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValueMatchesState( + "examplecloud_thing.one", + tfjsonpath.New("list_of_numbers"), + ), + }, + }, + }, + }) +} + +func TestExpectIdentityValueMatchesState_CheckState_List_DoesntMatch(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithMismatchedResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValueMatchesState( + "examplecloud_thing.one", + tfjsonpath.New("list_of_numbers"), + ), + }, + ExpectError: regexp.MustCompile(`expected identity and state value at path to match, but they differ: examplecloud_thing.one.list_of_numbers, identity value: \[1 2 3 4\], state value: \[4 3 2 1\]`), + }, + }, + }) +} + +func examplecloudProviderWithMismatchedResourceIdentity() func() (tfprotov6.ProviderServer, error) { + return providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_thing": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + "id": tftypes.String, + "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "test value"), + "id": tftypes.NewValue(tftypes.String, "321-di"), // doesn't match identity -> id + "list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 4), // doesn't match identity -> list_of_numbers[0] + tftypes.NewValue(tftypes.Number, 3), // doesn't match identity -> list_of_numbers[1] + tftypes.NewValue(tftypes.Number, 2), // doesn't match identity -> list_of_numbers[2] + tftypes.NewValue(tftypes.Number, 1), // doesn't match identity -> list_of_numbers[3] + }, + ), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "id-123"), + "list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 1), + tftypes.NewValue(tftypes.Number, 2), + tftypes.NewValue(tftypes.Number, 3), + tftypes.NewValue(tftypes.Number, 4), + }, + ), + }, + )), + }, + ReadResponse: &resource.ReadResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + "id": tftypes.String, + "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "test value"), + "id": tftypes.NewValue(tftypes.String, "321-di"), // doesn't match identity -> id + "list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 4), // doesn't match identity -> list_of_numbers[0] + tftypes.NewValue(tftypes.Number, 3), // doesn't match identity -> list_of_numbers[1] + tftypes.NewValue(tftypes.Number, 2), // doesn't match identity -> list_of_numbers[2] + tftypes.NewValue(tftypes.Number, 1), // doesn't match identity -> list_of_numbers[3] + }, + ), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "id-123"), + "list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 1), + tftypes.NewValue(tftypes.Number, 2), + tftypes.NewValue(tftypes.Number, 3), + tftypes.NewValue(tftypes.Number, 4), + }, + ), + }, + )), + }, + IdentitySchemaResponse: &resource.IdentitySchemaResponse{ + Schema: &tfprotov6.ResourceIdentitySchema{ + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + RequiredForImport: true, + }, + { + Name: "list_of_numbers", + Type: tftypes.List{ElementType: tftypes.Number}, + OptionalForImport: true, + }, + }, + }, + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "name", + Type: tftypes.String, + Computed: true, + }, + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + { + Name: "list_of_numbers", + Type: tftypes.List{ElementType: tftypes.Number}, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }) +} diff --git a/statecheck/expect_identity_value_test.go b/statecheck/expect_identity_value_test.go new file mode 100644 index 000000000..8ee701272 --- /dev/null +++ b/statecheck/expect_identity_value_test.go @@ -0,0 +1,461 @@ +// 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" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestExpectIdentityValue_CheckState_ResourceNotFound(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValue( + "examplecloud_thing.two", + tfjsonpath.New("id"), + knownvalue.StringExact("id-123"), + ), + }, + ExpectError: regexp.MustCompile("examplecloud_thing.two - Resource not found in state"), + }, + }, + }) +} + +func TestExpectIdentityValue_CheckState_No_Terraform_Identity_Support(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + tfversion.SkipAbove(tfversion.Version1_11_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + // Resource support identity, but the Terraform versions running will not. + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("id"), + knownvalue.StringExact("id-123"), + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in state. Either the resource ` + + `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, + ), + }, + }, + }) +} + +func TestExpectIdentityValue_CheckState_No_Identity(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + // Resource does not support identity + "examplecloud": examplecloudProviderNoIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("id"), + knownvalue.StringExact("id-123"), + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in state. Either the resource ` + + `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, + ), + }, + }, + }) +} + +func TestExpectIdentityValue_CheckState_String(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("id"), + knownvalue.StringExact("id-123")), + }, + }, + }, + }) +} + +func TestExpectIdentityValue_CheckState_String_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("id"), + knownvalue.Bool(true)), + }, + ExpectError: regexp.MustCompile("expected bool value for Bool check, got: string"), + }, + }, + }) +} + +func TestExpectIdentityValue_CheckState_String_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("id"), + knownvalue.StringExact("321-id")), + }, + ExpectError: regexp.MustCompile("expected value 321-id for StringExact check, got: id-123"), + }, + }, + }) +} + +func TestExpectIdentityValue_CheckState_List(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("list_of_numbers").AtSliceIndex(0), + knownvalue.Int64Exact(1), + ), + statecheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("list_of_numbers").AtSliceIndex(1), + knownvalue.Int64Exact(2), + ), + statecheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("list_of_numbers").AtSliceIndex(2), + knownvalue.Int64Exact(3), + ), + statecheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("list_of_numbers").AtSliceIndex(3), + knownvalue.Int64Exact(4), + ), + }, + }, + }, + }) +} + +func TestExpectIdentityValue_CheckState_List_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {} + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("list_of_numbers"), + knownvalue.MapExact(map[string]knownvalue.Check{}), + ), + }, + ExpectError: regexp.MustCompile(`expected map\[string\]any value for MapExact check, got: \[\]interface {}`), + }, + }, + }) +} + +func TestExpectIdentityValue_CheckState_List_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("list_of_numbers"), + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.Int64Exact(4), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(1), + }), + ), + }, + ExpectError: regexp.MustCompile(`list element index 0: expected value 4 for Int64Exact check, got: 1`), + }, + }, + }) +} + +func examplecloudProviderWithResourceIdentity() func() (tfprotov6.ProviderServer, error) { + return providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_thing": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + "id": tftypes.String, + "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "test value"), + "id": tftypes.NewValue(tftypes.String, "id-123"), + "list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 1), + tftypes.NewValue(tftypes.Number, 2), + tftypes.NewValue(tftypes.Number, 3), + tftypes.NewValue(tftypes.Number, 4), + }, + ), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "id-123"), + "list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 1), + tftypes.NewValue(tftypes.Number, 2), + tftypes.NewValue(tftypes.Number, 3), + tftypes.NewValue(tftypes.Number, 4), + }, + ), + }, + )), + }, + ReadResponse: &resource.ReadResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + "id": tftypes.String, + "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "test value"), + "id": tftypes.NewValue(tftypes.String, "id-123"), + "list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 1), + tftypes.NewValue(tftypes.Number, 2), + tftypes.NewValue(tftypes.Number, 3), + tftypes.NewValue(tftypes.Number, 4), + }, + ), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "id-123"), + "list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 1), + tftypes.NewValue(tftypes.Number, 2), + tftypes.NewValue(tftypes.Number, 3), + tftypes.NewValue(tftypes.Number, 4), + }, + ), + }, + )), + }, + IdentitySchemaResponse: &resource.IdentitySchemaResponse{ + Schema: &tfprotov6.ResourceIdentitySchema{ + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + RequiredForImport: true, + }, + { + Name: "list_of_numbers", + Type: tftypes.List{ElementType: tftypes.Number}, + OptionalForImport: true, + }, + }, + }, + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "name", + Type: tftypes.String, + Computed: true, + }, + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + { + Name: "list_of_numbers", + Type: tftypes.List{ElementType: tftypes.Number}, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }) +} + +func examplecloudProviderNoIdentity() func() (tfprotov6.ProviderServer, error) { + return providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_thing": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "test value"), + }, + ), + }, + ReadResponse: &resource.ReadResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "test value"), + }, + ), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "name", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }) +} diff --git a/statecheck/expect_known_output_value.go b/statecheck/expect_known_output_value.go new file mode 100644 index 000000000..951a36400 --- /dev/null +++ b/statecheck/expect_known_output_value.go @@ -0,0 +1,76 @@ +// 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/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +// Resource State Check +var _ StateCheck = expectKnownOutputValue{} + +type expectKnownOutputValue struct { + outputAddress string + knownValue knownvalue.Check +} + +// CheckState implements the state check logic. +func (e expectKnownOutputValue) CheckState(ctx context.Context, req CheckStateRequest, resp *CheckStateResponse) { + var output *tfjson.StateOutput + + 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 + } + + for address, oc := range req.State.Values.Outputs { + if e.outputAddress == address { + output = oc + + break + } + } + + if output == nil { + resp.Error = fmt.Errorf("%s - Output not found in state", e.outputAddress) + + return + } + + result, err := tfjsonpath.Traverse(output.Value, tfjsonpath.Path{}) + + if err != nil { + resp.Error = err + + return + } + + if err := e.knownValue.CheckValue(result); err != nil { + resp.Error = fmt.Errorf("error checking value for output at path: %s, err: %s", e.outputAddress, err) + + return + } +} + +// ExpectKnownOutputValue returns a state check that asserts that the specified value +// has a known type, and value. +func ExpectKnownOutputValue(outputAddress string, knownValue knownvalue.Check) StateCheck { + return expectKnownOutputValue{ + outputAddress: outputAddress, + knownValue: knownValue, + } +} diff --git a/statecheck/expect_known_output_value_at_path.go b/statecheck/expect_known_output_value_at_path.go new file mode 100644 index 000000000..1cdfcea79 --- /dev/null +++ b/statecheck/expect_known_output_value_at_path.go @@ -0,0 +1,78 @@ +// 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/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +// Resource State Check +var _ StateCheck = expectKnownOutputValueAtPath{} + +type expectKnownOutputValueAtPath struct { + outputAddress string + outputPath tfjsonpath.Path + knownValue knownvalue.Check +} + +// CheckState implements the state check logic. +func (e expectKnownOutputValueAtPath) CheckState(ctx context.Context, req CheckStateRequest, resp *CheckStateResponse) { + var output *tfjson.StateOutput + + 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 + } + + for address, oc := range req.State.Values.Outputs { + if e.outputAddress == address { + output = oc + + break + } + } + + if output == nil { + resp.Error = fmt.Errorf("%s - Output not found in state", e.outputAddress) + + return + } + + result, err := tfjsonpath.Traverse(output.Value, e.outputPath) + + if err != nil { + resp.Error = err + + return + } + + if err := e.knownValue.CheckValue(result); err != nil { + resp.Error = fmt.Errorf("error checking value for output at path: %s.%s, err: %s", e.outputAddress, e.outputPath.String(), err) + + return + } +} + +// ExpectKnownOutputValueAtPath returns a state check that asserts that the specified output at the given path +// has a known type and value. +func ExpectKnownOutputValueAtPath(outputAddress string, outputPath tfjsonpath.Path, knownValue knownvalue.Check) StateCheck { + return expectKnownOutputValueAtPath{ + outputAddress: outputAddress, + outputPath: outputPath, + knownValue: knownValue, + } +} diff --git a/statecheck/expect_known_output_value_at_path_example_test.go b/statecheck/expect_known_output_value_at_path_example_test.go new file mode 100644 index 000000000..a6bb4bc12 --- /dev/null +++ b/statecheck/expect_known_output_value_at_path_example_test.go @@ -0,0 +1,42 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func ExampleExpectKnownOutputValueAtPath() { + // A typical test would accept *testing.T as a function parameter, for instance `func TestSomething(t *testing.T) { ... }`. + t := &testing.T{} + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("bool_attribute"), + knownvalue.Bool(true), + ), + }, + }, + }, + }) +} diff --git a/statecheck/expect_known_output_value_at_path_test.go b/statecheck/expect_known_output_value_at_path_test.go new file mode 100644 index 000000000..34e0bfe0b --- /dev/null +++ b/statecheck/expect_known_output_value_at_path_test.go @@ -0,0 +1,1628 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck_test + +import ( + "context" + "fmt" + "math/big" + "regexp" + "testing" + + "github.com/google/go-cmp/cmp" + tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestExpectKnownOutputValueAtPath_CheckState_ResourceNotFound(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_two_output", + tfjsonpath.New("bool_attribute"), + knownvalue.Bool(true), + ), + }, + ExpectError: regexp.MustCompile("test_resource_two_output - Output not found in state"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_AttributeValueNull(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" {} + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("bool_attribute"), + knownvalue.Null(), + ), + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("float_attribute"), + knownvalue.Null(), + ), + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("int_attribute"), + knownvalue.Null(), + ), + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_attribute"), + knownvalue.Null(), + ), + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_nested_block"), + knownvalue.ListExact([]knownvalue.Check{}), + ), + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("map_attribute"), + knownvalue.Null(), + ), + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_attribute"), + knownvalue.Null(), + ), + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_nested_block"), + knownvalue.SetExact([]knownvalue.Check{}), + ), + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("string_attribute"), + knownvalue.Null(), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_AttributeValueNotNull(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + float_attribute = 1.23 + int_attribute = 123 + list_attribute = ["value1", "value2"] + list_nested_block { + list_nested_block_attribute = "str" + } + map_attribute = { + key1 = "value1" + } + set_attribute = ["value1", "value2"] + set_nested_block { + set_nested_block_attribute = "str" + } + string_attribute = "str" + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("bool_attribute"), + knownvalue.NotNull(), + ), + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("float_attribute"), + knownvalue.NotNull(), + ), + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("int_attribute"), + knownvalue.NotNull(), + ), + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_attribute"), + knownvalue.NotNull(), + ), + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_nested_block"), + knownvalue.ListSizeExact(1), + ), + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("map_attribute"), + knownvalue.NotNull(), + ), + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_attribute"), + knownvalue.NotNull(), + ), + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_nested_block"), + knownvalue.SetSizeExact(1), + ), + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("string_attribute"), + knownvalue.NotNull(), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_Bool(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("bool_attribute"), + knownvalue.Bool(true), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_Bool_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("bool_attribute"), + knownvalue.Float64Exact(1.23), + ), + }, + ExpectError: regexp.MustCompile(`expected json\.Number value for Float64Exact check, got: bool`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_Bool_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("bool_attribute"), + knownvalue.Bool(false), + ), + }, + ExpectError: regexp.MustCompile("expected value false for Bool check, got: true"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_Float64(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + float_attribute = 1.23 + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("float_attribute"), + knownvalue.Float64Exact(1.23), + ), + }, + }, + }, + }) +} + +// We do not need equivalent tests for Int64 and Number as they all test the same logic. +func TestExpectKnownOutputValueAtPath_CheckState_Float64_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + float_attribute = 1.23 + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("float_attribute"), + knownvalue.StringExact("str"), + ), + }, + ExpectError: regexp.MustCompile(`expected string value for StringExact check, got: json\.Number`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_Float64_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + float_attribute = 1.23 + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("float_attribute"), + knownvalue.Float64Exact(3.21), + ), + }, + ExpectError: regexp.MustCompile("expected value 3.21 for Float64Exact check, got: 1.23"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_Int64(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("int_attribute"), + knownvalue.Int64Exact(123), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_Int64_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("int_attribute"), + knownvalue.Int64Exact(321), + ), + }, + ExpectError: regexp.MustCompile("expected value 321 for Int64Exact check, got: 123"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_List(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_attribute"), + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("value1"), + knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_List_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_attribute"), + knownvalue.MapExact(map[string]knownvalue.Check{}), + ), + }, + ExpectError: regexp.MustCompile(`expected map\[string\]any value for MapExact check, got: \[\]interface {}`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_List_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_attribute"), + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("value3"), + knownvalue.StringExact("value4"), + }), + ), + }, + ExpectError: regexp.MustCompile(`list element index 0: expected value value3 for StringExact check, got: value1`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_ListPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_attribute"), + knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.StringExact("value1"), + }), + ), + }, + }, + }, + }) +} + +// No need to check KnownValueWrongType for ListPartial as all lists, and sets are []any in +// tfjson.State. +func TestExpectKnownOutputValueAtPath_CheckState_ListPartial_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_attribute"), + knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.StringExact("value3"), + }), + ), + }, + ExpectError: regexp.MustCompile(`list element 0: expected value value3 for StringExact check, got: value1`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_ListElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_attribute"), + knownvalue.ListSizeExact(2), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_ListElements_WrongNum(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_attribute"), + knownvalue.ListSizeExact(3), + ), + }, + ExpectError: regexp.MustCompile("expected 3 elements for ListSizeExact check, got 2 elements"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_ListNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_nested_block"), + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("str"), + }), + knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_ListNestedBlockPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_nested_block"), + knownvalue.ListPartial(map[int]knownvalue.Check{ + 1: knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_ListNestedBlockElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_nested_block"), + knownvalue.ListSizeExact(2), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_Map(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("map_attribute"), + knownvalue.MapExact(map[string]knownvalue.Check{ + "key1": knownvalue.StringExact("value1"), + "key2": knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_Map_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("map_attribute"), + knownvalue.ListExact([]knownvalue.Check{}), + ), + }, + ExpectError: regexp.MustCompile(`expected \[\]any value for ListExact check, got: map\[string\]interface {}`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_Map_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("map_attribute"), + knownvalue.MapExact(map[string]knownvalue.Check{ + "key3": knownvalue.StringExact("value3"), + "key4": knownvalue.StringExact("value4"), + }), + ), + }, + ExpectError: regexp.MustCompile(`missing element key3 for MapExact check`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_MapPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("map_attribute"), + knownvalue.MapPartial(map[string]knownvalue.Check{ + "key1": knownvalue.StringExact("value1"), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_MapPartial_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("map_attribute"), + knownvalue.MapPartial(map[string]knownvalue.Check{ + "key3": knownvalue.StringExact("value1"), + }), + ), + }, + ExpectError: regexp.MustCompile(`missing element key3 for MapPartial check`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_MapElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("map_attribute"), + knownvalue.MapSizeExact(2), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_MapElements_WrongNum(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("map_attribute"), + knownvalue.MapSizeExact(3), + ), + }, + ExpectError: regexp.MustCompile("expected 3 elements for MapSizeExact check, got 2 elements"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_Number(t *testing.T) { + t.Parallel() + + f, _, err := big.ParseFloat("123", 10, 512, big.ToNearestEven) + + if err != nil { + t.Errorf("%s", err) + } + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("int_attribute"), + knownvalue.NumberExact(f), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_Number_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + f, _, err := big.ParseFloat("321", 10, 512, big.ToNearestEven) + + if err != nil { + t.Errorf("%s", err) + } + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("int_attribute"), + knownvalue.NumberExact(f), + ), + }, + ExpectError: regexp.MustCompile("expected value 321 for NumberExact check, got: 123"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_Set(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_attribute"), + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.StringExact("value1"), + knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_Set_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_attribute"), + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.StringExact("value1"), + knownvalue.StringExact("value3"), + }), + ), + }, + ExpectError: regexp.MustCompile(`missing value value3 for SetExact check`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_SetPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_attribute"), + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_SetPartial_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_attribute"), + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.StringExact("value3"), + }), + ), + }, + ExpectError: regexp.MustCompile(`missing value value3 for SetPartial check`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_SetElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_attribute"), + knownvalue.SetSizeExact(2), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_SetNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_nested_block"), + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("str"), + }), + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_SetNestedBlockPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_nested_block"), + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_SetNestedBlockElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_nested_block"), + knownvalue.SetSizeExact(2), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_String(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("string_attribute"), + knownvalue.StringExact("str")), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_String_Custom(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "string" + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("string_attribute"), + StringContains("str")), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_String_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("string_attribute"), + knownvalue.Bool(true)), + }, + ExpectError: regexp.MustCompile("expected bool value for Bool check, got: string"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_String_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("string_attribute"), + knownvalue.StringExact("rts")), + }, + ExpectError: regexp.MustCompile("expected value rts for StringExact check, got: str"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckState_UnknownAttributeType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + knownValue knownvalue.Check + req statecheck.CheckStateRequest + expectedErr error + }{ + "unrecognised-type": { + knownValue: knownvalue.Int64Exact(123), + req: statecheck.CheckStateRequest{ + State: &tfjson.State{ + Values: &tfjson.StateValues{ + Outputs: map[string]*tfjson.StateOutput{ + "obj": { + Value: map[string]any{ + "float32_output": float32(123), + }, + }, + }, + }, + }, + }, + expectedErr: fmt.Errorf("error checking value for output at path: obj.float32_output, err: expected json.Number value for Int64Exact check, got: float32"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + e := statecheck.ExpectKnownOutputValueAtPath("obj", tfjsonpath.New("float32_output"), testCase.knownValue) + + resp := statecheck.CheckStateResponse{} + + e.CheckState(context.Background(), testCase.req, &resp) + + if diff := cmp.Diff(resp.Error, testCase.expectedErr, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/statecheck/expect_known_output_value_example_test.go b/statecheck/expect_known_output_value_example_test.go new file mode 100644 index 000000000..2d3e3275e --- /dev/null +++ b/statecheck/expect_known_output_value_example_test.go @@ -0,0 +1,40 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" +) + +func ExampleExpectKnownOutputValue() { + // A typical test would accept *testing.T as a function parameter, for instance `func TestSomething(t *testing.T) { ... }`. + t := &testing.T{} + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + output bool_output { + value = test_resource.one.bool_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "bool_output", + knownvalue.Bool(true), + ), + }, + }, + }, + }) +} diff --git a/statecheck/expect_known_output_value_test.go b/statecheck/expect_known_output_value_test.go new file mode 100644 index 000000000..69c63dc1d --- /dev/null +++ b/statecheck/expect_known_output_value_test.go @@ -0,0 +1,1562 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck_test + +import ( + "context" + "fmt" + "math/big" + "regexp" + "testing" + + "github.com/google/go-cmp/cmp" + tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" +) + +func TestExpectKnownOutputValue_CheckState_OutputNotFound(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + output bool_output { + value = test_resource.one.bool_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "bool_not_found", + knownvalue.Bool(true), + ), + }, + ExpectError: regexp.MustCompile("bool_not_found - Output not found in state"), + }, + }, + }) +} + +// TestExpectKnownOutputValue_CheckState_AttributeValueNull shows that outputs that reference +// null values do not appear in state. Indicating that there is no way to discriminate +// between null outputs and non-existent outputs. +// Reference: https://github.com/hashicorp/terraform/issues/34080 +func TestExpectKnownOutputValue_CheckState_AttributeValueNull(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" {} + output bool_output { + value = test_resource.one.bool_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "bool_output", + knownvalue.Bool(true), + ), + }, + ExpectError: regexp.MustCompile("bool_output - Output not found in state"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_AttributeValueNotNull(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + float_attribute = 1.23 + int_attribute = 123 + list_attribute = ["value1", "value2"] + list_nested_block { + list_nested_block_attribute = "str" + } + map_attribute = { + key1 = "value1" + } + set_attribute = ["value1", "value2"] + set_nested_block { + set_nested_block_attribute = "str" + } + string_attribute = "str" + } + output bool_output { + value = test_resource.one.bool_attribute + } + output float64_output { + value = test_resource.one.float_attribute + } + output int64_output { + value = test_resource.one.int_attribute + } + output list_output { + value = test_resource.one.list_attribute + } + output list_nested_block_output { + value = test_resource.one.list_nested_block + } + output map_output { + value = test_resource.one.map_attribute + } + output set_output { + value = test_resource.one.set_attribute + } + output set_nested_block_output { + value = test_resource.one.set_nested_block + } + output string_output { + value = test_resource.one.string_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "bool_output", + knownvalue.NotNull(), + ), + statecheck.ExpectKnownOutputValue( + "float64_output", + knownvalue.NotNull(), + ), + statecheck.ExpectKnownOutputValue( + "int64_output", + knownvalue.NotNull(), + ), + statecheck.ExpectKnownOutputValue( + "list_output", + knownvalue.NotNull(), + ), + statecheck.ExpectKnownOutputValue( + "list_nested_block_output", + knownvalue.ListSizeExact(1), + ), + statecheck.ExpectKnownOutputValue( + "map_output", + knownvalue.NotNull(), + ), + statecheck.ExpectKnownOutputValue( + "set_output", + knownvalue.NotNull(), + ), + statecheck.ExpectKnownOutputValue( + "set_nested_block_output", + knownvalue.SetSizeExact(1), + ), + statecheck.ExpectKnownOutputValue( + "string_output", + knownvalue.NotNull(), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_Bool(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + output bool_output { + value = test_resource.one.bool_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "bool_output", + knownvalue.Bool(true), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_Bool_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + output bool_output { + value = test_resource.one.bool_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "bool_output", + knownvalue.Float64Exact(1.23), + ), + }, + ExpectError: regexp.MustCompile(`expected json\.Number value for Float64Exact check, got: bool`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_Bool_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + output bool_output { + value = test_resource.one.bool_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "bool_output", + knownvalue.Bool(false), + ), + }, + ExpectError: regexp.MustCompile("expected value false for Bool check, got: true"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_Float64(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + float_attribute = 1.23 + } + + output float64_output { + value = test_resource.one.float_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "float64_output", + knownvalue.Float64Exact(1.23), + ), + }, + }, + }, + }) +} + +// We do not need equivalent tests for Int64 and Number as they all test the same logic. +func TestExpectKnownOutputValue_CheckState_Float64_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + float_attribute = 1.23 + } + + output float64_output { + value = test_resource.one.float_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "float64_output", + knownvalue.StringExact("str"), + ), + }, + ExpectError: regexp.MustCompile(`expected string value for StringExact check, got: json\.Number`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_Float64_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + float_attribute = 1.23 + } + + output float64_output { + value = test_resource.one.float_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "float64_output", + knownvalue.Float64Exact(3.21), + ), + }, + ExpectError: regexp.MustCompile("expected value 3.21 for Float64Exact check, got: 1.23"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_Int64(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + + output int64_output { + value = test_resource.one.int_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "int64_output", + knownvalue.Int64Exact(123), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_Int64_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + + output int64_output { + value = test_resource.one.int_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "int64_output", + knownvalue.Int64Exact(321), + ), + }, + ExpectError: regexp.MustCompile("expected value 321 for Int64Exact check, got: 123"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_List(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output list_output { + value = test_resource.one.list_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "list_output", + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("value1"), + knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_List_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output list_output { + value = test_resource.one.list_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "list_output", + knownvalue.MapExact(map[string]knownvalue.Check{}), + ), + }, + ExpectError: regexp.MustCompile(`expected map\[string\]any value for MapExact check, got: \[\]interface {}`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_List_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output list_output { + value = test_resource.one.list_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "list_output", + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("value3"), + knownvalue.StringExact("value4"), + }), + ), + }, + ExpectError: regexp.MustCompile(`list element index 0: expected value value3 for StringExact check, got: value1`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_ListPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output list_output { + value = test_resource.one.list_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "list_output", + knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.StringExact("value1"), + }), + ), + }, + }, + }, + }) +} + +// No need to check KnownValueWrongType for ListPartial as all lists, and sets are []any in +// tfjson.State. +func TestExpectKnownOutputValue_CheckState_ListPartial_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output list_output { + value = test_resource.one.list_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "list_output", + knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.StringExact("value3"), + }), + ), + }, + ExpectError: regexp.MustCompile(`list element 0: expected value value3 for StringExact check, got: value1`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_ListElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output list_output { + value = test_resource.one.list_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "list_output", + knownvalue.ListSizeExact(2), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_ListElements_WrongNum(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output list_output { + value = test_resource.one.list_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "list_output", + knownvalue.ListSizeExact(3), + ), + }, + ExpectError: regexp.MustCompile("expected 3 elements for ListSizeExact check, got 2 elements"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_ListNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + + output list_nested_block_output { + value = test_resource.one.list_nested_block + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "list_nested_block_output", + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("str"), + }), + knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_ListNestedBlockPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + + output list_nested_block_output { + value = test_resource.one.list_nested_block + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "list_nested_block_output", + knownvalue.ListPartial(map[int]knownvalue.Check{ + 1: knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_ListNestedBlockElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + + output list_nested_block_output { + value = test_resource.one.list_nested_block + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "list_nested_block_output", + knownvalue.ListSizeExact(2), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_Map(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output map_output { + value = test_resource.one.map_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "map_output", + knownvalue.MapExact(map[string]knownvalue.Check{ + "key1": knownvalue.StringExact("value1"), + "key2": knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_Map_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output map_output { + value = test_resource.one.map_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "map_output", + knownvalue.ListExact([]knownvalue.Check{}), + ), + }, + ExpectError: regexp.MustCompile(`expected \[\]any value for ListExact check, got: map\[string\]interface {}`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_Map_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output map_output { + value = test_resource.one.map_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "map_output", + knownvalue.MapExact(map[string]knownvalue.Check{ + "key3": knownvalue.StringExact("value3"), + "key4": knownvalue.StringExact("value4"), + }), + ), + }, + ExpectError: regexp.MustCompile(`missing element key3 for MapExact check`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_MapPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output map_output { + value = test_resource.one.map_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "map_output", + knownvalue.MapPartial(map[string]knownvalue.Check{ + "key1": knownvalue.StringExact("value1"), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_MapPartial_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output map_output { + value = test_resource.one.map_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "map_output", + knownvalue.MapPartial(map[string]knownvalue.Check{ + "key3": knownvalue.StringExact("value1"), + }), + ), + }, + ExpectError: regexp.MustCompile(`missing element key3 for MapPartial check`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_MapElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output map_output { + value = test_resource.one.map_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "map_output", + knownvalue.MapSizeExact(2), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_MapElements_WrongNum(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output map_output { + value = test_resource.one.map_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "map_output", + knownvalue.MapSizeExact(3), + ), + }, + ExpectError: regexp.MustCompile("expected 3 elements for MapSizeExact check, got 2 elements"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_Number(t *testing.T) { + t.Parallel() + + f, _, err := big.ParseFloat("123", 10, 512, big.ToNearestEven) + + if err != nil { + t.Errorf("%s", err) + } + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + + output int64_output { + value = test_resource.one.int_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "int64_output", + knownvalue.NumberExact(f), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_Number_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + f, _, err := big.ParseFloat("321", 10, 512, big.ToNearestEven) + + if err != nil { + t.Errorf("%s", err) + } + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + + output int64_output { + value = test_resource.one.int_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "int64_output", + knownvalue.NumberExact(f), + ), + }, + ExpectError: regexp.MustCompile("expected value 321 for NumberExact check, got: 123"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_Set(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + + output set_output { + value = test_resource.one.set_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "set_output", + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.StringExact("value1"), + knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_Set_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + + output set_output { + value = test_resource.one.set_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "set_output", + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.StringExact("value1"), + knownvalue.StringExact("value3"), + }), + ), + }, + ExpectError: regexp.MustCompile(`missing value value3 for SetExact check`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_SetPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + + output set_output { + value = test_resource.one.set_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "set_output", + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_SetPartial_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + + output set_output { + value = test_resource.one.set_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "set_output", + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.StringExact("value3"), + }), + ), + }, + ExpectError: regexp.MustCompile(`missing value value3 for SetPartial check`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_SetElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + + output set_output { + value = test_resource.one.set_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "set_output", + knownvalue.SetSizeExact(2), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_SetNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + + output set_nested_block_output { + value = test_resource.one.set_nested_block + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "set_nested_block_output", + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("str"), + }), + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_SetNestedBlockPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + + output set_nested_block_output { + value = test_resource.one.set_nested_block + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "set_nested_block_output", + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_SetNestedBlockElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + + output set_nested_block_output { + value = test_resource.one.set_nested_block + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "set_nested_block_output", + knownvalue.SetSizeExact(2), + ), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_String(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + output string_output { + value = test_resource.one.string_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "string_output", + knownvalue.StringExact("str")), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_String_Custom(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "string" + } + + output string_output { + value = test_resource.one.string_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "string_output", + StringContains("str")), + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_String_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + output string_output { + value = test_resource.one.string_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "string_output", + knownvalue.Bool(true)), + }, + ExpectError: regexp.MustCompile("expected bool value for Bool check, got: string"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_String_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + output string_output { + value = test_resource.one.string_attribute + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "string_output", + knownvalue.StringExact("rts")), + }, + ExpectError: regexp.MustCompile("expected value rts for StringExact check, got: str"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckState_UnknownAttributeType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + knownValue knownvalue.Check + req statecheck.CheckStateRequest + expectedErr error + }{ + "unrecognised-type": { + knownValue: knownvalue.Int64Exact(123), + req: statecheck.CheckStateRequest{ + State: &tfjson.State{ + Values: &tfjson.StateValues{ + Outputs: map[string]*tfjson.StateOutput{ + "float32_output": { + Value: float32(123), + }, + }, + }, + }, + }, + expectedErr: fmt.Errorf("error checking value for output at path: float32_output, err: expected json.Number value for Int64Exact check, got: float32"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + e := statecheck.ExpectKnownOutputValue("float32_output", testCase.knownValue) + + resp := statecheck.CheckStateResponse{} + + e.CheckState(context.Background(), testCase.req, &resp) + + if diff := cmp.Diff(resp.Error, testCase.expectedErr, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/statecheck/expect_known_value.go b/statecheck/expect_known_value.go new file mode 100644 index 000000000..aca58c4d2 --- /dev/null +++ b/statecheck/expect_known_value.go @@ -0,0 +1,84 @@ +// 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/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +// Resource State Check +var _ StateCheck = expectKnownValue{} + +type expectKnownValue struct { + resourceAddress string + attributePath tfjsonpath.Path + knownValue knownvalue.Check +} + +// CheckState implements the state check logic. +func (e expectKnownValue) 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 + } + + for _, r := range req.State.Values.RootModule.Resources { + if e.resourceAddress == r.Address { + resource = r + + break + } + } + + if resource == nil { + resp.Error = fmt.Errorf("%s - Resource not found in state", e.resourceAddress) + + return + } + + result, err := tfjsonpath.Traverse(resource.AttributeValues, e.attributePath) + + if err != nil { + resp.Error = err + + return + } + + if err := e.knownValue.CheckValue(result); err != nil { + resp.Error = fmt.Errorf("error checking value for attribute at path: %s.%s, err: %s", e.resourceAddress, e.attributePath.String(), err) + + return + } +} + +// ExpectKnownValue returns a state check that asserts that the specified attribute at the given resource +// has a known type and value. +func ExpectKnownValue(resourceAddress string, attributePath tfjsonpath.Path, knownValue knownvalue.Check) StateCheck { + return expectKnownValue{ + resourceAddress: resourceAddress, + attributePath: attributePath, + knownValue: knownValue, + } +} diff --git a/statecheck/expect_known_value_example_test.go b/statecheck/expect_known_value_example_test.go new file mode 100644 index 000000000..ef26d64c3 --- /dev/null +++ b/statecheck/expect_known_value_example_test.go @@ -0,0 +1,38 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func ExampleExpectKnownValue() { + // A typical test would accept *testing.T as a function parameter, for instance `func TestSomething(t *testing.T) { ... }`. + t := &testing.T{} + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + knownvalue.Bool(true), + ), + }, + }, + }, + }) +} diff --git a/statecheck/expect_known_value_test.go b/statecheck/expect_known_value_test.go new file mode 100644 index 000000000..18cc07901 --- /dev/null +++ b/statecheck/expect_known_value_test.go @@ -0,0 +1,1655 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck_test + +import ( + "context" + "fmt" + "math/big" + "regexp" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestExpectKnownValue_CheckState_ResourceNotFound(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.two", + tfjsonpath.New("bool_attribute"), + knownvalue.Bool(true), + ), + }, + ExpectError: regexp.MustCompile("test_resource.two - Resource not found in state"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_AttributeValueNull(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + knownvalue.Null(), + ), + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("float_attribute"), + knownvalue.Null(), + ), + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("int_attribute"), + knownvalue.Null(), + ), + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.Null(), + ), + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_nested_block"), + knownvalue.ListExact([]knownvalue.Check{}), + ), + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.Null(), + ), + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_attribute"), + knownvalue.Null(), + ), + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_nested_block"), + knownvalue.SetExact([]knownvalue.Check{}), + ), + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("string_attribute"), + knownvalue.Null(), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_AttributeValueNotNull(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + float_attribute = 1.23 + int_attribute = 123 + list_attribute = ["value1", "value2"] + list_nested_block { + list_nested_block_attribute = "str" + } + map_attribute = { + key1 = "value1" + } + set_attribute = ["value1", "value2"] + set_nested_block { + set_nested_block_attribute = "str" + } + string_attribute = "str" + }`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + knownvalue.NotNull(), + ), + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("float_attribute"), + knownvalue.NotNull(), + ), + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("int_attribute"), + knownvalue.NotNull(), + ), + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.NotNull(), + ), + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_nested_block"), + knownvalue.ListSizeExact(1), + ), + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.NotNull(), + ), + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_attribute"), + knownvalue.NotNull(), + ), + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_nested_block"), + knownvalue.SetSizeExact(1), + ), + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("string_attribute"), + knownvalue.NotNull(), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_Bool(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + knownvalue.Bool(true), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_Bool_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + knownvalue.Float64Exact(1.23), + ), + }, + ExpectError: regexp.MustCompile(`expected json\.Number value for Float64Exact check, got: bool`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_Bool_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + knownvalue.Bool(false), + ), + }, + ExpectError: regexp.MustCompile("expected value false for Bool check, got: true"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_Float64(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + float_attribute = 1.23 + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("float_attribute"), + knownvalue.Float64Exact(1.23), + ), + }, + }, + }, + }) +} + +// We do not need equivalent tests for Int64 and Number as they all test the same logic. +func TestExpectKnownValue_CheckState_Float64_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + float_attribute = 1.23 + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("float_attribute"), + knownvalue.StringExact("str"), + ), + }, + ExpectError: regexp.MustCompile(`expected string value for StringExact check, got: json\.Number`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_Float64_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + float_attribute = 1.23 + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("float_attribute"), + knownvalue.Float64Exact(3.21), + ), + }, + ExpectError: regexp.MustCompile("expected value 3.21 for Float64Exact check, got: 1.23"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_Int64(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("int_attribute"), + knownvalue.Int64Exact(123), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_Int64_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("int_attribute"), + knownvalue.Int64Exact(321), + ), + }, + ExpectError: regexp.MustCompile("expected value 321 for Int64Exact check, got: 123"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_List(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("value1"), + knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_List_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.MapExact(map[string]knownvalue.Check{}), + ), + }, + ExpectError: regexp.MustCompile(`expected map\[string\]any value for MapExact check, got: \[\]interface {}`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_List_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("value3"), + knownvalue.StringExact("value4"), + }), + ), + }, + ExpectError: regexp.MustCompile(`list element index 0: expected value value3 for StringExact check, got: value1`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_ListPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.StringExact("value1"), + }), + ), + }, + }, + }, + }) +} + +// No need to check KnownValueWrongType for ListPartial as all lists, and sets are []any in +// tfjson.State. +func TestExpectKnownValue_CheckState_ListPartial_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.StringExact("value3"), + }), + ), + }, + ExpectError: regexp.MustCompile(`list element 0: expected value value3 for StringExact check, got: value1`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_ListElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.ListSizeExact(2), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_ListElements_WrongNum(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.ListSizeExact(3), + ), + }, + ExpectError: regexp.MustCompile("expected 3 elements for ListSizeExact check, got 2 elements"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_ListNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_nested_block"), + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("str"), + }), + knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_ListNestedBlockPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_nested_block"), + knownvalue.ListPartial(map[int]knownvalue.Check{ + 1: knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_ListNestedBlockElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_nested_block"), + knownvalue.ListSizeExact(2), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_Map(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.MapExact(map[string]knownvalue.Check{ + "key1": knownvalue.StringExact("value1"), + "key2": knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_Map_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.ListExact([]knownvalue.Check{}), + ), + }, + ExpectError: regexp.MustCompile(`expected \[\]any value for ListExact check, got: map\[string\]interface {}`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_Map_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.MapExact(map[string]knownvalue.Check{ + "key3": knownvalue.StringExact("value3"), + "key4": knownvalue.StringExact("value4"), + }), + ), + }, + ExpectError: regexp.MustCompile(`missing element key3 for MapExact check`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_MapPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.MapPartial(map[string]knownvalue.Check{ + "key1": knownvalue.StringExact("value1"), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_MapPartial_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.MapPartial(map[string]knownvalue.Check{ + "key3": knownvalue.StringExact("value1"), + }), + ), + }, + ExpectError: regexp.MustCompile(`missing element key3 for MapPartial check`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_MapElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.MapSizeExact(2), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_MapElements_WrongNum(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.MapSizeExact(3), + ), + }, + ExpectError: regexp.MustCompile("expected 3 elements for MapSizeExact check, got 2 elements"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_Number(t *testing.T) { + t.Parallel() + + f, _, err := big.ParseFloat("123", 10, 512, big.ToNearestEven) + + if err != nil { + t.Errorf("%s", err) + } + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("int_attribute"), + knownvalue.NumberExact(f), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_Number_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + f, _, err := big.ParseFloat("321", 10, 512, big.ToNearestEven) + + if err != nil { + t.Errorf("%s", err) + } + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("int_attribute"), + knownvalue.NumberExact(f), + ), + }, + ExpectError: regexp.MustCompile("expected value 321 for NumberExact check, got: 123"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_Set(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_attribute"), + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.StringExact("value2"), + knownvalue.StringExact("value1"), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_Set_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_attribute"), + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.StringExact("value1"), + knownvalue.StringExact("value3"), + }), + ), + }, + ExpectError: regexp.MustCompile(`missing value value3 for SetExact check`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_SetPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_attribute"), + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_SetPartial_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_attribute"), + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.StringExact("value3"), + }), + ), + }, + ExpectError: regexp.MustCompile(`missing value value3 for SetPartial check`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_SetElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_attribute"), + knownvalue.SetSizeExact(2), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_SetNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_nested_block"), + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("str"), + }), + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_SetNestedBlock_Custom(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_block { + set_nested_block_attribute = "string" + } + set_nested_block { + set_nested_block_attribute = "girts" + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_nested_block"), + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": StringContains("str"), + }), + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": StringContains("rts"), + }), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_SetNestedBlockPartial(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_nested_block"), + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_SetNestedBlockElements(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_nested_block"), + knownvalue.SetSizeExact(2), + ), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_String(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("string_attribute"), + knownvalue.StringExact("str")), + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_String_Custom(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "string" + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("string_attribute"), + StringContains("tri")), + }, + }, + }, + }) +} + +var _ knownvalue.Check = stringContains{} + +type stringContains struct { + value string +} + +func (v stringContains) CheckValue(other any) error { + otherVal, ok := other.(string) + + if !ok { + return fmt.Errorf("expected string value for StringContains check, got: %T", other) + } + + if !strings.Contains(otherVal, v.value) { + return fmt.Errorf("expected string %q to contain %q for StringContains check", otherVal, v.value) + } + + return nil +} + +func (v stringContains) String() string { + return v.value +} + +func StringContains(value string) stringContains { + return stringContains{ + value: value, + } +} + +func TestExpectKnownValue_CheckState_String_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("string_attribute"), + knownvalue.Bool(true)), + }, + ExpectError: regexp.MustCompile("expected bool value for Bool check, got: string"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_String_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("string_attribute"), + knownvalue.StringExact("rts")), + }, + ExpectError: regexp.MustCompile("expected value rts for StringExact check, got: str"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckState_UnknownAttributeType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + knownValue knownvalue.Check + req statecheck.CheckStateRequest + expectedErr error + }{ + "unrecognised-type": { + knownValue: knownvalue.Int64Exact(123), + req: statecheck.CheckStateRequest{ + State: &tfjson.State{ + Values: &tfjson.StateValues{ + RootModule: &tfjson.StateModule{ + Resources: []*tfjson.StateResource{ + { + Address: "example_resource.test", + AttributeValues: map[string]any{ + "attribute": float32(123), + }, + }, + }, + }, + }, + }, + }, + expectedErr: fmt.Errorf("error checking value for attribute at path: example_resource.test.attribute, err: expected json.Number value for Int64Exact check, got: float32"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + e := statecheck.ExpectKnownValue("example_resource.test", tfjsonpath.New("attribute"), testCase.knownValue) + + resp := statecheck.CheckStateResponse{} + + e.CheckState(context.Background(), testCase.req, &resp) + + if diff := cmp.Diff(resp.Error, testCase.expectedErr, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +var equateErrorMessage = cmp.Comparer(func(x, y error) bool { + if x == nil || y == nil { + return x == nil && y == nil + } + + return x.Error() == y.Error() +}) + +func testProvider() *schema.Provider { + return &schema.Provider{ + ResourcesMap: map[string]*schema.Resource{ + "test_resource": { + CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + d.SetId("test") + + err := d.Set("string_computed_attribute", "computed") + if err != nil { + return diag.Errorf("error setting string_computed_attribute: %s", err) + } + + return nil + }, + UpdateContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + Schema: map[string]*schema.Schema{ + "bool_attribute": { + Optional: true, + Type: schema.TypeBool, + }, + "float_attribute": { + Optional: true, + Type: schema.TypeFloat, + }, + "int_attribute": { + Optional: true, + Type: schema.TypeInt, + }, + "list_attribute": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + }, + "list_nested_block": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "list_nested_block_attribute": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "map_attribute": { + Type: schema.TypeMap, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + }, + "set_attribute": { + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + }, + "set_nested_block": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "set_nested_block_attribute": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "set_nested_nested_block": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "set_nested_block": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "set_nested_block_attribute": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + "string_attribute": { + Optional: true, + Type: schema.TypeString, + }, + "string_computed_attribute": { + Computed: true, + Type: schema.TypeString, + }, + }, + }, + }, + } +} diff --git a/statecheck/expect_sensitive_value.go b/statecheck/expect_sensitive_value.go new file mode 100644 index 000000000..ea4671119 --- /dev/null +++ b/statecheck/expect_sensitive_value.go @@ -0,0 +1,101 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck + +import ( + "context" + "encoding/json" + "fmt" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +var _ StateCheck = expectSensitiveValue{} + +type expectSensitiveValue struct { + resourceAddress string + attributePath tfjsonpath.Path +} + +// CheckState implements the state check logic. +func (e expectSensitiveValue) 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 + } + + for _, r := range req.State.Values.RootModule.Resources { + if e.resourceAddress == r.Address { + resource = r + + break + } + } + + if resource == nil { + resp.Error = fmt.Errorf("%s - Resource not found in state", e.resourceAddress) + + return + } + + var data map[string]any + + err := json.Unmarshal(resource.SensitiveValues, &data) + + if err != nil { + resp.Error = fmt.Errorf("could not unmarshal SensitiveValues: %s", err) + + return + } + + result, err := tfjsonpath.Traverse(data, e.attributePath) + + if err != nil { + resp.Error = err + + return + } + + isSensitive, ok := result.(bool) + if !ok { + resp.Error = fmt.Errorf("invalid path: the path value cannot be asserted as bool") + + return + } + + if !isSensitive { + resp.Error = fmt.Errorf("attribute at path is not sensitive") + + return + } +} + +// ExpectSensitiveValue returns a state check that asserts that the specified attribute at the given resource has a sensitive value. +// +// Due to implementation differences between the terraform-plugin-sdk and the terraform-plugin-framework, representation of sensitive +// values may differ. For example, terraform-plugin-sdk based providers may have less precise representations of sensitive values, such +// as marking whole maps as sensitive rather than individual element values. +func ExpectSensitiveValue(resourceAddress string, attributePath tfjsonpath.Path) StateCheck { + return expectSensitiveValue{ + resourceAddress: resourceAddress, + attributePath: attributePath, + } +} diff --git a/statecheck/expect_sensitive_value_test.go b/statecheck/expect_sensitive_value_test.go new file mode 100644 index 000000000..55a44840a --- /dev/null +++ b/statecheck/expect_sensitive_value_test.go @@ -0,0 +1,308 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck_test + +import ( + "context" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func Test_ExpectSensitiveValue_SensitiveStringAttribute(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_4_6), // StateResource.SensitiveValues + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProviderSensitive(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "one" { + sensitive_string_attribute = "test" + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectSensitiveValue("test_resource.one", + tfjsonpath.New("sensitive_string_attribute")), + }, + }, + }, + }) +} + +func Test_ExpectSensitiveValue_SensitiveListAttribute(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_4_6), // StateResource.SensitiveValues + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProviderSensitive(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "one" { + sensitive_list_attribute = ["value1"] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectSensitiveValue("test_resource.one", + tfjsonpath.New("sensitive_list_attribute")), + }, + }, + }, + }) +} + +func Test_ExpectSensitiveValue_SensitiveSetAttribute(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_4_6), // StateResource.SensitiveValues + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProviderSensitive(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "one" { + sensitive_set_attribute = ["value1"] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectSensitiveValue("test_resource.one", + tfjsonpath.New("sensitive_set_attribute")), + }, + }, + }, + }) +} + +func Test_ExpectSensitiveValue_SensitiveMapAttribute(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_4_6), // StateResource.SensitiveValues + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProviderSensitive(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "one" { + sensitive_map_attribute = { + key1 = "value1", + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectSensitiveValue("test_resource.one", + tfjsonpath.New("sensitive_map_attribute")), + }, + }, + }, + }) +} + +func Test_ExpectSensitiveValue_ListNestedBlock_SensitiveAttribute(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_4_6), // StateResource.SensitiveValues + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProviderSensitive(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "one" { + list_nested_block_sensitive_attribute { + sensitive_list_nested_block_attribute = "sensitive-test" + list_nested_block_attribute = "test" + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectSensitiveValue("test_resource.one", + tfjsonpath.New("list_nested_block_sensitive_attribute").AtSliceIndex(0). + AtMapKey("sensitive_list_nested_block_attribute")), + }, + }, + }, + }) +} + +func Test_ExpectSensitiveValue_SetNestedBlock_SensitiveAttribute(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_4_6), // StateResource.SensitiveValues + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProviderSensitive(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "one" { + set_nested_block_sensitive_attribute { + sensitive_set_nested_block_attribute = "sensitive-test" + set_nested_block_attribute = "test" + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectSensitiveValue("test_resource.one", + tfjsonpath.New("set_nested_block_sensitive_attribute")), + }, + }, + }, + }) +} + +func Test_ExpectSensitiveValue_ExpectError_ResourceNotFound(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_4_6), // StateResource.SensitiveValues + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProviderSensitive(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource" "one" {} + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectSensitiveValue("test_resource.two", tfjsonpath.New("set_attribute")), + }, + ExpectError: regexp.MustCompile(`test_resource.two - Resource not found in state`), + }, + }, + }) +} + +func testProviderSensitive() *schema.Provider { + return &schema.Provider{ + ResourcesMap: map[string]*schema.Resource{ + "test_resource": { + CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + d.SetId("test") + return nil + }, + UpdateContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + Schema: map[string]*schema.Schema{ + "sensitive_string_attribute": { + Sensitive: true, + Optional: true, + Type: schema.TypeString, + }, + "sensitive_list_attribute": { + Sensitive: true, + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + }, + "sensitive_set_attribute": { + Sensitive: true, + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + }, + "sensitive_map_attribute": { + Sensitive: true, + Type: schema.TypeMap, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + }, + "list_nested_block_sensitive_attribute": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "list_nested_block_attribute": { + Type: schema.TypeString, + Optional: true, + }, + "sensitive_list_nested_block_attribute": { + Sensitive: true, + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "set_nested_block_sensitive_attribute": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "set_nested_block_attribute": { + Type: schema.TypeString, + Optional: true, + }, + "sensitive_set_nested_block_attribute": { + Sensitive: true, + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + } +} diff --git a/statecheck/state_check.go b/statecheck/state_check.go new file mode 100644 index 000000000..cfd2da6b1 --- /dev/null +++ b/statecheck/state_check.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck + +import ( + "context" + + tfjson "github.com/hashicorp/terraform-json" +) + +// StateCheck defines an interface for implementing test logic that checks a state file and then returns an error +// if the state file does not match what is expected. +type StateCheck interface { + // CheckState should perform the state check. + CheckState(context.Context, CheckStateRequest, *CheckStateResponse) +} + +// CheckStateRequest is a request for an invoke of the CheckState function. +type CheckStateRequest struct { + // State represents a parsed state file, retrieved via the `terraform show -json` command. + State *tfjson.State +} + +// CheckStateResponse is a response to an invoke of the CheckState function. +type CheckStateResponse struct { + // Error is used to report the failure of a state check assertion and is combined with other StateCheck errors + // to be reported as a test failure. + Error error +} diff --git a/terraform/diff.go b/terraform/diff.go index 87f73d568..f70e46e4d 100644 --- a/terraform/diff.go +++ b/terraform/diff.go @@ -35,6 +35,10 @@ const ( var multiVal = regexp.MustCompile(`\.(#|%)$`) // InstanceDiff is the diff of a resource from some state to another. +// +// Deprecated: This type is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. type InstanceDiff struct { mu sync.Mutex Attributes map[string]*ResourceAttrDiff @@ -53,7 +57,14 @@ type InstanceDiff struct { Meta map[string]interface{} } -func (d *InstanceDiff) Lock() { d.mu.Lock() } +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. +func (d *InstanceDiff) Lock() { d.mu.Lock() } + +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (d *InstanceDiff) Unlock() { d.mu.Unlock() } // ApplyToValue merges the receiver into the given base value, returning a @@ -62,6 +73,10 @@ func (d *InstanceDiff) Unlock() { d.mu.Unlock() } // // This method is intended for shimming old subsystems that still use this // legacy diff type to work with the new-style types. +// +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (d *InstanceDiff) ApplyToValue(base cty.Value, schema *configschema.Block) (cty.Value, error) { // Create an InstanceState attributes from our existing state. // We can use this to more easily apply the diff changes. @@ -84,6 +99,10 @@ func (d *InstanceDiff) ApplyToValue(base cty.Value, schema *configschema.Block) // // This method is intended for shimming old subsystems that still use this // legacy diff type to work with the new-style types. +// +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (d *InstanceDiff) Apply(attrs map[string]string, schema *configschema.Block) (map[string]string, error) { // We always build a new value here, even if the given diff is "empty", // because we might be planning to create a new instance that happens @@ -604,6 +623,10 @@ func countFlatmapContainerValues(key string, attrs map[string]string) string { } // ResourceAttrDiff is the diff of a single attribute of a resource. +// +// Deprecated: This type is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. type ResourceAttrDiff struct { Old string // Old Value New string // New Value @@ -626,12 +649,19 @@ func (d *ResourceAttrDiff) GoString() string { // "private_ip". type diffAttrType byte +// Deprecated: This function is unintentionally exported by this Go module and +// not supported for external consumption. It will be removed in the next major +// version. func NewInstanceDiff() *InstanceDiff { return &InstanceDiff{Attributes: make(map[string]*ResourceAttrDiff)} } // ChangeType returns the diffChangeType represented by the diff // for this single instance. +// +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (d *InstanceDiff) ChangeType() diffChangeType { if d.Empty() { return diffNone @@ -653,6 +683,10 @@ func (d *InstanceDiff) ChangeType() diffChangeType { } // Empty returns true if this diff encapsulates no changes. +// +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (d *InstanceDiff) Empty() bool { if d == nil { return true @@ -693,6 +727,10 @@ func (d *InstanceDiff) GoString() string { // RequiresNew returns true if the diff requires the creation of a new // resource (implying the destruction of the old). +// +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (d *InstanceDiff) RequiresNew() bool { if d == nil { return false @@ -722,6 +760,9 @@ func (d *InstanceDiff) requiresNew() bool { return false } +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (d *InstanceDiff) GetDestroyDeposed() bool { d.mu.Lock() defer d.mu.Unlock() @@ -729,6 +770,9 @@ func (d *InstanceDiff) GetDestroyDeposed() bool { return d.DestroyDeposed } +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (d *InstanceDiff) GetDestroyTainted() bool { d.mu.Lock() defer d.mu.Unlock() @@ -736,6 +780,9 @@ func (d *InstanceDiff) GetDestroyTainted() bool { return d.DestroyTainted } +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (d *InstanceDiff) GetDestroy() bool { d.mu.Lock() defer d.mu.Unlock() @@ -743,6 +790,9 @@ func (d *InstanceDiff) GetDestroy() bool { return d.Destroy } +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (d *InstanceDiff) GetAttribute(key string) (*ResourceAttrDiff, bool) { d.mu.Lock() defer d.mu.Unlock() @@ -752,6 +802,10 @@ func (d *InstanceDiff) GetAttribute(key string) (*ResourceAttrDiff, bool) { } // Safely copies the Attributes map +// +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (d *InstanceDiff) CopyAttributes() map[string]*ResourceAttrDiff { d.mu.Lock() defer d.mu.Unlock() @@ -768,6 +822,10 @@ func (d *InstanceDiff) CopyAttributes() map[string]*ResourceAttrDiff { // we say "same", it is not necessarily exactly equal. Instead, it is // just checking that the same attributes are changing, a destroy // isn't suddenly happening, etc. +// +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (d *InstanceDiff) Same(d2 *InstanceDiff) (bool, string) { // we can safely compare the pointers without a lock switch { diff --git a/terraform/diff_test.go b/terraform/diff_test.go index df75bedc2..325b208f6 100644 --- a/terraform/diff_test.go +++ b/terraform/diff_test.go @@ -820,8 +820,6 @@ func TestInstanceDiffSame(t *testing.T) { } for i, tc := range cases { - i, tc := i, tc - t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { t.Parallel() @@ -867,8 +865,6 @@ func TestCountFlatmapContainerValues(t *testing.T) { count: "2", }, } { - i, tc := i, tc - t.Run(strconv.Itoa(i), func(t *testing.T) { t.Parallel() diff --git a/terraform/resource.go b/terraform/resource.go index 30ace1ceb..e5887b536 100644 --- a/terraform/resource.go +++ b/terraform/resource.go @@ -11,8 +11,6 @@ import ( "strings" "github.com/hashicorp/go-cty/cty" - "github.com/mitchellh/copystructure" - "github.com/mitchellh/reflectwalk" "github.com/hashicorp/terraform-plugin-testing/internal/configs/configschema" "github.com/hashicorp/terraform-plugin-testing/internal/configs/hcl2shim" @@ -20,6 +18,10 @@ import ( // InstanceInfo is used to hold information about the instance and/or // resource being modified. +// +// Deprecated: This type is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. type InstanceInfo struct { // Id is a unique name to represent this instance. This is not related // to InstanceState.ID in any way. @@ -36,6 +38,10 @@ type InstanceInfo struct { // ResourceConfig is a legacy type that was formerly used to represent // interpolatable configuration blocks. It is now only used to shim to old // APIs that still use this type, via NewResourceConfigShimmed. +// +// Deprecated: This type is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. type ResourceConfig struct { ComputedKeys []string Raw map[string]interface{} @@ -48,6 +54,10 @@ type ResourceConfig struct { // The given value may contain hcl2shim.UnknownVariableValue to signal that // something is computed, but it must not contain unprocessed interpolation // sequences as we might've seen in Terraform v0.11 and prior. +// +// Deprecated: This function is unintentionally exported by this Go module and +// not supported for external consumption. It will be removed in the next major +// version. Use real Terraform configuration instead. func NewResourceConfigRaw(raw map[string]interface{}) *ResourceConfig { v := hcl2shim.HCL2ValueFromConfigValue(raw) @@ -79,6 +89,10 @@ func NewResourceConfigRaw(raw map[string]interface{}) *ResourceConfig { // // If the given value is not of an object type that conforms to the given // schema then this function will panic. +// +// Deprecated: This function is unintentionally exported by this Go module and +// not supported for external consumption. It will be removed in the next major +// version. func NewResourceConfigShimmed(val cty.Value, schema *configschema.Block) *ResourceConfig { if !val.Type().IsObjectType() { panic(fmt.Errorf("NewResourceConfigShimmed given %#v; an object type is required", val.Type())) @@ -151,25 +165,41 @@ func newResourceConfigShimmedComputedKeys(val cty.Value, path string) []string { // DeepCopy performs a deep copy of the configuration. This makes it safe // to modify any of the structures that are part of the resource config without // affecting the original configuration. +// +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (c *ResourceConfig) DeepCopy() *ResourceConfig { // DeepCopying a nil should return a nil to avoid panics if c == nil { return nil } - // Copy, this will copy all the exported attributes - copiedConfig, err := copystructure.Config{Lock: true}.Copy(c) - if err != nil { - panic(err) + copied := &ResourceConfig{} + + if c.ComputedKeys != nil { + copied.ComputedKeys = make([]string, len(c.ComputedKeys)) + + copy(copied.ComputedKeys, c.ComputedKeys) } - // Force the type - result, ok := copiedConfig.(*ResourceConfig) - if !ok { - panic(fmt.Errorf("unexpected type %T for copiedConfig", copiedConfig)) + if c.Config != nil { + copied.Config = make(map[string]any, len(c.Config)) + + for key, value := range c.Config { + copied.Config[key] = value + } } - return result + if c.Raw != nil { + copied.Raw = make(map[string]any, len(c.Raw)) + + for key, value := range c.Raw { + copied.Raw[key] = value + } + } + + return copied } // Equal checks the equality of two resource configs. @@ -206,6 +236,10 @@ func (c *ResourceConfig) Equal(c2 *ResourceConfig) bool { // The second return value is true if the get was successful. Get will // return the raw value if the key is computed, so you should pair this // with IsComputed. +// +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (c *ResourceConfig) Get(k string) (interface{}, bool) { // We aim to get a value from the configuration. If it is computed, // then we return the pure raw value. @@ -222,11 +256,19 @@ func (c *ResourceConfig) Get(k string) (interface{}, bool) { // // The second return value is true if the get was successful. Get will // not succeed if the value is being computed. +// +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (c *ResourceConfig) GetRaw(k string) (interface{}, bool) { return c.get(k, c.Raw) } // IsComputed returns whether the given key is computed or not. +// +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (c *ResourceConfig) IsComputed(k string) bool { // The next thing we do is check the config if we get a computed // value out of it. @@ -241,12 +283,7 @@ func (c *ResourceConfig) IsComputed(k string) bool { } // Test if the value contains an unknown value - var w unknownCheckWalker - if err := reflectwalk.Walk(v, &w); err != nil { - panic(err) - } - - return w.Unknown + return unknownValueWalk(reflect.ValueOf(v)) } func (c *ResourceConfig) get( @@ -323,18 +360,3 @@ func (c *ResourceConfig) get( return current, true } - -// unknownCheckWalker -type unknownCheckWalker struct { - Unknown bool -} - -// TODO: investigate why deleting this causes odd runtime test failures -// must be some kind of interface implementation -func (w *unknownCheckWalker) Primitive(v reflect.Value) error { - if v.Interface() == hcl2shim.UnknownVariableValue { - w.Unknown = true - } - - return nil -} diff --git a/terraform/resource_address_test.go b/terraform/resource_address_test.go index 6cbacc257..285c2fb33 100644 --- a/terraform/resource_address_test.go +++ b/terraform/resource_address_test.go @@ -189,8 +189,6 @@ func TestParseResourceAddress(t *testing.T) { } for tn, tc := range cases { - tn, tc := tn, tc - t.Run(tn, func(t *testing.T) { t.Parallel() @@ -288,7 +286,6 @@ func TestResourceAddressLess(t *testing.T) { } for _, test := range tests { - test := test t.Run(fmt.Sprintf("%s < %s", test.A, test.B), func(t *testing.T) { t.Parallel() diff --git a/terraform/resource_provider.go b/terraform/resource_provider.go index c8e7008c0..6de283544 100644 --- a/terraform/resource_provider.go +++ b/terraform/resource_provider.go @@ -4,6 +4,10 @@ package terraform // ResourceType is a type of resource that a resource provider can manage. +// +// Deprecated: This type is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. type ResourceType struct { Name string // Name of the resource, example "instance" (no provider prefix) Importable bool // Whether this resource supports importing @@ -17,6 +21,10 @@ type ResourceType struct { } // DataSource is a data source that a resource provider implements. +// +// Deprecated: This type is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. type DataSource struct { Name string diff --git a/terraform/resource_test.go b/terraform/resource_test.go index d4678a32d..baccc4254 100644 --- a/terraform/resource_test.go +++ b/terraform/resource_test.go @@ -9,7 +9,6 @@ import ( "testing" "github.com/hashicorp/go-cty/cty" - "github.com/mitchellh/reflectwalk" "github.com/hashicorp/terraform-plugin-testing/internal/configs/configschema" "github.com/hashicorp/terraform-plugin-testing/internal/configs/hcl2shim" @@ -186,8 +185,6 @@ func TestResourceConfigGet(t *testing.T) { } for i, tc := range cases { - i, tc := i, tc - rc := NewResourceConfigShimmed(tc.Config, tc.Schema) // Test getting a key @@ -281,60 +278,6 @@ func TestResourceConfigEqual_computedKeyOrder(t *testing.T) { } } -func TestUnknownCheckWalker(t *testing.T) { - t.Parallel() - - cases := []struct { - Name string - Input interface{} - Result bool - }{ - { - "primitive", - 42, - false, - }, - - { - "primitive computed", - hcl2shim.UnknownVariableValue, - true, - }, - - { - "list", - []interface{}{"foo", hcl2shim.UnknownVariableValue}, - true, - }, - - { - "nested list", - []interface{}{ - "foo", - []interface{}{hcl2shim.UnknownVariableValue}, - }, - true, - }, - } - - for i, tc := range cases { - i, tc := i, tc - - t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { - t.Parallel() - - var w unknownCheckWalker - if err := reflectwalk.Walk(tc.Input, &w); err != nil { - t.Fatalf("err: %s", err) - } - - if w.Unknown != tc.Result { - t.Fatalf("bad: %v", w.Unknown) - } - }) - } -} - func TestNewResourceConfigShimmed(t *testing.T) { t.Parallel() @@ -691,7 +634,6 @@ func TestNewResourceConfigShimmed(t *testing.T) { }, }, } { - tc := tc t.Run(tc.Name, func(t *testing.T) { t.Parallel() diff --git a/terraform/schemas.go b/terraform/schemas.go index a1b61f181..1cec2eb6f 100644 --- a/terraform/schemas.go +++ b/terraform/schemas.go @@ -13,6 +13,10 @@ import ( // The completeness of this structure depends on how it was constructed. // When constructed for a configuration, it will generally include only // resource types and data sources used by that configuration. +// +// Deprecated: This type is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. type ProviderSchema struct { Provider *configschema.Block ResourceTypes map[string]*configschema.Block @@ -23,6 +27,10 @@ type ProviderSchema struct { // ProviderSchemaRequest is used to describe to a ResourceProvider which // aspects of schema are required, when calling the GetSchema method. +// +// Deprecated: This type is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. type ProviderSchemaRequest struct { ResourceTypes []string DataSources []string diff --git a/terraform/state.go b/terraform/state.go index 4df211100..68267757a 100644 --- a/terraform/state.go +++ b/terraform/state.go @@ -7,6 +7,7 @@ import ( "bufio" "bytes" "encoding/json" + "errors" "fmt" "log" "os" @@ -17,9 +18,7 @@ import ( "sync" "github.com/hashicorp/go-cty/cty" - "github.com/hashicorp/go-multierror" "github.com/hashicorp/go-uuid" - "github.com/mitchellh/copystructure" "github.com/hashicorp/terraform-plugin-testing/internal/addrs" "github.com/hashicorp/terraform-plugin-testing/internal/configs/hcl2shim" @@ -92,11 +91,19 @@ type State struct { // Remote is used to track the metadata required to // pull and push state files from a remote storage endpoint. + // + // Deprecated: This field is unintentionally exported by this Go module and + // external consumption is not supported. It will be removed in the next + // major version. Remote *RemoteState `json:"remote,omitempty"` // Backend tracks the configuration for the backend in use with // this state. This is used to track any changes in the backend // configuration. + // + // Deprecated: This field is unintentionally exported by this Go module and + // external consumption is not supported. It will be removed in the next + // major version. Backend *BackendState `json:"backend,omitempty"` // Modules contains all the modules in a breadth-first order @@ -106,13 +113,28 @@ type State struct { // IsBinaryDrivenTest is a special flag that assists with a binary driver // heuristic, it should not be set externally + // + // Deprecated: This field is unintentionally exported by this Go module and + // external consumption is not supported. It will be removed in the next + // major version. IsBinaryDrivenTest bool } -func (s *State) Lock() { s.mu.Lock() } +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. +func (s *State) Lock() { s.mu.Lock() } + +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (s *State) Unlock() { s.mu.Unlock() } // NewState is used to initialize a blank state +// +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func NewState() *State { s := &State{} s.init() @@ -122,6 +144,10 @@ func NewState() *State { // Children returns the ModuleStates that are direct children of // the given path. If the path is "root", for example, then children // returned might be "root.child", but not "root.child.grandchild". +// +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (s *State) Children(path []string) []*ModuleState { s.Lock() defer s.Unlock() @@ -154,6 +180,10 @@ func (s *State) children(path []string) []*ModuleState { // // This should be the preferred method to add module states since it // allows us to optimize lookups later as well as control sorting. +// +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (s *State) AddModule(path addrs.ModuleInstance) *ModuleState { s.Lock() defer s.Unlock() @@ -200,6 +230,10 @@ func (s *State) addModule(path addrs.ModuleInstance) *ModuleState { // ModuleByPath is used to lookup the module state for the given path. // This should be the preferred lookup mechanism as it allows for future // lookup optimizations. +// +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (s *State) ModuleByPath(path addrs.ModuleInstance) *ModuleState { if s == nil { return nil @@ -227,6 +261,10 @@ func (s *State) moduleByPath(path addrs.ModuleInstance) *ModuleState { } // Empty returns true if the state is empty. +// +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (s *State) Empty() bool { if s == nil { return true @@ -241,6 +279,10 @@ func (s *State) Empty() bool { // // This is similar to !s.Empty, but returns true also in the case where the // state has modules but all of them are devoid of resources. +// +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (s *State) HasResources() bool { if s.Empty() { return false @@ -257,6 +299,10 @@ func (s *State) HasResources() bool { // IsRemote returns true if State represents a state that exists and is // remote. +// +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (s *State) IsRemote() bool { if s == nil { return false @@ -283,11 +329,15 @@ func (s *State) IsRemote() bool { // // If this returns an error, then the user should be notified. The error // response will include detailed information on the nature of the error. +// +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (s *State) Validate() error { s.Lock() defer s.Unlock() - var result error + var result []error // !!!! FOR DEVELOPERS !!!! // @@ -309,7 +359,7 @@ func (s *State) Validate() error { key := strings.Join(ms.Path, ".") if _, ok := found[key]; ok { - result = multierror.Append(result, fmt.Errorf( + result = append(result, fmt.Errorf( strings.TrimSpace(stateValidateErrMultiModule), key)) continue } @@ -318,7 +368,7 @@ func (s *State) Validate() error { } } - return result + return errors.Join(result...) } // Remove removes the item in the state at the given address, returning @@ -327,6 +377,10 @@ func (s *State) Validate() error { // If the address references a module state or resource, it will delete // all children as well. To check what will be deleted, use a StateFilter // first. +// +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (s *State) Remove(addr ...string) error { s.Lock() defer s.Unlock() @@ -437,6 +491,10 @@ func (s *State) RootModule() *ModuleState { } // Equal tests if one state is equal to another. +// +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (s *State) Equal(other *State) bool { // If one is nil, we do a direct check if s == nil || other == nil { @@ -478,6 +536,9 @@ func (s *State) equal(other *State) bool { return true } +// Deprecated: This type is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. type StateAgeComparison int const ( @@ -498,6 +559,10 @@ const ( // the argument, positive if the converse, and zero if they are equal. // An error is returned if the two states are not of the same lineage, // in which case the integer returned has no meaning. +// +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (s *State) CompareAges(other *State) (StateAgeComparison, error) { // nil states are "older" than actual states switch { @@ -530,6 +595,10 @@ func (s *State) CompareAges(other *State) (StateAgeComparison, error) { // SameLineage returns true only if the state given in argument belongs // to the same "lineage" of states as the receiver. +// +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (s *State) SameLineage(other *State) bool { s.Lock() defer s.Unlock() @@ -547,24 +616,59 @@ func (s *State) SameLineage(other *State) bool { // DeepCopy performs a deep copy of the state structure and returns // a new structure. +// +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (s *State) DeepCopy() *State { if s == nil { return nil } - copiedState, err := copystructure.Config{Lock: true}.Copy(s) - if err != nil { - panic(err) + copied := &State{ + IsBinaryDrivenTest: s.IsBinaryDrivenTest, + Lineage: s.Lineage, + Serial: s.Serial, + TFVersion: s.TFVersion, + Version: s.Version, + } + + if s.Backend != nil { + copied.Backend = &BackendState{ + Hash: s.Backend.Hash, + ConfigRaw: s.Backend.ConfigRaw, + Type: s.Backend.Type, + } } - state, ok := copiedState.(*State) - if !ok { - panic(fmt.Errorf("unexpected type %T for copiedState", state)) + // Best effort single level copy is fine; this is method is not used by this + // Go module and its already deprecated. + if s.Modules != nil { + copied.Modules = make([]*ModuleState, len(s.Modules)) + + copy(copied.Modules, s.Modules) + } + + if s.Remote != nil { + copied.Remote = &RemoteState{ + Type: s.Remote.Type, + } + + if s.Remote.Config != nil { + copied.Remote.Config = make(map[string]string, len(s.Remote.Config)) + + for key, value := range s.Remote.Config { + copied.Remote.Config[key] = value + } + } } - return state + return copied } +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (s *State) Init() { s.Lock() defer s.Unlock() @@ -593,6 +697,9 @@ func (s *State) init() { } +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (s *State) EnsureHasLineage() { s.Lock() defer s.Unlock() @@ -618,6 +725,10 @@ func (s *State) ensureHasLineage() { } // AddModuleState insert this module state and override any existing ModuleState +// +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (s *State) AddModuleState(mod *ModuleState) { mod.init() s.Lock() @@ -709,6 +820,10 @@ func (s *State) String() string { } // BackendState stores the configuration to connect to a remote backend. +// +// Deprecated: This type is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. type BackendState struct { Type string `json:"type"` // Backend type ConfigRaw json.RawMessage `json:"config"` // Backend raw config @@ -717,6 +832,10 @@ type BackendState struct { // RemoteState is used to track the information about a remote // state store that we push/pull state to. +// +// Deprecated: This type is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. type RemoteState struct { // Type controls the client we use for the remote state Type string `json:"type"` @@ -728,7 +847,14 @@ type RemoteState struct { mu sync.Mutex } -func (s *RemoteState) Lock() { s.mu.Lock() } +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. +func (s *RemoteState) Lock() { s.mu.Lock() } + +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (s *RemoteState) Unlock() { s.mu.Unlock() } func (r *RemoteState) init() { @@ -740,6 +866,9 @@ func (r *RemoteState) init() { } } +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (r *RemoteState) Empty() bool { if r == nil { return true @@ -765,7 +894,14 @@ type OutputState struct { mu sync.Mutex } -func (s *OutputState) Lock() { s.mu.Lock() } +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. +func (s *OutputState) Lock() { s.mu.Lock() } + +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (s *OutputState) Unlock() { s.mu.Unlock() } func (s *OutputState) String() string { @@ -774,6 +910,10 @@ func (s *OutputState) String() string { // Equal compares two OutputState structures for equality. nil values are // considered equal. +// +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (s *OutputState) Equal(other *OutputState) bool { if s == nil && other == nil { return true @@ -840,7 +980,14 @@ type ModuleState struct { mu sync.Mutex } -func (s *ModuleState) Lock() { s.mu.Lock() } +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. +func (s *ModuleState) Lock() { s.mu.Lock() } + +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (s *ModuleState) Unlock() { s.mu.Unlock() } // Equal tests whether one module state is equal to another. @@ -1202,7 +1349,14 @@ type ResourceState struct { mu sync.Mutex } -func (s *ResourceState) Lock() { s.mu.Lock() } +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. +func (s *ResourceState) Lock() { s.mu.Lock() } + +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (s *ResourceState) Unlock() { s.mu.Unlock() } // Equal tests whether two ResourceStates are equal. @@ -1323,7 +1477,14 @@ type InstanceState struct { mu sync.Mutex } -func (s *InstanceState) Lock() { s.mu.Lock() } +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. +func (s *InstanceState) Lock() { s.mu.Lock() } + +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (s *InstanceState) Unlock() { s.mu.Unlock() } func (s *InstanceState) init() { @@ -1344,6 +1505,10 @@ func (s *InstanceState) init() { // legacy InstanceState representation. // // This is for shimming to old components only and should not be used in new code. +// +// Deprecated: This function is unintentionally exported by this Go module and +// not supported for external consumption. It will be removed in the next major +// version. func NewInstanceStateShimmedFromValue(state cty.Value, schemaVersion int) *InstanceState { attrs := hcl2shim.FlatmapValueFromHCL2(state) return &InstanceState{ @@ -1364,6 +1529,10 @@ func NewInstanceStateShimmedFromValue(state cty.Value, schemaVersion int) *Insta // // This is for shimming from old components only and should not be used in // new code. +// +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (s *InstanceState) AttrsAsObjectValue(ty cty.Type) (cty.Value, error) { if s == nil { // if the state is nil, we need to construct a complete cty.Value with @@ -1385,6 +1554,10 @@ func (s *InstanceState) AttrsAsObjectValue(ty cty.Type) (cty.Value, error) { } // Copy all the Fields from another InstanceState +// +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (s *InstanceState) Set(from *InstanceState) { s.Lock() defer s.Unlock() @@ -1399,20 +1572,51 @@ func (s *InstanceState) Set(from *InstanceState) { s.Tainted = from.Tainted } +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (s *InstanceState) DeepCopy() *InstanceState { - copiedState, err := copystructure.Config{Lock: true}.Copy(s) - if err != nil { - panic(err) + if s == nil { + return nil + } + + copied := &InstanceState{ + Ephemeral: EphemeralState{ + ConnInfo: s.Ephemeral.ConnInfo, + Type: s.Ephemeral.Type, + }, + ID: s.ID, + ProviderMeta: s.ProviderMeta, + RawConfig: s.RawConfig, + RawPlan: s.RawPlan, + RawState: s.RawState, + Tainted: s.Tainted, + } + + if s.Attributes != nil { + copied.Attributes = make(map[string]string, len(s.Attributes)) + + for k, v := range s.Attributes { + copied.Attributes[k] = v + } } - instanceState, ok := copiedState.(*InstanceState) - if !ok { - panic(fmt.Errorf("unexpected type %T for copiedState", copiedState)) + // Best effort single level copy is fine; this is not used by this Go module + // and its already deprecated. + if s.Meta != nil { + copied.Meta = make(map[string]any, len(s.Meta)) + + for k, v := range s.Meta { + copied.Meta[k] = v + } } - return instanceState + return copied } +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (s *InstanceState) Empty() bool { if s == nil { return true @@ -1492,6 +1696,10 @@ func (s *InstanceState) Equal(other *InstanceState) bool { // If the diff attribute requires computing the value, and hence // won't be available until apply, the value is replaced with the // computeID. +// +// Deprecated: This method is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. func (s *InstanceState) MergeDiff(d *InstanceDiff) *InstanceState { result := s.DeepCopy() if result == nil { @@ -1564,6 +1772,10 @@ func (s *InstanceState) String() string { } // EphemeralState is used for transient state that is only kept in-memory +// +// Deprecated: This type is unintentionally exported by this Go module and not +// supported for external consumption. It will be removed in the next major +// version. type EphemeralState struct { // ConnInfo is used for the providers to export information which is // used to connect to the resource for provisioning. For example, diff --git a/terraform/state_test.go b/terraform/state_test.go index a7a233cc9..41c536856 100644 --- a/terraform/state_test.go +++ b/terraform/state_test.go @@ -183,8 +183,6 @@ func TestStateDeepCopy(t *testing.T) { } for i, tc := range cases { - i, tc := i, tc - t.Run(fmt.Sprintf("copy-%d", i), func(t *testing.T) { t.Parallel() @@ -389,8 +387,6 @@ func TestStateEqual(t *testing.T) { } for i, tc := range cases { - i, tc := i, tc - t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { t.Parallel() diff --git a/terraform/unknown_value_walk.go b/terraform/unknown_value_walk.go new file mode 100644 index 000000000..66e129549 --- /dev/null +++ b/terraform/unknown_value_walk.go @@ -0,0 +1,85 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package terraform + +import ( + "reflect" + + "github.com/hashicorp/terraform-plugin-testing/internal/configs/hcl2shim" +) + +// unknownValueWalk is a reimplementation of the prior walk() logic from +// github.com/mitchellh/reflectwalk and the only walker implemented in this +// module that checked values for hcl2shim.UnknownVariableValue. +// +// Using reflection instead of known logic here is a Go anti-pattern, however +// this logic will be removed in the next major version, so the reflection +// approach is preserved to minimize reimplementation effort. +func unknownValueWalk(v reflect.Value) bool { + for { + switch v.Kind() { + case reflect.Interface: + v = v.Elem() + + continue + case reflect.Pointer: + v = reflect.Indirect(v) + + continue + } + + break + } + + switch v.Kind() { + case reflect.Bool, + reflect.Complex128, + reflect.Complex64, + reflect.Float32, + reflect.Float64, + reflect.Int, + reflect.Int16, + reflect.Int32, + reflect.Int64, + reflect.Int8, + reflect.Uint, + reflect.Uint16, + reflect.Uint32, + reflect.Uint64, + reflect.Uint8, + reflect.Uintptr, + reflect.String: + value := v.Interface() + + return value == hcl2shim.UnknownVariableValue + case reflect.Map: + for _, k := range v.MapKeys() { + value := v.MapIndex(k) + + if foundUnknown := unknownValueWalk(value); foundUnknown { + return true + } + } + case reflect.Array, reflect.Slice: + for index := 0; index < v.Len(); index++ { + value := v.Index(index) + + if foundUnknown := unknownValueWalk(value); foundUnknown { + return true + } + } + case reflect.Struct: + for index := 0; index < v.Type().NumField(); index++ { + value := v.FieldByIndex([]int{index}) + + if foundUnknown := unknownValueWalk(value); foundUnknown { + return true + } + } + default: + panic("unsupported reflect type: " + v.Kind().String()) + } + + return false +} diff --git a/terraform/unknown_value_walk_test.go b/terraform/unknown_value_walk_test.go new file mode 100644 index 000000000..775c76b48 --- /dev/null +++ b/terraform/unknown_value_walk_test.go @@ -0,0 +1,69 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package terraform + +import ( + "reflect" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/internal/configs/hcl2shim" +) + +func TestUnknownValueWalk(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + value any + expected bool + }{ + "primitive": { + value: 42, + expected: false, + }, + "primitive computed": { + value: hcl2shim.UnknownVariableValue, + expected: true, + }, + "list": { + value: []any{ + "foo", + hcl2shim.UnknownVariableValue, + }, + expected: true, + }, + "nested list": { + value: []any{ + "foo", + []any{hcl2shim.UnknownVariableValue}, + }, + expected: true, + }, + "map": { + value: map[string]any{ + "testkey1": "foo", + "testkey2": hcl2shim.UnknownVariableValue, + }, + expected: true, + }, + "nested map": { + value: map[string]any{ + "testkey1": "foo", + "testkey2": map[string]any{"testkey": hcl2shim.UnknownVariableValue}, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := unknownValueWalk(reflect.ValueOf(testCase.value)) + + if got != testCase.expected { + t.Errorf("expected: %t, got: %t", testCase.expected, got) + } + }) + } +} diff --git a/terraform/util_test.go b/terraform/util_test.go index 0c68f9e67..461cea234 100644 --- a/terraform/util_test.go +++ b/terraform/util_test.go @@ -43,8 +43,6 @@ func TestUniqueStrings(t *testing.T) { } for i, tc := range cases { - i, tc := i, tc - t.Run(fmt.Sprintf("unique-%d", i), func(t *testing.T) { t.Parallel() diff --git a/tfjsonpath/doc.go b/tfjsonpath/doc.go new file mode 100644 index 000000000..4b1a4923b --- /dev/null +++ b/tfjsonpath/doc.go @@ -0,0 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package tfjsonpath implements terraform-json path functionality, which defines +// traversals into Terraform JSON data, for testing purposes. +package tfjsonpath diff --git a/tfjsonpath/path.go b/tfjsonpath/path.go new file mode 100644 index 000000000..c29ae2608 --- /dev/null +++ b/tfjsonpath/path.go @@ -0,0 +1,135 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfjsonpath + +import ( + "fmt" + "strings" +) + +// Path represents exact traversal steps specifying a value inside +// Terraform JSON data. These steps always start from a MapStep with a key +// specifying the name of a top-level JSON object or array. +// +// The [terraform-json] library serves as the de facto documentation +// for JSON format of Terraform data. +// +// Use the New() function to create a Path with an initial AtMapKey() step. +// Path functionality follows a builder pattern, which allows for chaining method +// calls to construct a full path. The available traversal steps after Path +// creation are: +// +// - AtSliceIndex(): Step into a slice at a specific 0-based index +// - AtMapKey(): Step into a map at a specific key +// +// For example, to represent the first element of a JSON array +// underneath a "some_array" property of this JSON value: +// +// { +// "some_array": [true] +// } +// +// The path code would be represented by: +// +// tfjsonpath.New("some_array").AtSliceIndex(0) +// +// [terraform-json]: (https://pkg.go.dev/github.com/hashicorp/terraform-json) +type Path struct { + steps []step +} + +// New creates a new path with an initial MapStep or SliceStep. +func New[T int | string](firstStep T) Path { + switch t := any(firstStep).(type) { + case int: + return Path{ + steps: []step{ + SliceStep(t), + }, + } + case string: + return Path{ + steps: []step{ + MapStep(t), + }, + } + } + + // Unreachable code + return Path{} +} + +// AtSliceIndex returns a copied Path with a new SliceStep at the end. +func (s Path) AtSliceIndex(index int) Path { + newSteps := append(s.steps, SliceStep(index)) + s.steps = newSteps + return s +} + +// AtMapKey returns a copied Path with a new MapStep at the end. +func (s Path) AtMapKey(key string) Path { + newSteps := append(s.steps, MapStep(key)) + s.steps = newSteps + return s +} + +// String returns a string representation of the Path. +func (s Path) String() string { + var pathStr []string + + for _, step := range s.steps { + pathStr = append(pathStr, fmt.Sprintf("%v", step)) + } + + return strings.Join(pathStr, ".") +} + +// Traverse returns the element found when traversing the given +// object using the specified Path. The object is an unmarshalled +// JSON object representing Terraform data. +// +// Traverse returns an error if the value specified by the Path +// is not found in the given object or if the given object does not +// conform to format of Terraform JSON data. +func Traverse(object any, attrPath Path) (any, error) { + result := object + + var steps []string + + for _, step := range attrPath.steps { + switch s := step.(type) { + case MapStep: + steps = append(steps, string(s)) + + mapObj, ok := result.(map[string]any) + + if !ok { + return nil, fmt.Errorf("path not found: cannot convert object at MapStep %s to map[string]any", strings.Join(steps, ".")) + } + + result, ok = mapObj[string(s)] + + if !ok { + return nil, fmt.Errorf("path not found: specified key %s not found in map at %s", string(s), strings.Join(steps, ".")) + } + + case SliceStep: + steps = append(steps, fmt.Sprint(s)) + + sliceObj, ok := result.([]any) + + if !ok { + return nil, fmt.Errorf("path not found: cannot convert object at SliceStep %s to []any", strings.Join(steps, ".")) + } + + if int(s) >= len(sliceObj) { + return nil, fmt.Errorf("path not found: SliceStep index %s is out of range with slice length %d", strings.Join(steps, "."), len(sliceObj)) + } + + result = sliceObj[s] + } + } + + return result, nil +} diff --git a/tfjsonpath/path_test.go b/tfjsonpath/path_test.go new file mode 100644 index 000000000..878def1fa --- /dev/null +++ b/tfjsonpath/path_test.go @@ -0,0 +1,581 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfjsonpath + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func Test_Traverse_StringValue(t *testing.T) { + t.Parallel() + + path := New("StringValue") + + actual, err := Traverse(createTestObject(), path) + if err != nil { + t.Errorf("Error traversing JSON object %s", err) + } + expected := "example" + + if expected != actual { + t.Errorf("Output %v not equal to expected %v", actual, expected) + } +} + +func Test_Traverse_Array_StringValue(t *testing.T) { + t.Parallel() + + path := New(0).AtMapKey("StringValue") + + actual, err := Traverse(createTestArray(), path) + if err != nil { + t.Errorf("Error traversing JSON object %s", err) + } + expected := "example" + + if expected != actual { + t.Errorf("Output %v not equal to expected %v", actual, expected) + } +} + +func Test_Traverse_NumberValue(t *testing.T) { + t.Parallel() + + path := New("NumberValue") + + actual, err := Traverse(createTestObject(), path) + if err != nil { + t.Errorf("Error traversing JSON object %s", err) + } + expected := 0.0 + + if expected != actual { + t.Errorf("Output %v not equal to expected %v", actual, expected) + } +} + +func Test_Traverse_Array_NumberValue(t *testing.T) { + t.Parallel() + + path := New(0).AtMapKey("NumberValue") + + actual, err := Traverse(createTestArray(), path) + if err != nil { + t.Errorf("Error traversing JSON object %s", err) + } + expected := 0.0 + + if expected != actual { + t.Errorf("Output %v not equal to expected %v", actual, expected) + } +} + +func Test_Traverse_BooleanValue(t *testing.T) { + t.Parallel() + + path := New("BooleanValue") + + actual, err := Traverse(createTestObject(), path) + if err != nil { + t.Errorf("Error traversing JSON object %s", err) + } + expected := false + + if expected != actual { + t.Errorf("Output %v not equal to expected %v", actual, expected) + } +} + +func Test_Traverse_Array_BooleanValue(t *testing.T) { + t.Parallel() + + path := New(0).AtMapKey("BooleanValue") + + actual, err := Traverse(createTestArray(), path) + if err != nil { + t.Errorf("Error traversing JSON object %s", err) + } + expected := false + + if expected != actual { + t.Errorf("Output %v not equal to expected %v", actual, expected) + } +} + +func Test_Traverse_NullValue(t *testing.T) { + t.Parallel() + + path := New("NullValue") + + actual, err := Traverse(createTestObject(), path) + if err != nil { + t.Errorf("Error traversing JSON object %s", err) + } + + if actual != nil { + t.Errorf("Output %v not equal to expected %v", actual, nil) + } +} + +func Test_Traverse_Array_NullValue(t *testing.T) { + t.Parallel() + + path := New(0).AtMapKey("NullValue") + + actual, err := Traverse(createTestArray(), path) + if err != nil { + t.Errorf("Error traversing JSON object %s", err) + } + + if actual != nil { + t.Errorf("Output %v not equal to expected %v", actual, nil) + } +} + +func Test_Traverse_Array(t *testing.T) { + t.Parallel() + + testCases := []struct { + path Path + expected any + }{ + { + path: New("Array").AtSliceIndex(0), + expected: 10.0, + }, + { + path: New("Array").AtSliceIndex(1), + expected: 15.2, + }, + { + path: New("Array").AtSliceIndex(2), + expected: "example2", + }, + { + path: New("Array").AtSliceIndex(3), + expected: nil, + }, + { + path: New("Array").AtSliceIndex(4), + expected: true, + }, + { + path: New("Array").AtSliceIndex(5).AtMapKey("NestedStringValue"), + expected: "example3", + }, + { + path: New("Array").AtSliceIndex(6).AtSliceIndex(0), + expected: true, + }, + } + + for _, tc := range testCases { + actual, err := Traverse(createTestObject(), tc.path) + if err != nil { + t.Errorf("Error traversing JSON object %s", err) + } + expected := tc.expected + + if expected != actual { + t.Errorf("Output %v not equal to expected %v", actual, expected) + } + } +} + +func Test_Traverse_Array_Array(t *testing.T) { + t.Parallel() + + testCases := []struct { + path Path + expected any + }{ + { + path: New(0).AtMapKey("Array").AtSliceIndex(0), + expected: 10.0, + }, + { + path: New(0).AtMapKey("Array").AtSliceIndex(1), + expected: 15.2, + }, + { + path: New(0).AtMapKey("Array").AtSliceIndex(2), + expected: "example2", + }, + { + path: New(0).AtMapKey("Array").AtSliceIndex(3), + expected: nil, + }, + { + path: New(0).AtMapKey("Array").AtSliceIndex(4), + expected: true, + }, + { + path: New(0).AtMapKey("Array").AtSliceIndex(5).AtMapKey("NestedStringValue"), + expected: "example3", + }, + { + path: New(0).AtMapKey("Array").AtSliceIndex(6).AtSliceIndex(0), + expected: true, + }, + } + + for _, tc := range testCases { + actual, err := Traverse(createTestArray(), tc.path) + if err != nil { + t.Errorf("Error traversing JSON object %s", err) + } + expected := tc.expected + + if expected != actual { + t.Errorf("Output %v not equal to expected %v", actual, expected) + } + } +} + +func Test_Traverse_Object(t *testing.T) { + t.Parallel() + + testCases := []struct { + path Path + expected any + }{ + { + path: New("Object").AtMapKey("StringValue"), + expected: "example", + }, + { + path: New("Object").AtMapKey("NumberValue"), + expected: 0.0, + }, + { + path: New("Object").AtMapKey("BooleanValue"), + expected: false, + }, + { + path: New("Object").AtMapKey("ArrayValue").AtSliceIndex(0), + expected: 10.0, + }, + { + path: New("Object").AtMapKey("ArrayValue").AtSliceIndex(1), + expected: 15.2, + }, + { + path: New("Object").AtMapKey("ArrayValue").AtSliceIndex(2), + expected: "example2", + }, + { + path: New("Object").AtMapKey("ArrayValue").AtSliceIndex(3), + expected: nil, + }, + { + path: New("Object").AtMapKey("ArrayValue").AtSliceIndex(4), + expected: true, + }, + { + path: New("Object").AtMapKey("ObjectValue").AtMapKey("NestedStringValue"), + expected: "example3", + }, + } + + for _, tc := range testCases { + actual, err := Traverse(createTestObject(), tc.path) + if err != nil { + t.Errorf("Error traversing JSON object %s", err) + } + expected := tc.expected + + if expected != actual { + t.Errorf("Output %v not equal to expected %v", actual, expected) + } + } +} + +func Test_Traverse_Array_Object(t *testing.T) { + t.Parallel() + + testCases := []struct { + path Path + expected any + }{ + { + path: New(0).AtMapKey("Object").AtMapKey("StringValue"), + expected: "example", + }, + { + path: New(0).AtMapKey("Object").AtMapKey("NumberValue"), + expected: 0.0, + }, + { + path: New(0).AtMapKey("Object").AtMapKey("BooleanValue"), + expected: false, + }, + { + path: New(0).AtMapKey("Object").AtMapKey("ArrayValue").AtSliceIndex(0), + expected: 10.0, + }, + { + path: New(0).AtMapKey("Object").AtMapKey("ArrayValue").AtSliceIndex(1), + expected: 15.2, + }, + { + path: New(0).AtMapKey("Object").AtMapKey("ArrayValue").AtSliceIndex(2), + expected: "example2", + }, + { + path: New(0).AtMapKey("Object").AtMapKey("ArrayValue").AtSliceIndex(3), + expected: nil, + }, + { + path: New(0).AtMapKey("Object").AtMapKey("ArrayValue").AtSliceIndex(4), + expected: true, + }, + { + path: New(0).AtMapKey("Object").AtMapKey("ObjectValue").AtMapKey("NestedStringValue"), + expected: "example3", + }, + } + + for _, tc := range testCases { + actual, err := Traverse(createTestArray(), tc.path) + if err != nil { + t.Errorf("Error traversing JSON object %s", err) + } + expected := tc.expected + + if expected != actual { + t.Errorf("Output %v not equal to expected %v", actual, expected) + } + } +} + +func Test_Traverse_ExpectError(t *testing.T) { + t.Parallel() + + testCases := []struct { + path Path + expectedError func(err error) bool + }{ + // specified key not found + { + path: New("ObjectA"), + expectedError: func(err error) bool { + return strings.Contains(err.Error(), `path not found: specified key ObjectA not found in map at ObjectA`) + }, + }, + { + path: New("Object").AtMapKey("MapValueA"), + expectedError: func(err error) bool { + return strings.Contains(err.Error(), `path not found: specified key MapValueA not found in map at Object.MapValueA`) + }, + }, + + // cannot convert object + { + path: New("StringValue").AtSliceIndex(0), + expectedError: func(err error) bool { + return strings.Contains(err.Error(), `path not found: cannot convert object at SliceStep StringValue.0 to []any`) + }, + }, + { + path: New("StringValue").AtMapKey("MapKeyA"), + expectedError: func(err error) bool { + return strings.Contains(err.Error(), `path not found: cannot convert object at MapStep StringValue.MapKeyA to map[string]any`) + }, + }, + { + path: New("Array").AtSliceIndex(0).AtMapKey("MapValueA"), + expectedError: func(err error) bool { + return strings.Contains(err.Error(), `path not found: cannot convert object at MapStep Array.0.MapValueA to map[string]any`) + }, + }, + + // index out of bounds + { + path: New("Array").AtSliceIndex(10), + expectedError: func(err error) bool { + return strings.Contains(err.Error(), `path not found: SliceStep index Array.10 is out of range with slice length 7`) + }, + }, + { + path: New("Array").AtSliceIndex(7), + expectedError: func(err error) bool { + return strings.Contains(err.Error(), `path not found: SliceStep index Array.7 is out of range with slice length 7`) + }, + }, + } + + for _, tc := range testCases { + _, err := Traverse(createTestObject(), tc.path) + if err == nil { + t.Fatalf("Expected error but got none") + } + + if !tc.expectedError(err) { + t.Errorf("Unexpected error: %s", err) + } + } +} + +func Test_Traverse_Array_ExpectError(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + path Path + expectedError func(err error) bool + }{ + // specified index not found + "unknown_index": { + path: New(1), + expectedError: func(err error) bool { + return strings.Contains(err.Error(), `path not found: SliceStep index 1 is out of range with slice length 1`) + }, + }, + "unknown_nested_index": { + path: New(0).AtSliceIndex(0), + expectedError: func(err error) bool { + return strings.Contains(err.Error(), `path not found: cannot convert object at SliceStep 0.0 to []any`) + }, + }, + + // cannot convert object + "unknown_map_index": { + path: New(0).AtMapKey("StringValue").AtSliceIndex(0), + expectedError: func(err error) bool { + return strings.Contains(err.Error(), `path not found: cannot convert object at SliceStep 0.StringValue.0 to []any`) + }, + }, + "unknown_map_key": { + path: New(0).AtMapKey("StringValue").AtMapKey("MapKeyA"), + expectedError: func(err error) bool { + return strings.Contains(err.Error(), `path not found: cannot convert object at MapStep 0.StringValue.MapKeyA to map[string]any`) + }, + }, + "unknown_slice_map_key": { + path: New(0).AtMapKey("Array").AtSliceIndex(0).AtMapKey("MapValueA"), + expectedError: func(err error) bool { + return strings.Contains(err.Error(), `path not found: cannot convert object at MapStep 0.Array.0.MapValueA to map[string]any`) + }, + }, + + // index out of bounds + "out_of_bounds": { + path: New(0).AtMapKey("Array").AtSliceIndex(10), + expectedError: func(err error) bool { + return strings.Contains(err.Error(), `path not found: SliceStep index 0.Array.10 is out of range with slice length 7`) + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + _, err := Traverse(createTestArray(), tc.path) + + if err == nil { + t.Fatalf("Expected error but got none") + } + + if !tc.expectedError(err) { + t.Errorf("Unexpected error: %s", err) + } + }) + } +} + +func TestPath_String(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + path Path + expected string + }{ + "slice_step": { + path: New(1), + expected: "1", + }, + "map_step": { + path: New("attr"), + expected: "attr", + }, + "slice_step_map_step": { + path: New(0).AtMapKey("attr"), + expected: "0.attr", + }, + "map_step_slice_step": { + path: New("attr").AtSliceIndex(0), + expected: "attr.0", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := tc.path.String() + + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func createTestObject() any { + var jsonObject any + jsonstring := + `{ + "StringValue": "example", + "NumberValue": 0, + "BooleanValue": false, + "NullValue": null, + "Array": [10, 15.2, "example2", null, true, {"NestedStringValue": "example3"}, [true]], + "Object":{ + "StringValue": "example", + "NumberValue": 0, + "BooleanValue": false, + "ArrayValue": [10, 15.2, "example2", null, true], + "ObjectValue": { + "NestedStringValue": "example3" + } + } + }` + err := json.Unmarshal([]byte(jsonstring), &jsonObject) + if err != nil { + return nil + } + + return jsonObject +} + +func createTestArray() any { + var jsonObject any + jsonstring := + `[{ + "StringValue": "example", + "NumberValue": 0, + "BooleanValue": false, + "NullValue": null, + "Array": [10, 15.2, "example2", null, true, {"NestedStringValue": "example3"}, [true]], + "Object":{ + "StringValue": "example", + "NumberValue": 0, + "BooleanValue": false, + "ArrayValue": [10, 15.2, "example2", null, true], + "ObjectValue": { + "NestedStringValue": "example3" + } + } + }]` + err := json.Unmarshal([]byte(jsonstring), &jsonObject) + if err != nil { + return nil + } + + return jsonObject +} diff --git a/tfjsonpath/step.go b/tfjsonpath/step.go new file mode 100644 index 000000000..5a779640d --- /dev/null +++ b/tfjsonpath/step.go @@ -0,0 +1,14 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfjsonpath + +// step represents a traversal type indicating the underlying Go type +// representation for a Terraform JSON value. +type step any + +// MapStep represents a traversal for map[string]any +type MapStep string + +// SliceStep represents a traversal for []any +type SliceStep int diff --git a/tfversion/all_test.go b/tfversion/all_test.go index 4751eeb5c..aa326d6bf 100644 --- a/tfversion/all_test.go +++ b/tfversion/all_test.go @@ -12,17 +12,18 @@ import ( r "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" + "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/tfversion" ) func Test_All_RunTest(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.1.0") r.UnitTest(t, r.TestCase{ ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature - return nil, nil - }, + "test": providerserver.NewProviderServer(testprovider.Provider{}), }, TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.Any( @@ -45,6 +46,7 @@ func Test_All_RunTest(t *testing.T) { //nolint:paralleltest } func Test_All_SkipTest(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.0.7") r.UnitTest(t, r.TestCase{ @@ -75,6 +77,7 @@ func Test_All_SkipTest(t *testing.T) { //nolint:paralleltest } func Test_All_Error(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.1.0") plugintest.TestExpectTFatal(t, func() { diff --git a/tfversion/any.go b/tfversion/any.go index 27088e1a5..2fee9cb10 100644 --- a/tfversion/any.go +++ b/tfversion/any.go @@ -5,9 +5,8 @@ package tfversion import ( "context" + "errors" "strings" - - "github.com/hashicorp/terraform-plugin-testing/internal/errorshim" ) // Any will return a nil error and empty skip message (run the test) @@ -28,7 +27,7 @@ type anyCheck struct { // CheckTerraformVersion satisfies the TerraformVersionCheck interface. func (a anyCheck) CheckTerraformVersion(ctx context.Context, req CheckTerraformVersionRequest, resp *CheckTerraformVersionResponse) { - var joinedErrors error + var joinedErrors []error strBuilder := strings.Builder{} for _, subCheck := range a.terraformVersionChecks { @@ -42,11 +41,7 @@ func (a anyCheck) CheckTerraformVersion(ctx context.Context, req CheckTerraformV return } - if checkResp.Error != nil { - // TODO: Once Go 1.20 is the minimum supported version for this module, replace with `errors.Join` function - // - https://github.com/hashicorp/terraform-plugin-testing/issues/99 - joinedErrors = errorshim.Join(joinedErrors, checkResp.Error) - } + joinedErrors = append(joinedErrors, checkResp.Error) if checkResp.Skip != "" { strBuilder.WriteString(checkResp.Skip) @@ -54,6 +49,6 @@ func (a anyCheck) CheckTerraformVersion(ctx context.Context, req CheckTerraformV } } - resp.Error = joinedErrors + resp.Error = errors.Join(joinedErrors...) resp.Skip = strings.TrimSpace(strBuilder.String()) } diff --git a/tfversion/any_test.go b/tfversion/any_test.go index bf5e1f7b1..76eacaa01 100644 --- a/tfversion/any_test.go +++ b/tfversion/any_test.go @@ -12,17 +12,18 @@ import ( r "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" + "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/tfversion" ) func Test_Any_RunTest(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.1.0") r.UnitTest(t, r.TestCase{ ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature - return nil, nil - }, + "test": providerserver.NewProviderServer(testprovider.Provider{}), }, TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.Any( @@ -42,6 +43,7 @@ func Test_Any_RunTest(t *testing.T) { //nolint:paralleltest } func Test_Any_SkipTest(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.1.0") r.UnitTest(t, r.TestCase{ @@ -68,6 +70,7 @@ func Test_Any_SkipTest(t *testing.T) { //nolint:paralleltest } func Test_Any_Error(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.1.0") plugintest.TestExpectTFatal(t, func() { diff --git a/tfversion/require_above.go b/tfversion/require_above.go index 4734bcf6e..bea04c67a 100644 --- a/tfversion/require_above.go +++ b/tfversion/require_above.go @@ -11,9 +11,19 @@ import ( ) // RequireAbove will fail the test if the Terraform CLI -// version is below the given version. For example, if given -// version.Must(version.NewVersion("0.15.0")), then 0.14.x or -// any other prior minor versions will fail the test. +// version is exclusively below the given version. For example, if given +// version.Must(version.NewVersion("1.8.0")), then 1.7.x or +// any other prior versions will fail the test. +// +// Prereleases of Terraform CLI (whether alpha, beta, or rc) are considered +// equal to a given patch version. For example, if given +// version.Must(version.NewVersion("1.8.0")), then 1.8.0-rc1 will run, not fail, +// the test. Terraform prereleases are considered as potential candidates for +// the upcoming version and therefore are treated as semantically equal for +// testing. If failing prereleases of the same patch release is desired, give a +// higher prerelease version. For example, if given +// version.Must(version.NewVersion("1.8.0-rc2")), then 1.8.0-rc1 will fail the +// test. func RequireAbove(minimumVersion *version.Version) TerraformVersionCheck { return requireAboveCheck{ minimumVersion: minimumVersion, @@ -27,8 +37,17 @@ type requireAboveCheck struct { // CheckTerraformVersion satisfies the TerraformVersionCheck interface. func (r requireAboveCheck) CheckTerraformVersion(ctx context.Context, req CheckTerraformVersionRequest, resp *CheckTerraformVersionResponse) { + var terraformVersion *version.Version - if req.TerraformVersion.LessThan(r.minimumVersion) { + // If given a prerelease version, check the Terraform CLI version directly, + // otherwise use the core version so that prereleases are treated as equal. + if r.minimumVersion.Prerelease() != "" { + terraformVersion = req.TerraformVersion + } else { + terraformVersion = req.TerraformVersion.Core() + } + + if terraformVersion.LessThan(r.minimumVersion) { resp.Error = fmt.Errorf("expected Terraform CLI version above %s but detected version is %s", r.minimumVersion, req.TerraformVersion) } diff --git a/tfversion/require_above_test.go b/tfversion/require_above_test.go index b78ac8b13..8e8c3ea8e 100644 --- a/tfversion/require_above_test.go +++ b/tfversion/require_above_test.go @@ -11,20 +11,44 @@ import ( r "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" + "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/tfversion" testinginterface "github.com/mitchellh/go-testing-interface" ) -func Test_RequireAbove(t *testing.T) { //nolint:paralleltest +func Test_RequireAbove_Equal(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.1.0") r.UnitTest(t, r.TestCase{ ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature - return nil, nil + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(version.Must(version.NewVersion("1.1.0"))), + }, + Steps: []r.TestStep{ + { + //nullable argument only available in TF v1.1.0+ + Config: `variable "a" { + nullable = true + default = "hello" + }`, }, }, + }) +} + +func Test_RequireAbove_Higher(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.1.1") + + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.RequireAbove(version.Must(version.NewVersion("1.1.0"))), }, @@ -40,7 +64,8 @@ func Test_RequireAbove(t *testing.T) { //nolint:paralleltest }) } -func Test_RequireAbove_Error(t *testing.T) { //nolint:paralleltest +func Test_RequireAbove_Lower(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.0.7") plugintest.TestExpectTFatal(t, func() { @@ -61,3 +86,137 @@ func Test_RequireAbove_Error(t *testing.T) { //nolint:paralleltest }) }) } + +func Test_RequireAbove_Prerelease_EqualCoreVersion(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc1") + + // Pragmatic compromise that 1.8.0-rc1 prerelease is considered to + // be equivalent to the 1.8.0 core version. This enables developers + // to assert that prerelease versions are ran with upcoming + // core versions. + // + // Reference: https://github.com/hashicorp/terraform-plugin-testing/issues/303 + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(version.Must(version.NewVersion("1.8.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_RequireAbove_Prerelease_EqualPrerelease(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc1") + + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(version.Must(version.NewVersion("1.8.0-rc1"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_RequireAbove_Prerelease_HigherCoreVersion(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.7.0-rc1") + + // The 1.7.0-rc1 prerelease should always be considered to be below the + // 1.8.0 core version. This intentionally verifies that the logic does not + // ignore the core version of the prerelease version when compared against + // the core version of the check. + plugintest.TestExpectTFatal(t, func() { + r.UnitTest(&testinginterface.RuntimeT{}, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(version.Must(version.NewVersion("1.8.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) + }) +} + +func Test_RequireAbove_Prerelease_HigherPrerelease(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.7.0-rc1") + + // The 1.7.0-rc1 prerelease should always be considered to be + // below the 1.7.0-rc2 prerelease. + plugintest.TestExpectTFatal(t, func() { + r.UnitTest(&testinginterface.RuntimeT{}, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(version.Must(version.NewVersion("1.7.0-rc2"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) + }) +} + +func Test_RequireAbove_Prerelease_LowerCoreVersion(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc1") + + // The 1.8.0-rc1 prerelease should always be considered to be + // above the 1.7.0 core version. + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(version.Must(version.NewVersion("1.7.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_RequireAbove_Prerelease_LowerPrerelease(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc1") + + // The 1.8.0-rc1 prerelease should always be considered to be + // above the 1.8.0-beta1 prerelease. + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(version.Must(version.NewVersion("1.8.0-beta1"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} diff --git a/tfversion/require_below.go b/tfversion/require_below.go index 99efa5346..1b1f2cfb2 100644 --- a/tfversion/require_below.go +++ b/tfversion/require_below.go @@ -11,9 +11,19 @@ import ( ) // RequireBelow will fail the test if the Terraform CLI -// version is above the given version. For example, if given -// version.Must(version.NewVersion("0.15.0")), then versions 0.15.x and +// version is inclusively above the given version. For example, if given +// version.Must(version.NewVersion("1.8.0")), then versions 1.8.x and // above will fail the test. +// +// Prereleases of Terraform CLI (whether alpha, beta, or rc) are considered +// equal to a given patch version. For example, if given +// version.Must(version.NewVersion("1.8.0")), then 1.8.0-rc1 will fail, not run, +// the test. Terraform prereleases are considered as potential candidates for +// the upcoming version and therefore are treated as semantically equal for +// testing purposes. If failing prereleases of the same patch release is +// desired, give a lower prerelease version. For example, if given +// version.Must(version.NewVersion("1.8.0-rc1")), then 1.8.0-rc2 will fail the +// test. func RequireBelow(maximumVersion *version.Version) TerraformVersionCheck { return requireBelowCheck{ maximumVersion: maximumVersion, @@ -27,8 +37,17 @@ type requireBelowCheck struct { // CheckTerraformVersion satisfies the TerraformVersionCheck interface. func (s requireBelowCheck) CheckTerraformVersion(ctx context.Context, req CheckTerraformVersionRequest, resp *CheckTerraformVersionResponse) { + var terraformVersion *version.Version - if req.TerraformVersion.GreaterThan(s.maximumVersion) { + // If given a prerelease version, check the Terraform CLI version directly, + // otherwise use the core version so that prereleases are treated as equal. + if s.maximumVersion.Prerelease() != "" { + terraformVersion = req.TerraformVersion + } else { + terraformVersion = req.TerraformVersion.Core() + } + + if terraformVersion.GreaterThanOrEqual(s.maximumVersion) { resp.Error = fmt.Errorf("expected Terraform CLI version below %s but detected version is %s", s.maximumVersion, req.TerraformVersion) } diff --git a/tfversion/require_below_test.go b/tfversion/require_below_test.go index 9c3e40d8b..01d09d6b8 100644 --- a/tfversion/require_below_test.go +++ b/tfversion/require_below_test.go @@ -11,12 +11,36 @@ import ( r "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" + "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/tfversion" testinginterface "github.com/mitchellh/go-testing-interface" ) -func Test_RequireBelow(t *testing.T) { //nolint:paralleltest +func Test_RequireBelow_Equal(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.7.0") + + plugintest.TestExpectTFatal(t, func() { + r.UnitTest(&testinginterface.RuntimeT{}, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireBelow(version.Must(version.NewVersion("1.7.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) + }) +} + +func Test_RequireBelow_Lower(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.2.0") r.UnitTest(t, r.TestCase{ @@ -41,7 +65,8 @@ func Test_RequireBelow(t *testing.T) { //nolint:paralleltest }) } -func Test_RequireBelow_Error(t *testing.T) { //nolint:paralleltest +func Test_RequireBelow_Higher(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.4.0") plugintest.TestExpectTFatal(t, func() { @@ -62,3 +87,120 @@ func Test_RequireBelow_Error(t *testing.T) { //nolint:paralleltest }) }) } + +func Test_RequireBelow_Prerelease_EqualCoreVersion(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc1") + + // Pragmatic compromise that 1.8.0-rc1 prerelease is considered to + // be equivalent to the 1.8.0 core version. This enables developers + // to assert that prerelease versions are ran with upcoming + // core versions. + // + // Reference: https://github.com/hashicorp/terraform-plugin-testing/issues/303 + plugintest.TestExpectTFatal(t, func() { + r.UnitTest(&testinginterface.RuntimeT{}, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireBelow(version.Must(version.NewVersion("1.8.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) + }) +} + +func Test_RequireBelow_Prerelease_HigherCoreVersion(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.7.0-rc1") + + // The 1.7.0-rc1 prerelease should always be considered to be below the + // 1.8.0 core version. This intentionally verifies that the logic does not + // ignore the core version of the prerelease version when compared against + // the core version of the check. + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireBelow(version.Must(version.NewVersion("1.8.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_RequireBelow_Prerelease_HigherPrerelease(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.7.0-rc1") + + // The 1.7.0-rc1 prerelease should always be considered to be + // below the 1.7.0-rc2 prerelease. + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireBelow(version.Must(version.NewVersion("1.7.0-rc2"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_RequireBelow_Prerelease_LowerCoreVersion(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc1") + + // The 1.8.0-rc1 prerelease should always be considered to be + // above the 1.7.0 core version. + plugintest.TestExpectTFatal(t, func() { + r.UnitTest(&testinginterface.RuntimeT{}, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireBelow(version.Must(version.NewVersion("1.7.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) + }) +} + +func Test_RequireBelow_Prerelease_LowerPrerelease(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc1") + + // The 1.8.0-rc1 prerelease should always be considered to be + // above the 1.8.0-beta1 prerelease. + plugintest.TestExpectTFatal(t, func() { + r.UnitTest(&testinginterface.RuntimeT{}, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireBelow(version.Must(version.NewVersion("1.8.0-beta1"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) + }) +} diff --git a/tfversion/require_between.go b/tfversion/require_between.go index b99297928..ac0150773 100644 --- a/tfversion/require_between.go +++ b/tfversion/require_between.go @@ -12,9 +12,19 @@ import ( // RequireBetween will fail the test if the Terraform CLI // version is outside the given minimum (exclusive) and maximum (inclusive). -// For example, if given a minimum version of version.Must(version.NewVersion("0.15.0")) -// and a maximum version of version.Must(version.NewVersion("1.0.0")), then 0.15.x or -// any other prior versions and versions greater than 1.0.0 will fail the test. +// For example, if given a minimum version of version.Must(version.NewVersion("1.7.0")) +// and a maximum version of version.Must(version.NewVersion("1.8.0")), then 1.6.x or +// any other prior versions and versions greater than or equal to 1.8.0 will fail the test. +// +// Prereleases of Terraform CLI (whether alpha, beta, or rc) are considered +// equal to a given patch version. For example, if given a minimum version of +// version.Must(version.NewVersion("1.8.0")), then 1.8.0-rc1 will run, not fail, +// the test. Terraform prereleases are considered as potential candidates for +// the upcoming version and therefore are treated as semantically equal for +// testing purposes. If failing prereleases of the same patch release is +// desired, give a higher prerelease version. For example, if given a minimum +// version of version.Must(version.NewVersion("1.8.0-rc2")), then 1.8.0-rc1 will +// fail the test. func RequireBetween(minimumVersion, maximumVersion *version.Version) TerraformVersionCheck { return requireBetweenCheck{ minimumVersion: minimumVersion, @@ -30,8 +40,27 @@ type requireBetweenCheck struct { // CheckTerraformVersion satisfies the TerraformVersionCheck interface. func (s requireBetweenCheck) CheckTerraformVersion(ctx context.Context, req CheckTerraformVersionRequest, resp *CheckTerraformVersionResponse) { + var maxTerraformVersion, minTerraformVersion *version.Version - if req.TerraformVersion.LessThan(s.minimumVersion) || req.TerraformVersion.GreaterThanOrEqual(s.maximumVersion) { + // If given a prerelease maximum version, check the Terraform CLI version + // directly, otherwise use the core version so that prereleases are treated + // as equal. + if s.maximumVersion.Prerelease() != "" { + maxTerraformVersion = req.TerraformVersion + } else { + maxTerraformVersion = req.TerraformVersion.Core() + } + + // If given a prerelease minimum version, check the Terraform CLI version + // directly, otherwise use the core version so that prereleases are treated + // as equal. + if s.minimumVersion.Prerelease() != "" { + minTerraformVersion = req.TerraformVersion + } else { + minTerraformVersion = req.TerraformVersion.Core() + } + + if minTerraformVersion.LessThan(s.minimumVersion) || maxTerraformVersion.GreaterThanOrEqual(s.maximumVersion) { resp.Error = fmt.Errorf("expected Terraform CLI version between %s and %s but detected version is %s", s.minimumVersion, s.maximumVersion, req.TerraformVersion) } diff --git a/tfversion/require_between_test.go b/tfversion/require_between_test.go index c15e22006..9508ec2a3 100644 --- a/tfversion/require_between_test.go +++ b/tfversion/require_between_test.go @@ -11,12 +11,15 @@ import ( r "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" + "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/tfversion" testinginterface "github.com/mitchellh/go-testing-interface" ) func Test_RequireBetween(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.2.0") r.UnitTest(t, r.TestCase{ @@ -54,6 +57,7 @@ func Test_RequireBetween(t *testing.T) { //nolint:paralleltest } func Test_RequireBetween_Error_BelowMin(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.1.0") plugintest.TestExpectTFatal(t, func() { @@ -96,3 +100,235 @@ func Test_RequireBetween_Error_EqToMax(t *testing.T) { //nolint:paralleltest }) }) } + +func Test_RequireBetween_Prerelease_MaxEqualCoreVersion(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc1") + + // Pragmatic compromise that 1.8.0-rc1 prerelease is considered to + // be equivalent to the 1.8.0 core version. This enables developers + // to assert that prerelease versions are ran with upcoming + // core versions. + // + // Reference: https://github.com/hashicorp/terraform-plugin-testing/issues/303 + plugintest.TestExpectTFatal(t, func() { + r.UnitTest(&testinginterface.RuntimeT{}, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireBetween(version.Must(version.NewVersion("1.7.0")), version.Must(version.NewVersion("1.8.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) + }) +} + +func Test_RequireBetween_Prerelease_MinEqualCoreVersion(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc1") + + // Pragmatic compromise that 1.8.0-rc1 prerelease is considered to + // be equivalent to the 1.8.0 core version. This enables developers + // to assert that prerelease versions are ran with upcoming + // core versions. + // + // Reference: https://github.com/hashicorp/terraform-plugin-testing/issues/303 + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireBetween(version.Must(version.NewVersion("1.8.0")), version.Must(version.NewVersion("1.9.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_RequireBetween_Prerelease_MaxHigherCoreVersion(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.7.0-rc1") + + // The 1.7.0-rc1 prerelease should always be considered to be below the + // 1.8.0 core version. This intentionally verifies that the logic does not + // ignore the core version of the prerelease version when compared against + // the core version of the check. + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireBetween(version.Must(version.NewVersion("1.6.0")), version.Must(version.NewVersion("1.8.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_RequireBetween_Prerelease_MinHigherCoreVersion(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.7.0-rc1") + + // The 1.7.0-rc1 prerelease should always be considered to be below the + // 1.8.0 core version. This intentionally verifies that the logic does not + // ignore the core version of the prerelease version when compared against + // the core version of the check. + plugintest.TestExpectTFatal(t, func() { + r.UnitTest(&testinginterface.RuntimeT{}, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireBetween(version.Must(version.NewVersion("1.8.0")), version.Must(version.NewVersion("1.9.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) + }) +} + +func Test_RequireBetween_Prerelease_MaxHigherPrerelease(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.7.0-rc1") + + // The 1.7.0-rc1 prerelease should always be considered to be + // below the 1.7.0-rc2 prerelease. + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireBetween(version.Must(version.NewVersion("1.6.0")), version.Must(version.NewVersion("1.7.0-rc2"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_RequireBetween_Prerelease_MinHigherPrerelease(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.7.0-rc1") + + // The 1.7.0-rc1 prerelease should always be considered to be + // below the 1.7.0-rc2 prerelease. + plugintest.TestExpectTFatal(t, func() { + r.UnitTest(&testinginterface.RuntimeT{}, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireBetween(version.Must(version.NewVersion("1.7.0-rc2")), version.Must(version.NewVersion("1.8.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) + }) +} + +func Test_RequireBetween_Prerelease_MaxLowerCoreVersion(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc1") + + // The 1.8.0-rc1 prerelease should always be considered to be + // above the 1.7.0 core version. + plugintest.TestExpectTFatal(t, func() { + r.UnitTest(&testinginterface.RuntimeT{}, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireBetween(version.Must(version.NewVersion("1.6.0")), version.Must(version.NewVersion("1.7.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) + }) +} + +func Test_RequireBetween_Prerelease_MinLowerCoreVersion(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc1") + + // The 1.8.0-rc1 prerelease should always be considered to be + // above the 1.7.0 core version. + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireBetween(version.Must(version.NewVersion("1.7.0")), version.Must(version.NewVersion("1.9.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_RequireBetween_Prerelease_MaxLowerPrerelease(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc1") + + // The 1.8.0-rc1 prerelease should always be considered to be + // above the 1.8.0-beta1 prerelease. + plugintest.TestExpectTFatal(t, func() { + r.UnitTest(&testinginterface.RuntimeT{}, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireBetween(version.Must(version.NewVersion("1.7.0")), version.Must(version.NewVersion("1.8.0-beta1"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) + }) +} + +func Test_RequireBetween_Prerelease_MinLowerPrerelease(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc1") + + // The 1.8.0-rc1 prerelease should always be considered to be + // above the 1.8.0-beta1 prerelease. + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireBetween(version.Must(version.NewVersion("1.8.0-beta1")), version.Must(version.NewVersion("1.9.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} diff --git a/tfversion/require_not.go b/tfversion/require_not.go index 18a9b68d1..2addb2814 100644 --- a/tfversion/require_not.go +++ b/tfversion/require_not.go @@ -12,6 +12,16 @@ import ( // RequireNot will fail the test if the Terraform CLI // version matches the given version. +// +// Prereleases of Terraform CLI (whether alpha, beta, or rc) are considered +// equal to a given patch version. For example, if given +// version.Must(version.NewVersion("1.8.0")), then 1.8.0-rc1 will fail, not run, +// the test. Terraform prereleases are considered as potential candidates for +// the upcoming version and therefore are treated as semantically equal for +// testing purposes. If running prereleases of the same patch release is +// desired, give a different prerelease version. For example, if given +// version.Must(version.NewVersion("1.8.0-rc2")), then 1.8.0-rc1 will +// run the test. func RequireNot(version *version.Version) TerraformVersionCheck { return requireNotCheck{ version: version, @@ -25,8 +35,17 @@ type requireNotCheck struct { // CheckTerraformVersion satisfies the TerraformVersionCheck interface. func (s requireNotCheck) CheckTerraformVersion(ctx context.Context, req CheckTerraformVersionRequest, resp *CheckTerraformVersionResponse) { + var terraformVersion *version.Version - if req.TerraformVersion.Equal(s.version) { + // If given a prerelease version, check the Terraform CLI version directly, + // otherwise use the core version so that prereleases are treated as equal. + if s.version.Prerelease() != "" { + terraformVersion = req.TerraformVersion + } else { + terraformVersion = req.TerraformVersion.Core() + } + + if terraformVersion.Equal(s.version) { resp.Error = fmt.Errorf("unexpected Terraform CLI version: %s", s.version) } } diff --git a/tfversion/require_not_test.go b/tfversion/require_not_test.go index dee5968a6..6eba3e7ba 100644 --- a/tfversion/require_not_test.go +++ b/tfversion/require_not_test.go @@ -11,19 +11,20 @@ import ( r "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" + "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/tfversion" testinginterface "github.com/mitchellh/go-testing-interface" ) func Test_RequireNot(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.4.3") r.UnitTest(t, r.TestCase{ ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature - return nil, nil - }, + "test": providerserver.NewProviderServer(testprovider.Provider{}), }, TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.RequireNot(version.Must(version.NewVersion("1.1.0"))), @@ -37,6 +38,7 @@ func Test_RequireNot(t *testing.T) { //nolint:paralleltest } func Test_RequireNot_Error(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.1.0") plugintest.TestExpectTFatal(t, func() { @@ -57,3 +59,116 @@ func Test_RequireNot_Error(t *testing.T) { //nolint:paralleltest }) }) } + +func Test_RequireNot_Prerelease_EqualCoreVersion(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc1") + + // Pragmatic compromise that 1.8.0-rc1 prerelease is considered to + // be equivalent to the 1.8.0 core version. This enables developers + // to assert that prerelease versions are not ran with upcoming + // core versions. + // + // Reference: https://github.com/hashicorp/terraform-plugin-testing/issues/303 + plugintest.TestExpectTFatal(t, func() { + r.UnitTest(&testinginterface.RuntimeT{}, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireNot(version.Must(version.NewVersion("1.8.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) + }) +} + +func Test_RequireNot_Prerelease_HigherCoreVersion(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.7.0-rc1") + + // The 1.7.0-rc1 prerelease should always be considered to be below the + // 1.8.0 core version. This intentionally verifies that the logic does not + // ignore the core version of the prerelease version when compared against + // the core version of the check. + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireNot(version.Must(version.NewVersion("1.8.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_RequireNot_Prerelease_HigherPrerelease(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.7.0-rc1") + + // The 1.7.0-rc1 prerelease should always be considered to be + // below the 1.7.0-rc2 prerelease. + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireNot(version.Must(version.NewVersion("1.7.0-rc2"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_RequireNot_Prerelease_LowerCoreVersion(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc1") + + // The 1.8.0-rc1 prerelease should always be considered to be + // above the 1.7.0 core version. + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireNot(version.Must(version.NewVersion("1.7.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_RequireNot_Prerelease_LowerPrerelease(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc1") + + // The 1.8.0-rc1 prerelease should always be considered to be + // above the 1.8.0-beta1 prerelease. + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireNot(version.Must(version.NewVersion("1.8.0-beta1"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} diff --git a/tfversion/skip_above.go b/tfversion/skip_above.go index ffc69b857..b56918c65 100644 --- a/tfversion/skip_above.go +++ b/tfversion/skip_above.go @@ -11,9 +11,19 @@ import ( ) // SkipAbove will skip (pass) the test if the Terraform CLI -// version is below the given version. For example, if given -// version.Must(version.NewVersion("0.15.0")), then 0.14.x or -// any other prior minor versions will skip the test. +// version is exclusively above the given version. For example, if given +// version.Must(version.NewVersion("1.8.0")), then 1.8.x or +// any other later versions will skip the test. +// +// Prereleases of Terraform CLI (whether alpha, beta, or rc) are considered +// equal to a given patch version. For example, if given +// version.Must(version.NewVersion("1.8.0")), then 1.8.0-rc1 will run, not skip, +// the test. Terraform prereleases are considered as potential candidates for +// the upcoming version and therefore are treated as semantically equal for +// testing. If skipping prereleases of the same patch release is desired, give a +// lower prerelease version. For example, if given +// version.Must(version.NewVersion("1.8.0-rc1")), then 1.8.0-rc2 will skip the +// test. func SkipAbove(maximumVersion *version.Version) TerraformVersionCheck { return skipAboveCheck{ maximumVersion: maximumVersion, @@ -27,8 +37,17 @@ type skipAboveCheck struct { // CheckTerraformVersion satisfies the TerraformVersionCheck interface. func (s skipAboveCheck) CheckTerraformVersion(ctx context.Context, req CheckTerraformVersionRequest, resp *CheckTerraformVersionResponse) { + var terraformVersion *version.Version - if req.TerraformVersion.GreaterThan(s.maximumVersion) { + // If given a prerelease version, check the Terraform CLI version directly, + // otherwise use the core version so that prereleases are treated as equal. + if s.maximumVersion.Prerelease() != "" { + terraformVersion = req.TerraformVersion + } else { + terraformVersion = req.TerraformVersion.Core() + } + + if terraformVersion.GreaterThan(s.maximumVersion) { resp.Skip = fmt.Sprintf("Terraform CLI version %s is above maximum version %s: skipping test", req.TerraformVersion, s.maximumVersion) } diff --git a/tfversion/skip_above_test.go b/tfversion/skip_above_test.go index 4390cc9cf..db9898c37 100644 --- a/tfversion/skip_above_test.go +++ b/tfversion/skip_above_test.go @@ -10,10 +10,13 @@ import ( "github.com/hashicorp/terraform-plugin-go/tfprotov6" r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" "github.com/hashicorp/terraform-plugin-testing/tfversion" ) -func Test_SkipAbove_SkipTest(t *testing.T) { //nolint:paralleltest +func Test_SkipAbove_Lower(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.3.0") r.UnitTest(t, r.TestCase{ @@ -38,7 +41,8 @@ func Test_SkipAbove_SkipTest(t *testing.T) { //nolint:paralleltest }) } -func Test_SkipAbove_RunTest(t *testing.T) { //nolint:paralleltest +func Test_SkipAbove_Equal(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.2.9") r.UnitTest(t, r.TestCase{ @@ -62,3 +66,152 @@ func Test_SkipAbove_RunTest(t *testing.T) { //nolint:paralleltest }, }) } + +func Test_SkipAbove_Higher(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.7.1") + + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipAbove(version.Must(version.NewVersion("1.7.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_SkipAbove_Prerelease_EqualCoreVersion(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc1") + + // Pragmatic compromise that 1.8.0-rc1 prerelease is considered to + // be equivalent to the 1.8.0 core version. This enables developers + // to assert that prerelease versions are skipped with upcoming + // core versions. + // + // Reference: https://github.com/hashicorp/terraform-plugin-testing/issues/303 + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipAbove(version.Must(version.NewVersion("1.8.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_SkipAbove_Prerelease_EqualPrerelease(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc1") + + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipAbove(version.Must(version.NewVersion("1.8.0-rc1"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_SkipAbove_Prerelease_HigherCoreVersion(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.7.0-rc1") + + // The 1.7.0-rc1 prerelease should always be considered to be below the + // 1.8.0 core version. This intentionally verifies that the logic does not + // ignore the core version of the prerelease version when compared against + // the core version of the check. + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipAbove(version.Must(version.NewVersion("1.8.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_SkipAbove_Prerelease_HigherPrerelease(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.7.0-rc1") + + // The 1.7.0-rc1 prerelease should always be considered to be + // below the 1.7.0-rc2 prerelease. + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipAbove(version.Must(version.NewVersion("1.7.0-rc2"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_SkipAbove_Prerelease_LowerCoreVersion(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc1") + + // The 1.8.0-rc1 prerelease should always be considered to be + // above the 1.7.0 core version. + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipAbove(version.Must(version.NewVersion("1.7.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_SkipAbove_Prerelease_LowerPrerelease(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc1") + + // The 1.8.0-rc1 prerelease should always be considered to be + // above the 1.8.0-beta1 prerelease. + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipAbove(version.Must(version.NewVersion("1.8.0-beta1"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} diff --git a/tfversion/skip_below.go b/tfversion/skip_below.go index 0b3dffddc..15cb8a498 100644 --- a/tfversion/skip_below.go +++ b/tfversion/skip_below.go @@ -11,9 +11,19 @@ import ( ) // SkipBelow will skip (pass) the test if the Terraform CLI -// version is below the given version. For example, if given -// version.Must(version.NewVersion("0.15.0")), then 0.14.x or -// any other prior minor versions will skip the test. +// version is exclusively below the given version. For example, if given +// version.Must(version.NewVersion("1.8.0")), then 1.7.x or +// any other prior versions will skip the test. +// +// Prereleases of Terraform CLI (whether alpha, beta, or rc) are considered +// equal to a given patch version. For example, if given +// version.Must(version.NewVersion("1.8.0")), then 1.8.0-rc1 will run, not skip, +// the test. Terraform prereleases are considered as potential candidates for +// the upcoming version and therefore are treated as important for testing to +// run. If skipping prereleases of the same patch release is desired, give a +// higher prerelease version. For example, if given +// version.Must(version.NewVersion("1.8.0-rc2")), then 1.8.0-rc1 will skip the +// test. func SkipBelow(minimumVersion *version.Version) TerraformVersionCheck { return skipBelowCheck{ minimumVersion: minimumVersion, @@ -27,8 +37,17 @@ type skipBelowCheck struct { // CheckTerraformVersion satisfies the TerraformVersionCheck interface. func (s skipBelowCheck) CheckTerraformVersion(ctx context.Context, req CheckTerraformVersionRequest, resp *CheckTerraformVersionResponse) { + var terraformVersion *version.Version - if req.TerraformVersion.LessThan(s.minimumVersion) { + // If given a prerelease version, check the Terraform CLI version directly, + // otherwise use the core version so that prereleases are treated as equal. + if s.minimumVersion.Prerelease() != "" { + terraformVersion = req.TerraformVersion + } else { + terraformVersion = req.TerraformVersion.Core() + } + + if terraformVersion.LessThan(s.minimumVersion) { resp.Skip = fmt.Sprintf("Terraform CLI version %s is below minimum version %s: skipping test", req.TerraformVersion, s.minimumVersion) } diff --git a/tfversion/skip_below_test.go b/tfversion/skip_below_test.go index e8c43089e..4999a2dea 100644 --- a/tfversion/skip_below_test.go +++ b/tfversion/skip_below_test.go @@ -10,10 +10,13 @@ import ( "github.com/hashicorp/terraform-plugin-go/tfprotov6" r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" "github.com/hashicorp/terraform-plugin-testing/tfversion" ) -func Test_SkipBelow_SkipTest(t *testing.T) { //nolint:paralleltest +func Test_SkipBelow_Lower(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.0.7") r.UnitTest(t, r.TestCase{ @@ -37,15 +40,37 @@ func Test_SkipBelow_SkipTest(t *testing.T) { //nolint:paralleltest }) } -func Test_SkipBelow_RunTest(t *testing.T) { //nolint:paralleltest +func Test_SkipBelow_Equal(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.1.0") r.UnitTest(t, r.TestCase{ ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature - return nil, nil + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.1.0"))), + }, + Steps: []r.TestStep{ + { + //nullable argument only available in TF v1.1.0+ + Config: `variable "a" { + nullable = true + default = "hello" + }`, }, }, + }) +} + +func Test_SkipBelow_Higher(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.1.1") + + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.SkipBelow(version.Must(version.NewVersion("1.1.0"))), }, @@ -60,3 +85,133 @@ func Test_SkipBelow_RunTest(t *testing.T) { //nolint:paralleltest }, }) } + +func Test_SkipBelow_Prerelease_EqualCoreVersion(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc1") + + // Pragmatic compromise that 1.8.0-rc1 prerelease is considered to + // be equivalent to the 1.8.0 core release. This enables developers + // to assert that prerelease versions are compatible with upcoming + // core versions. + // + // Reference: https://github.com/hashicorp/terraform-plugin-testing/issues/303 + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.8.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_SkipBelow_Prerelease_EqualPrerelease(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc1") + + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.8.0-rc1"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_SkipBelow_Prerelease_HigherCoreVersion(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.7.0-rc1") + + // The 1.7.0-rc1 prerelease should always be considered to be below the + // 1.8.0 core version. This intentionally verifies that the logic does not + // ignore the core version of the prerelease version when compared against + // the core version of the check. + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.8.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_SkipBelow_Prerelease_HigherPrerelease(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.7.0-rc1") + + // The 1.7.0-rc1 prerelease should always be considered to be + // below the 1.7.0-rc2 prerelease. + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.7.0-rc2"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_SkipBelow_Prerelease_LowerCoreVersion(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc1") + + // The 1.8.0-rc1 prerelease should always be considered to be + // above the 1.7.0 core version. + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.7.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_SkipBelow_Prerelease_LowerPrerelease(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc1") + + // The 1.8.0-rc1 prerelease should always be considered to be + // above the 1.8.0-beta1 prerelease. + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.8.0-beta1"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} diff --git a/tfversion/skip_between.go b/tfversion/skip_between.go index fb6e94108..555ff79cc 100644 --- a/tfversion/skip_between.go +++ b/tfversion/skip_between.go @@ -12,9 +12,19 @@ import ( // SkipBetween will skip the test if the Terraform CLI // version is between the given minimum (inclusive) and maximum (exclusive). -// For example, if given a minimum version of version.Must(version.NewVersion("0.15.0")) -// and a maximum version of version.Must(version.NewVersion("0.16.0")), then versions 0.15.x +// For example, if given a minimum version of version.Must(version.NewVersion("1.7.0")) +// and a maximum version of version.Must(version.NewVersion("1.8.0")), then versions 1.7.x // will skip the test. +// +// Prereleases of Terraform CLI (whether alpha, beta, or rc) are considered +// equal to a given patch version. For example, if given a minimum version of +// version.Must(version.NewVersion("1.8.0")), then 1.8.0-rc1 will skip, not run, +// the test. Terraform prereleases are considered as potential candidates for +// the upcoming version and therefore are treated as semantically equal for +// testing purposes. If running prereleases of the same patch release is +// desired, give a higher prerelease version. For example, if given a minimum +// version of version.Must(version.NewVersion("1.8.0-rc2")), then 1.8.0-rc1 will +// run the test. func SkipBetween(minimumVersion, maximumVersion *version.Version) TerraformVersionCheck { return skipBetweenCheck{ minimumVersion: minimumVersion, @@ -30,8 +40,27 @@ type skipBetweenCheck struct { // CheckTerraformVersion satisfies the TerraformVersionCheck interface. func (s skipBetweenCheck) CheckTerraformVersion(ctx context.Context, req CheckTerraformVersionRequest, resp *CheckTerraformVersionResponse) { + var maxTerraformVersion, minTerraformVersion *version.Version - if req.TerraformVersion.GreaterThanOrEqual(s.minimumVersion) && req.TerraformVersion.LessThan(s.maximumVersion) { + // If given a prerelease maximum version, check the Terraform CLI version + // directly, otherwise use the core version so that prereleases are treated + // as equal. + if s.maximumVersion.Prerelease() != "" { + maxTerraformVersion = req.TerraformVersion + } else { + maxTerraformVersion = req.TerraformVersion.Core() + } + + // If given a prerelease minimum version, check the Terraform CLI version + // directly, otherwise use the core version so that prereleases are treated + // as equal. + if s.minimumVersion.Prerelease() != "" { + minTerraformVersion = req.TerraformVersion + } else { + minTerraformVersion = req.TerraformVersion.Core() + } + + if minTerraformVersion.GreaterThanOrEqual(s.minimumVersion) && maxTerraformVersion.LessThan(s.maximumVersion) { resp.Skip = fmt.Sprintf("Terraform CLI version %s is between %s and %s: skipping test.", req.TerraformVersion, s.minimumVersion, s.maximumVersion) } diff --git a/tfversion/skip_between_test.go b/tfversion/skip_between_test.go index 098c03d20..35ced5279 100644 --- a/tfversion/skip_between_test.go +++ b/tfversion/skip_between_test.go @@ -10,12 +10,15 @@ import ( "github.com/hashicorp/terraform-plugin-go/tfprotov6" r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" "github.com/hashicorp/terraform-plugin-testing/tfversion" testinginterface "github.com/mitchellh/go-testing-interface" ) func Test_SkipBetween_SkipTest(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.2.0") r.UnitTest(t, r.TestCase{ @@ -53,13 +56,12 @@ func Test_SkipBetween_SkipTest(t *testing.T) { //nolint:paralleltest } func Test_SkipBetween_RunTest_AboveMax(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.3.0") r.UnitTest(&testinginterface.RuntimeT{}, r.TestCase{ ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature - return nil, nil - }, + "test": providerserver.NewProviderServer(testprovider.Provider{}), }, TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.SkipBetween(version.Must(version.NewVersion("1.2.0")), version.Must(version.NewVersion("1.3.0"))), @@ -73,13 +75,12 @@ func Test_SkipBetween_RunTest_AboveMax(t *testing.T) { //nolint:paralleltest } func Test_SkipBetween_RunTest_EqToMin(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.2.0") r.UnitTest(&testinginterface.RuntimeT{}, r.TestCase{ ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature - return nil, nil - }, + "test": providerserver.NewProviderServer(testprovider.Provider{}), }, TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.SkipBetween(version.Must(version.NewVersion("1.2.0")), version.Must(version.NewVersion("1.3.0"))), @@ -91,3 +92,225 @@ func Test_SkipBetween_RunTest_EqToMin(t *testing.T) { //nolint:paralleltest }, }) } + +func Test_SkipBetween_Prerelease_MaxEqualCoreVersion(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc1") + + // Pragmatic compromise that 1.8.0-rc1 prerelease is considered to + // be equivalent to the 1.8.0 core version. This enables developers + // to assert that prerelease versions are ran with upcoming + // core versions. + // + // Reference: https://github.com/hashicorp/terraform-plugin-testing/issues/303 + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBetween(version.Must(version.NewVersion("1.7.0")), version.Must(version.NewVersion("1.8.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_SkipBetween_Prerelease_MinEqualCoreVersion(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc1") + + // Pragmatic compromise that 1.8.0-rc1 prerelease is considered to + // be equivalent to the 1.8.0 core version. This enables developers + // to assert that prerelease versions are ran with upcoming + // core versions. + // + // Reference: https://github.com/hashicorp/terraform-plugin-testing/issues/303 + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBetween(version.Must(version.NewVersion("1.8.0")), version.Must(version.NewVersion("1.9.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_SkipBetween_Prerelease_MaxHigherCoreVersion(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.7.0-rc1") + + // The 1.7.0-rc1 prerelease should always be considered to be below the + // 1.8.0 core version. This intentionally verifies that the logic does not + // ignore the core version of the prerelease version when compared against + // the core version of the check. + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBetween(version.Must(version.NewVersion("1.6.0")), version.Must(version.NewVersion("1.8.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_SkipBetween_Prerelease_MinHigherCoreVersion(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.7.0-rc1") + + // The 1.7.0-rc1 prerelease should always be considered to be below the + // 1.8.0 core version. This intentionally verifies that the logic does not + // ignore the core version of the prerelease version when compared against + // the core version of the check. + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBetween(version.Must(version.NewVersion("1.8.0")), version.Must(version.NewVersion("1.9.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_SkipBetween_Prerelease_MaxHigherPrerelease(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.7.0-rc1") + + // The 1.7.0-rc1 prerelease should always be considered to be + // below the 1.7.0-rc2 prerelease. + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBetween(version.Must(version.NewVersion("1.6.0")), version.Must(version.NewVersion("1.7.0-rc2"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_SkipBetween_Prerelease_MinHigherPrerelease(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.7.0-rc1") + + // The 1.7.0-rc1 prerelease should always be considered to be + // below the 1.7.0-rc2 prerelease. + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBetween(version.Must(version.NewVersion("1.7.0-rc2")), version.Must(version.NewVersion("1.8.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_SkipBetween_Prerelease_MaxLowerCoreVersion(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc1") + + // The 1.8.0-rc1 prerelease should always be considered to be + // above the 1.7.0 core version. + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBetween(version.Must(version.NewVersion("1.6.0")), version.Must(version.NewVersion("1.7.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_SkipBetween_Prerelease_MinLowerCoreVersion(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc1") + + // The 1.8.0-rc1 prerelease should always be considered to be + // above the 1.7.0 core version. + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBetween(version.Must(version.NewVersion("1.7.0")), version.Must(version.NewVersion("1.9.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_SkipBetween_Prerelease_MaxLowerPrerelease(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc1") + + // The 1.8.0-rc1 prerelease should always be considered to be + // above the 1.8.0-beta1 prerelease. + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBetween(version.Must(version.NewVersion("1.7.0")), version.Must(version.NewVersion("1.8.0-beta1"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_SkipBetween_Prerelease_MinLowerPrerelease(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc1") + + // The 1.8.0-rc1 prerelease should always be considered to be + // above the 1.8.0-beta1 prerelease. + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBetween(version.Must(version.NewVersion("1.8.0-beta1")), version.Must(version.NewVersion("1.9.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} diff --git a/tfversion/skip_if.go b/tfversion/skip_if.go index 6ece5e05d..e5b7d96fc 100644 --- a/tfversion/skip_if.go +++ b/tfversion/skip_if.go @@ -12,6 +12,16 @@ import ( // SkipIf will skip (pass) the test if the Terraform CLI // version matches the given version. +// +// Prereleases of Terraform CLI (whether alpha, beta, or rc) are considered +// equal to a given patch version. For example, if given +// version.Must(version.NewVersion("1.8.0")), then 1.8.0-rc1 will skip, not run, +// the test. Terraform prereleases are considered as potential candidates for +// the upcoming version and therefore are treated as semantically equal for +// testing purposes. If running prereleases of the same patch release is +// desired, give a different prerelease version. For example, if given +// version.Must(version.NewVersion("1.8.0-rc2")), then 1.8.0-rc1 will +// run the test. func SkipIf(version *version.Version) TerraformVersionCheck { return skipIfCheck{ version: version, @@ -25,8 +35,17 @@ type skipIfCheck struct { // CheckTerraformVersion satisfies the TerraformVersionCheck interface. func (s skipIfCheck) CheckTerraformVersion(ctx context.Context, req CheckTerraformVersionRequest, resp *CheckTerraformVersionResponse) { + var terraformVersion *version.Version - if req.TerraformVersion.Equal(s.version) { + // If given a prerelease version, check the Terraform CLI version directly, + // otherwise use the core version so that prereleases are treated as equal. + if s.version.Prerelease() != "" { + terraformVersion = req.TerraformVersion + } else { + terraformVersion = req.TerraformVersion.Core() + } + + if terraformVersion.Equal(s.version) { resp.Skip = fmt.Sprintf("Terraform CLI version is %s: skipping test.", s.version) } } diff --git a/tfversion/skip_if_not_alpha.go b/tfversion/skip_if_not_alpha.go new file mode 100644 index 000000000..413ee1b43 --- /dev/null +++ b/tfversion/skip_if_not_alpha.go @@ -0,0 +1,31 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfversion + +import ( + "context" + "fmt" + "strings" +) + +// SkipIfNotAlpha will skip (pass) the test if the Terraform CLI +// version is not an alpha prerelease (for example, 1.10.0-alpha20241023). +// +// Alpha builds of Terraform include experimental features, so this version check +// can be used for acceptance testing of experimental features, such as deferred actions. +func SkipIfNotAlpha() TerraformVersionCheck { + return skipIfNotAlphaCheck{} +} + +// skipIfNotAlphaCheck implements the TerraformVersionCheck interface +type skipIfNotAlphaCheck struct{} + +// CheckTerraformVersion satisfies the TerraformVersionCheck interface. +func (s skipIfNotAlphaCheck) CheckTerraformVersion(ctx context.Context, req CheckTerraformVersionRequest, resp *CheckTerraformVersionResponse) { + if strings.Contains(req.TerraformVersion.Prerelease(), "alpha") { + return + } + + resp.Skip = fmt.Sprintf("Terraform CLI version %s is not an alpha build: skipping test.", req.TerraformVersion) +} diff --git a/tfversion/skip_if_not_alpha_test.go b/tfversion/skip_if_not_alpha_test.go new file mode 100644 index 000000000..f778968a9 --- /dev/null +++ b/tfversion/skip_if_not_alpha_test.go @@ -0,0 +1,94 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfversion_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + + testinginterface "github.com/mitchellh/go-testing-interface" +) + +func Test_SkipIfNotAlpha_SkipTest_Stable(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0") + + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature + return nil, nil + }, + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipIfNotAlpha(), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_SkipIfNotAlpha_SkipTest_Beta1(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-beta1") + + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipIfNotAlpha(), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} +func Test_SkipIfNotAlpha_SkipTest_RC(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc2") + + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipIfNotAlpha(), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_SkipIfNotAlpha_RunTest_Alpha(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.9.0-alpha20240501") + + r.UnitTest(&testinginterface.RuntimeT{}, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipIfNotAlpha(), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} diff --git a/tfversion/skip_if_not_prerelease.go b/tfversion/skip_if_not_prerelease.go new file mode 100644 index 000000000..c49400b29 --- /dev/null +++ b/tfversion/skip_if_not_prerelease.go @@ -0,0 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfversion + +import ( + "context" + "fmt" +) + +// SkipIfNotPrerelease will skip (pass) the test if the Terraform CLI +// version does not include prerelease information. This will include builds +// of Terraform that are from source. (e.g. 1.8.0-dev) +func SkipIfNotPrerelease() TerraformVersionCheck { + return skipIfNotPrereleaseCheck{} +} + +// skipIfNotPrereleaseCheck implements the TerraformVersionCheck interface +type skipIfNotPrereleaseCheck struct{} + +// CheckTerraformVersion satisfies the TerraformVersionCheck interface. +func (s skipIfNotPrereleaseCheck) CheckTerraformVersion(ctx context.Context, req CheckTerraformVersionRequest, resp *CheckTerraformVersionResponse) { + if req.TerraformVersion.Prerelease() != "" { + return + } + + resp.Skip = fmt.Sprintf("Terraform CLI version %s is not a prerelease build: skipping test.", req.TerraformVersion) +} diff --git a/tfversion/skip_if_not_prerelease_test.go b/tfversion/skip_if_not_prerelease_test.go new file mode 100644 index 000000000..545017ed0 --- /dev/null +++ b/tfversion/skip_if_not_prerelease_test.go @@ -0,0 +1,94 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfversion_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + + testinginterface "github.com/mitchellh/go-testing-interface" +) + +func Test_SkipIfNotPrerelease_SkipTest_Stable(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0") + + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature + return nil, nil + }, + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipIfNotPrerelease(), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_SkipIfNotPrerelease_RunTest_Alpha(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.9.0-alpha20240501") + + r.UnitTest(&testinginterface.RuntimeT{}, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipIfNotPrerelease(), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_SkipIfNotPrerelease_RunTest_Beta1(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-beta1") + + r.UnitTest(&testinginterface.RuntimeT{}, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipIfNotPrerelease(), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} +func Test_SkipIfNotPrerelease_RunTest_RC(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc2") + + r.UnitTest(&testinginterface.RuntimeT{}, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipIfNotPrerelease(), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} diff --git a/tfversion/skip_if_test.go b/tfversion/skip_if_test.go index 8e9a8be9e..5b645b954 100644 --- a/tfversion/skip_if_test.go +++ b/tfversion/skip_if_test.go @@ -10,12 +10,15 @@ import ( "github.com/hashicorp/terraform-plugin-go/tfprotov6" r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" "github.com/hashicorp/terraform-plugin-testing/tfversion" testinginterface "github.com/mitchellh/go-testing-interface" ) func Test_SkipIf_SkipTest(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.4.3") r.UnitTest(t, r.TestCase{ @@ -36,13 +39,12 @@ func Test_SkipIf_SkipTest(t *testing.T) { //nolint:paralleltest } func Test_SkipIf_RunTest(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.1.0") r.UnitTest(&testinginterface.RuntimeT{}, r.TestCase{ ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature - return nil, nil - }, + "test": providerserver.NewProviderServer(testprovider.Provider{}), }, TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.SkipIf(version.Must(version.NewVersion("1.2.0"))), @@ -54,3 +56,114 @@ func Test_SkipIf_RunTest(t *testing.T) { //nolint:paralleltest }, }) } + +func Test_SkipIf_Prerelease_EqualCoreVersion(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc1") + + // Pragmatic compromise that 1.8.0-rc1 prerelease is considered to + // be equivalent to the 1.8.0 core version. This enables developers + // to assert that prerelease versions are skipped with upcoming + // core versions. + // + // Reference: https://github.com/hashicorp/terraform-plugin-testing/issues/303 + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipIf(version.Must(version.NewVersion("1.8.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_SkipIf_Prerelease_HigherCoreVersion(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.7.0-rc1") + + // The 1.7.0-rc1 prerelease should always be considered to be below the + // 1.8.0 core version. This intentionally verifies that the logic does not + // ignore the core version of the prerelease version when compared against + // the core version of the check. + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipIf(version.Must(version.NewVersion("1.8.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_SkipIf_Prerelease_HigherPrerelease(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.7.0-rc1") + + // The 1.7.0-rc1 prerelease should always be considered to be + // below the 1.7.0-rc2 prerelease. + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipIf(version.Must(version.NewVersion("1.7.0-rc2"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_SkipIf_Prerelease_LowerCoreVersion(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc1") + + // The 1.8.0-rc1 prerelease should always be considered to be + // above the 1.7.0 core version. + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipIf(version.Must(version.NewVersion("1.7.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_SkipIf_Prerelease_LowerPrerelease(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc1") + + // The 1.8.0-rc1 prerelease should always be considered to be + // above the 1.8.0-beta1 prerelease. + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipIf(version.Must(version.NewVersion("1.8.0-beta1"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} diff --git a/tfversion/versions.go b/tfversion/versions.go index 6cd04b27f..dc5cc7dfe 100644 --- a/tfversion/versions.go +++ b/tfversion/versions.go @@ -28,5 +28,17 @@ var ( Version1_2_0 *version.Version = version.Must(version.NewVersion("1.2.0")) Version1_3_0 *version.Version = version.Must(version.NewVersion("1.3.0")) Version1_4_0 *version.Version = version.Must(version.NewVersion("1.4.0")) + // Version1_4_6 fixed inclusion of sensitive values in `terraform show -json` output. + // Reference: https://github.com/hashicorp/terraform/releases/tag/v1.4.6 + Version1_4_6 *version.Version = version.Must(version.NewVersion("1.4.6")) Version1_5_0 *version.Version = version.Must(version.NewVersion("1.5.0")) + Version1_6_0 *version.Version = version.Must(version.NewVersion("1.6.0")) + Version1_7_0 *version.Version = version.Must(version.NewVersion("1.7.0")) + Version1_8_0 *version.Version = version.Must(version.NewVersion("1.8.0")) + Version1_9_0 *version.Version = version.Must(version.NewVersion("1.9.0")) + Version1_10_0 *version.Version = version.Must(version.NewVersion("1.10.0")) + Version1_11_0 *version.Version = version.Must(version.NewVersion("1.11.0")) + Version1_12_0 *version.Version = version.Must(version.NewVersion("1.12.0")) + Version1_13_0 *version.Version = version.Must(version.NewVersion("1.13.0")) + Version1_14_0 *version.Version = version.Must(version.NewVersion("1.14.0")) ) diff --git a/tools/go.mod b/tools/go.mod index 6d5aaf998..1ad4d797a 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -1,63 +1,57 @@ module tools -go 1.19 +go 1.24.0 -require github.com/hashicorp/copywrite v0.16.3 +require github.com/hashicorp/copywrite v0.22.0 require ( - github.com/AlecAivazis/survey/v2 v2.3.6 // indirect + github.com/AlecAivazis/survey/v2 v2.3.7 // indirect + github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect github.com/bmatcuk/doublestar/v4 v4.6.0 // indirect - github.com/bradleyfalzon/ghinstallation/v2 v2.1.0 // indirect - github.com/cli/go-gh v1.0.0 // indirect + github.com/bradleyfalzon/ghinstallation/v2 v2.5.0 // indirect + github.com/cli/go-gh/v2 v2.12.1 // indirect github.com/cli/safeexec v1.0.0 // indirect - github.com/cli/shurcooL-graphql v0.0.2 // indirect + github.com/cloudflare/circl v1.6.1 // indirect github.com/fatih/color v1.13.0 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/go-openapi/errors v0.20.2 // indirect github.com/go-openapi/strfmt v0.21.3 // indirect - github.com/golang-jwt/jwt/v4 v4.4.2 // indirect - github.com/golang/protobuf v1.5.2 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/google/go-github/v45 v45.2.0 // indirect + github.com/google/go-github/v53 v53.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect - github.com/hashicorp/go-hclog v1.4.0 // indirect + github.com/hashicorp/go-hclog v1.5.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/henvic/httpretty v0.0.6 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/jedib0t/go-pretty v4.3.0+incompatible // indirect - github.com/jedib0t/go-pretty/v6 v6.4.4 // indirect + github.com/jedib0t/go-pretty/v6 v6.4.6 // indirect github.com/joho/godotenv v1.3.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/knadh/koanf v1.5.0 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.17 // indirect - github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mergestat/timediff v0.0.3 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/muesli/termenv v0.12.0 // indirect github.com/oklog/ulid v1.3.1 // indirect - github.com/rivo/uniseg v0.2.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/samber/lo v1.37.0 // indirect github.com/spf13/cobra v1.6.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/thanhpk/randstr v1.0.4 // indirect - github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e // indirect go.mongodb.org/mongo-driver v1.10.0 // indirect - golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect + golang.org/x/crypto v0.36.0 // indirect golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect - golang.org/x/net v0.7.0 // indirect - golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect - golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.5.0 // indirect - golang.org/x/term v0.5.0 // indirect - golang.org/x/text v0.7.0 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.28.0 // indirect + golang.org/x/oauth2 v0.27.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/term v0.30.0 // indirect + golang.org/x/text v0.23.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/tools/go.sum b/tools/go.sum index 90da047a1..d064f5bfe 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -1,43 +1,15 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/AlecAivazis/survey/v2 v2.3.6 h1:NvTuVHISgTHEHeBFqt6BHOe4Ny/NwGZr7w+F8S9ziyw= -github.com/AlecAivazis/survey/v2 v2.3.6/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI= +cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= +github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= +github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA= +github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -66,20 +38,21 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bmatcuk/doublestar/v4 v4.6.0 h1:HTuxyug8GyFbRkrffIpzNCSK4luc0TY3wzXvzIZhEXc= github.com/bmatcuk/doublestar/v4 v4.6.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= -github.com/bradleyfalzon/ghinstallation/v2 v2.1.0 h1:5+NghM1Zred9Z078QEZtm28G/kfDfZN/92gkDlLwGVA= -github.com/bradleyfalzon/ghinstallation/v2 v2.1.0/go.mod h1:Xg3xPRN5Mcq6GDqeUVhFbjEWMb4JHCyWEeeBGEYQoTU= +github.com/bradleyfalzon/ghinstallation/v2 v2.5.0 h1:yaYcGQ7yEIGbsJfW/9z7v1sLiZg/5rSNNXwmMct5XaE= +github.com/bradleyfalzon/ghinstallation/v2 v2.5.0/go.mod h1:amcvPQMrRkWNdueWOjPytGL25xQGzox7425qMgzo+Vo= +github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/cli/go-gh v1.0.0 h1:zE1YUAUYqGXNZuICEBeOkIMJ5F50BS0ftvtoWGlsEFI= -github.com/cli/go-gh v1.0.0/go.mod h1:bqxLdCoTZ73BuiPEJx4olcO/XKhVZaFDchFagYRBweE= +github.com/cli/go-gh/v2 v2.12.1 h1:SVt1/afj5FRAythyMV3WJKaUfDNsxXTIe7arZbwTWKA= +github.com/cli/go-gh/v2 v2.12.1/go.mod h1:+5aXmEOJsH9fc9mBHfincDwnS02j2AIA/DsTH0Bk5uw= github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI= github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= -github.com/cli/shurcooL-graphql v0.0.2 h1:rwP5/qQQ2fM0TzkUTwtt6E2LbIYf6R+39cUXTa04NYk= -github.com/cli/shurcooL-graphql v0.0.2/go.mod h1:tlrLmw/n5Q/+4qSvosT+9/W5zc8ZMjnJeYBxSdb4nWA= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -106,9 +79,6 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4 github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -125,26 +95,15 @@ github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr6 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= -github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= @@ -154,60 +113,47 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github/v45 v45.2.0 h1:5oRLszbrkvxDDqBCNj2hjDZMKmvexaZ1xw/FCD+K3FI= github.com/google/go-github/v45 v45.2.0/go.mod h1:FObaZJEDSTa/WGCzZ2Z3eoCDXWJKMenWWTrd8jrta28= +github.com/google/go-github/v53 v53.0.0 h1:T1RyHbSnpHYnoF0ZYKiIPSgPtuJ8G6vgc0MKodXsQDQ= +github.com/google/go-github/v53 v53.0.0/go.mod h1:XhFRObz+m/l+UCm9b7KSIC3lT3NWSXGt7mOsAWEloao= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ= github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= -github.com/hashicorp/copywrite v0.16.3 h1:9yOzvuMAVurKEmn2lIWLUYq1Nn7lsYTZMyXbUdEB9wk= -github.com/hashicorp/copywrite v0.16.3/go.mod h1:wl92lMJ9VBqxH9M5KWfseHzXtjj7Q2u5LnKhpS0Rclo= +github.com/hashicorp/copywrite v0.22.0 h1:mqjMrgP3VptS7aLbu2l39rtznoK+BhphHst6i7HiTAo= +github.com/hashicorp/copywrite v0.22.0/go.mod h1:FqvGJt2+yoYDpVYgFSdg3R2iyhkCVaBmPMhfso0MR2k= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v1.4.0 h1:ctuWFGrhFha8BnnzxqeRGidlEcQkDyL5u8J8t5eA11I= -github.com/hashicorp/go-hclog v1.4.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= +github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= @@ -234,19 +180,16 @@ github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoI github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= -github.com/henvic/httpretty v0.0.6 h1:JdzGzKZBajBfnvlMALXXMVQWxWMF/ofTy8C3/OSUTxs= -github.com/henvic/httpretty v0.0.6/go.mod h1:X38wLjWXHkXT7r2+uK8LjCMne9rsuNaBLJ+5cU2/Pmo= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/hjson/hjson-go/v4 v4.0.0 h1:wlm6IYYqHjOdXH1gHev4VoXCaW20HdQAGCxdOEEg2cs= github.com/hjson/hjson-go/v4 v4.0.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= -github.com/jedib0t/go-pretty/v6 v6.4.4 h1:N+gz6UngBPF4M288kiMURPHELDMIhF/Em35aYuKrsSc= -github.com/jedib0t/go-pretty/v6 v6.4.4/go.mod h1:MgmISkTWDSFu0xOqiZ0mKNntMQ2mDgOcwOkwBEkMDJI= +github.com/jedib0t/go-pretty/v6 v6.4.6 h1:v6aG9h6Uby3IusSSEjHaZNXpHFhzqMmjXcPq1Rjl9Jw= +github.com/jedib0t/go-pretty/v6 v6.4.6/go.mod h1:Ndk3ase2CkQbXLLNf5QDHoYb6J9WtVfmHZu9n8rk2xs= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= @@ -255,8 +198,6 @@ github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= @@ -272,12 +213,11 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= @@ -293,10 +233,11 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= -github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mergestat/timediff v0.0.3 h1:ucCNh4/ZrTPjFZ081PccNbhx9spymCJkFxSzgVuPU+Y= github.com/mergestat/timediff v0.0.3/go.mod h1:yvMUaRu2oetc+9IbPLYBJviz6sA7xz8OXMDfhBl7YSI= @@ -328,9 +269,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= -github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= -github.com/muesli/termenv v0.12.0 h1:KuQRUE3PgxRFWhq4gHvZtPSLCGDqM5q/cYr1pZ39ytc= -github.com/muesli/termenv v0.12.0/go.mod h1:WCCv32tusQ/EEZ5S8oUIIrC/nIuBcxCVqlN4Xfkv+7A= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= @@ -342,6 +280,7 @@ github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FI github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -367,10 +306,10 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -383,7 +322,8 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk= +github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= +github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -401,79 +341,52 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/thanhpk/randstr v1.0.4 h1:IN78qu/bR+My+gHCvMEXhR/i5oriVHcTB/BJJIRTsNo= github.com/thanhpk/randstr v1.0.4/go.mod h1:M/H2P1eNLZzlDwAzpkkkUvoyNNMbzRGhESZuEQk3r0U= -github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e h1:BuzhfgfWQbX0dWzYzT1zsORLnHRv3bcRcsaUk0VmXA8= -github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= go.mongodb.org/mongo-driver v1.10.0 h1:UtV6N5k14upNp4LTduX0QCufG124fSu25Wz9tu94GLg= go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -481,57 +394,42 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 h1:OSnWWcOd/CtWQC2cYSBgbTSJv3ciqd8r54ySIW2y3RE= -golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= +golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/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-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -539,41 +437,21 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -585,154 +463,75 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 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.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -743,11 +542,9 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= @@ -756,8 +553,7 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -773,13 +569,5 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/website/Makefile b/website/Makefile deleted file mode 100644 index 0d35466ad..000000000 --- a/website/Makefile +++ /dev/null @@ -1,60 +0,0 @@ -###################################################### -# NOTE: This file is managed by the Digital Team's # -# Terraform configuration @ hashicorp/mktg-terraform # -###################################################### - -.DEFAULT_GOAL := website - -# Set the preview mode for the website shell to "developer" or "io" -PREVIEW_MODE ?= developer -REPO ?= terraform-plugin-testing - -# Enable setting alternate docker tool, e.g. 'make DOCKER_CMD=podman' -DOCKER_CMD ?= docker - -CURRENT_GIT_BRANCH=$$(git rev-parse --abbrev-ref HEAD) -LOCAL_CONTENT_DIR=../docs -PWD=$$(pwd) - -DOCKER_IMAGE="hashicorp/dev-portal" -DOCKER_IMAGE_LOCAL="dev-portal-local" -DOCKER_RUN_FLAGS=-it \ - --publish "3000:3000" \ - --rm \ - --tty \ - --volume "$(PWD)/docs:/app/docs" \ - --volume "$(PWD)/img:/app/public/img" \ - --volume "$(PWD)/data:/app/data" \ - --volume "$(PWD)/redirects.js:/app/redirects.js" \ - --volume "next-dir:/app/website-preview/.next" \ - --volume "$(PWD)/.env:/app/.env" \ - --volume "$(PWD)/.env.development:/app/website-preview/.env.development" \ - --volume "$(PWD)/.env.local:/app/website-preview/.env.local" \ - -e "REPO=$(REPO)" \ - -e "PREVIEW_FROM_REPO=$(REPO)" \ - -e "IS_CONTENT_PREVIEW=true" \ - -e "LOCAL_CONTENT_DIR=$(LOCAL_CONTENT_DIR)" \ - -e "CURRENT_GIT_BRANCH=$(CURRENT_GIT_BRANCH)" \ - -e "PREVIEW_MODE=$(PREVIEW_MODE)" - -# Default: run this if working on the website locally to run in watch mode. -.PHONY: website -website: - @echo "==> Downloading latest Docker image..." - @$(DOCKER_CMD) pull $(DOCKER_IMAGE) - @echo "==> Starting website..." - @$(DOCKER_CMD) run $(DOCKER_RUN_FLAGS) $(DOCKER_IMAGE) - -# Use this if you have run `website/build-local` to use the locally built image. -.PHONY: website/local -website/local: - @echo "==> Starting website from local image..." - @$(DOCKER_CMD) run $(DOCKER_RUN_FLAGS) $(DOCKER_IMAGE_LOCAL) - -# Run this to generate a new local Docker image. -.PHONY: website/build-local -website/build-local: - @echo "==> Building local Docker image" - @$(DOCKER_CMD) build https://github.com/hashicorp/dev-portal.git\#main \ - -t $(DOCKER_IMAGE_LOCAL) - \ No newline at end of file diff --git a/website/README.md b/website/README.md deleted file mode 100644 index 21976cb0a..000000000 --- a/website/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# Terraform Documentation - -This directory contains the portions of [the Terraform website][terraform.io] that pertain to the Terraform Plugin Testing. - -The files in this directory are intended to be used in conjunction with -[the `terraform-website` repository](https://github.com/hashicorp/terraform-website), which brings all of the -different documentation sources together and contains the scripts for testing and building the site as -a whole. - -## Updating Sidebar Navigation - -You must update the sidebar navigation for the `terraform-plugin-testing` documentation any time that you add or delete a documentation page. The website builds the sidebar navigation menu from the [nav-data] JSON file. For more details about how to update this file, refer to https://github.com/hashicorp/terraform-website#editing-navigation-sidebars. - -## Adding Redirects - -You must add a redirect when you move, rename, or delete documentation pages. Refer to https://github.com/hashicorp/terraform-website#redirects for details. - -## Previewing Changes - -You should preview your changes locally to ensure that the content is rendering properly before you create a pull request. The build includes content from this repository and the [`terraform-website`](https://github.com/hashicorp/terraform-website/) repository, allowing you to preview the entire Terraform documentation site. - -To preview your content, complete the following steps: - -**Set Up Local Environment** - -1. [Install Docker](https://docs.docker.com/get-docker/). -1. Restart your terminal or command line session. - -**Launch Site Locally** - -1. Navigate into your local `terraform-plugin-testing` top-level directory and run `make website`. -1. Open `http://localhost:3000` in your web browser. While the preview is running, you can edit pages and Next.js will automatically rebuild them. -1. When you're done with the preview, press `ctrl-C` in your terminal to stop the server. - -### Validating Content - -Content changes are automatically validated against a set of rules as part of the pull request process. If you want to run these checks locally to validate your content before committing your changes, you can run the following command: - -``` -npm run content-check -``` - -If the validation fails, actionable error messages will be displayed to help you address detected issues. - -## Deployment - -The website reads content from release tags to generate documentation for all versions of `terraform-plugin-testing` documentation. Changes merged into `main` will be included in the documentation for the next product release. - -You cannot edit documentation for past versions of `terraform-plugin-testing` on the site. Documentation is an artifact of a product release. We push docs fixes forward for the next release, rather than retroactively fixing older versions. - -[nav-data]: ../website/data/plugin-testing-nav-data.json -[terraform.io]: https://www.terraform.io/ diff --git a/website/data/plugin-testing-nav-data.json b/website/data/plugin-testing-nav-data.json deleted file mode 100644 index 5e6468792..000000000 --- a/website/data/plugin-testing-nav-data.json +++ /dev/null @@ -1,42 +0,0 @@ -[ - { "heading": "Testing" }, - { "title": "Overview", "path": "" }, - { "title": "Migrating from SDK", "path": "migrating" }, - { - "title": "Acceptance Testing", - "routes": [ - { - "title": "Overview", - "path": "acceptance-tests" - }, - { - "title": "Test Cases", - "path": "acceptance-tests/testcase" - }, - { - "title": "Test Steps", - "path": "acceptance-tests/teststep" - }, - { - "title": "Terraform Version Checks", - "path": "acceptance-tests/tfversion-checks" - }, - { - "title": "Plan Checks", - "path": "acceptance-tests/plan-checks" - }, - { - "title": "Sweepers", - "path": "acceptance-tests/sweepers" - } - ] - }, - { - "title": "Testing Patterns", - "path": "testing-patterns" - }, - { - "title": "Unit Testing", - "path": "unit-testing" - } -] diff --git a/website/docs/plugin/testing/acceptance-tests/index.mdx b/website/docs/plugin/testing/acceptance-tests/index.mdx deleted file mode 100644 index c153a3a3f..000000000 --- a/website/docs/plugin/testing/acceptance-tests/index.mdx +++ /dev/null @@ -1,305 +0,0 @@ ---- -page_title: Plugin Development - Acceptance Testing -description: |- - Terraform includes a framework for constructing acceptance tests that - imitate applying one or more configuration files. ---- - -# Acceptance Tests - -In order to deliver on our promise to be safe and predictable, we need to be -able to easily and routinely verify that Terraform Plugins produce the expected -outcome. The most common usage of an acceptance test is in Terraform Providers, -where each Resource is tested with configuration files and the resulting -infrastructure is 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. - -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 -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 -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 -destroy. It is the developer's responsibility to clean up any dangling resources -left over from testing and development. - -## How Acceptance Tests Work - -Provider acceptance tests use a Terraform CLI binary to run real Terraform commands. The goal is to approximate using the provider with Terraform in production as closely as possible. - -Terraform Core and Terraform Plugins act as gRPC client and server, implemented using HashiCorp's [go-plugin](https://github.com/hashicorp/go-plugin) system (refer to the [RPC Plugin Model](https://github.com/hashicorp/terraform/tree/main/docs/plugin-protocol) section of the Terraform documentation). When `go test` is run, the acceptance test framework starts a plugin server in the same process as the Go test framework. This plugin server runs for the duration of the test case, and each Terraform command (`terraform plan`, `terraform apply`, etc) creates a client that reattaches to this server. - -Real-world Terraform usage requires a config file and Terraform working directory on the local filesystem. The module uses the [`internal/plugintest` package](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/internal/plugintest) to manage temporary directories and files during test runs. Provider developers should not use this library directly. - -While the test framework provides a reasonable simulation of real-world usage, there are some differences, the major one being in the lifecycle of the plugin gRPC server. During normal Terraform operation, the plugin server starts and stops once per graph walk, of which there may be several during one Terraform command. The acceptance test framework, however, maintains one plugin gRPC server for the duration of each test case. In theory, it is possible for providers to carry internal state between operations during tests - but providers would have to go out of their way (and the SDK's public API) to do this. - -## Test files - -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 -example file structure: - -``` -terraform-plugin-example/ -├── provider.go -├── provider_test.go -├── example/ -│ ├── resource_example_compute.go -│ ├── resource_example_compute_test.go -``` - -To create an acceptance test in the example `resource_example_compute_test.go` -file, the function name must begin with `TestAccXxx`, and have the following -signature: - -``` -func TestAccXxx(*testing.T) -``` - -## Requirements and Recommendations - -Acceptance tests have the following requirements: - -- **[Go](https://go.dev/)**: The most recent stable version. -- **Terraform CLI**: Version 0.12.26 or later. -- **Provider Access**: Network or system access to the provider and any resources being tested. -- **Provider Credentials**: Authorized credentials to the provider and any resources being tested. -- **TF_ACC Environment Variable**: Set to any value. Prevents developers from incurring unintended charges when running other Go tests. - -We also recommend the following when running acceptance tests: - -- **Separate Account**: Use a separate provider account or namespace for acceptance testing. This prevents Terraform from unexpectedly modifying or destroying infrastructure due to code or testing issues. -- **Previous Terraform CLI Installation**: Install Terraform CLI either into the operating system `PATH` or use the `TF_ACC_TERRAFORM_PATH` environment variable prior to running acceptance tests. Otherwise, the testing framework will download and install the latest Terraform CLI version into a temporary directory for every test invocation. Refer to the [Terraform CLI Installation Behaviors](#terraform-cli-installation-behaviors) section for details. - -Each provider may have additional requirements and setup recommendations. Refer to the provider's codebase for more details. - -### Terraform CLI Installation Behaviors - -The testing framework implements the following Terraform CLI discovery and installation behaviors: - -- If the `TF_ACC_TERRAFORM_PATH` environment variable is set, the framework will use that Terraform CLI binary if it exists and is executable. If the framework cannot find the binary or it is not executable, the framework returns an error unless the `TF_ACC_TERRAFORM_VERSION` environment variable is also set. -- If the `TF_ACC_TERRAFORM_VERSION` environment variable is set, the framework will install and use that Terraform CLI version. -- If both the `TF_ACC_TERRAFORM_PATH` and `TF_ACC_TERRAFORM_VERSION` environment variables are unset, the framework will search for the Terraform CLI binary based on the operating system `PATH`. If the framework cannot find the specified binary, it installs the latest available Terraform CLI binary. - -Refer to the [Environment Variables](#environment-variables) section for more details about behaviors and valid configurations. - -## Running Acceptance Tests - -Ensure that the [acceptance testing requirements](#requirements-and-recommendations) are met and then use the [`go test`](https://pkg.go.dev/cmd/go/internal/test) command to run acceptance tests. You can run the acceptance tests on any environment capable of running `go test`, such as a local workstation [command line](#command-line-workflow), or continuous integration runner, such as [GitHub Actions](#github-actions-workflow). - -~> **Note**: Acceptance tests typically create and destroy actual infrastructure resources, possibly incurring expenses during or after the test duration. - -### Command Line Workflow - -Run acceptance testing with the command line of any workstation. Use these instructions as the basis for other environments such as continuous integration runners. - -The following example will execute all available acceptance tests in a provider codebase: - -```shell -TF_ACC=1 go test -v ./... -``` - -Some provider codebases also implement a [Makefile](https://www.gnu.org/software/make/manual/make.html#Makefiles) with a `testacc` target, which will set `TF_ACC` and other testing flags automatically. - -The following is an example Makefile configuration: - -```make -testacc: - TF_ACC=1 go test -v ./... -``` - -The Makefile configuration lets developers to use the following command to run acceptance tests: - -```shell -make testacc -``` - -### GitHub Actions Workflow - -If using [GitHub](https://github.com/), run acceptance testing via [GitHub Actions](https://github.com/features/actions). Other continuous integration runners, while not exhaustively documented, are also supported. - -Ensure the [GitHub Organization settings for GitHub Actions](https://docs.github.com/en/organizations/managing-organization-settings/disabling-or-limiting-github-actions-for-your-organization) and [GitHub Repository settings for GitHub Actions](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository) allows running workflows and allows the `actions/checkout`, `actions/setup-go`, and `hashicorp/setup-terraform` actions. - -Create a [GitHub Actions workflow](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions) file, such as `.github/workflows/test.yaml`, that does the following: - -- Runs when pull requests are submitted or on [other events](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows) as appropriate. -- Uses [`actions/checkout`](https://github.com/actions/checkout) to checkout the provider codebase. -- Uses [`actions/setup-go`](https://github.com/actions/setup-go) to install Go. -- Uses [`hashicorp/setup-terraform`](https://github.com/hashicorp/setup-terraform) to install Terraform CLI. -- Runs the `go test` command with the appropriate environment variables and flags. - -Use the [`matrix`](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategymatrix) strategy for more advanced configuration, such as running acceptance testing against multiple Terraform CLI versions. - -The following example workflow runs acceptance testing for the provider using the latest patch versions of Go 1.19 and Terraform CLI 1.3: - -```yaml -name: Terraform Provider Tests - -on: - pull_request: - paths: - - '.github/workflows/test.yaml' - - '**.go' - -permissions: - # Permission for checking out code - contents: read - -jobs: - acceptance: - name: Acceptance Tests - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-go@v3 - with: - go-version: '1.19' - - uses: hashicorp/setup-terraform@v2 - with: - terraform_version: '1.3.*' - terraform_wrapper: false - - run: go test -v -cover ./... - env: - TF_ACC: '1' - unit: - name: Unit Tests - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-go@v3 - with: - go-version: '1.19' - - run: go test -v -cover ./... -``` - - -The following example workflow runs acceptance testing for the provider using the latest patch versions of Go 1.19 and Terraform CLI 0.12 through 1.3: - -```yaml -name: Terraform Provider Tests - -on: - pull_request: - paths: - - '.github/workflows/test.yaml' - - '**.go' - -permissions: - # Permission for checking out code - contents: read - -jobs: - acceptance: - name: Acceptance Tests (Terraform ${{ matrix.terraform-version }}) - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - terraform-version: - - '0.12.*' - - '0.13.*' - - '0.14.*' - - '0.15.*' - - '1.0.*' - - '1.1.*' - - '1.2.*' - - '1.3.*' - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-go@v3 - with: - go-version: '1.19' - - uses: hashicorp/setup-terraform@v2 - with: - terraform_version: ${{ matrix.terraform-version }} - terraform_wrapper: false - - run: go test -v -cover ./... - env: - TF_ACC: '1' - unit: - name: Unit Tests - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-go@v3 - with: - go-version: '1.19' - - run: go test -v -cover ./... -``` - -## Environment Variables - -A number of environment variables are available to control aspects of acceptance test execution. - -| Environment Variable Name | Default | Description | -|------------------------------|-------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `TF_ACC` | N/A | Set to any value to enable acceptance testing via the [`helper/resource.ParallelTest()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/helper/resource#ParallelTest) and [`helper/resource.Test()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/helper/resource#Test) functions. | -| `TF_ACC_PROVIDER_HOST`: | `registry.terraform.io` | Set the hostname of the provider under test, such as `example.com` in the `example.com/myorg/myprovider` provider source address. This is only required if any [`TestStep.Config`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/helper/resource#TestStep.Config) specifies a provider source address, such as in the [`terraform` configuration block `required_providers` attribute](/terraform/language/settings#specifying-provider-requirements). | -| `TF_ACC_PROVIDER_NAMESPACE` | `hashicorp` | Set the namespace of the provider under test, such as `myorg` in the `registry.terraform.io/myorg/myprovider` provider source address. This is only required if any [`TestStep.Config`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/helper/resource#TestStep.Config) specifies a provider source address, such as in the [`terraform` configuration block `required_providers` attribute](/terraform/language/settings#specifying-provider-requirements). | -| `TF_ACC_STATE_LINEAGE` | N/A | Set to `1` to enable state lineage debug logs, which are normally suppressed during acceptance testing. | -| `TF_ACC_TEMP_DIR` | Operating system specific via [`os.TempDir()`](https://pkg.go.dev/os#TempDir) | Set a temporary directory used for testing files and installing Terraform CLI, if installation is required. | -| `TF_ACC_TERRAFORM_PATH` | N/A | Set the path to a Terraform CLI binary on the local filesystem to be used during testing. It must be executable. If not found and `TF_ACC_TERRAFORM_VERSION` is not set, an error is returned. | -| `TF_ACC_TERRAFORM_VERSION` | N/A | Set the exact version of Terraform CLI to automatically install into `TF_ACC_TEMP_DIR`. For example, `1.1.6` or `v1.0.11`. | -| `TF_ACC_PERSIST_WORKING_DIR` | N/A | Set to any value to enable persisting the working directory and the files generated during execution of each `TestStep`. The location of each directory is written to the test output for each `TestStep` when the `go test -v` (verbose) flag is provided. | - -### Logging Environment Variables - -A number of environment variables available to control logging aspects during acceptance test execution. Some of these modify or replace the production behaviors defined in [managing provider log output](/terraform/plugin/log/managing) and [debugging Terraform](/terraform/internals/debugging). - -#### Logging Levels - -| Environment Variable Name | Default | Description | -|---------------------------|--------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `TF_ACC_LOG` | N/A | Set the `TF_LOG` environment variable used by Terraform CLI while testing. If set, overrides `TF_LOG_CORE`. Use `TF_LOG_CORE` and `TF_LOG_PROVIDER` to configure separate levels for Terraform CLI logging. | -| `TF_LOG` | N/A | Set the log level for the Go standard library `log` package. If set to any level, sets the `TRACE` log level for any SDK and provider logs written by [`terraform-plugin-log`](/terraform/plugin/log/writing). Use the `TF_LOG_SDK*` and `TF_LOG_PROVIDER_*` environment variables described in [managing log output](/terraform/plugin/log/managing) to decrease or disable SDK and provider logs written by [`terraform-plugin-log`](/terraform/plugin/log/writing). Use `TF_ACC_LOG`, `TF_LOG_CORE`, or `TF_LOG_PROVIDER` environment variables to set the logging levels used by Terraform CLI while testing. | -| `TF_LOG_CORE` | `TF_ACC_LOG` | Set the `TF_LOG_CORE` environment variable used by Terraform CLI logging of graph operations and other core functionality while testing. If `TF_ACC_LOG` is set, this setting has no effect. Use `TF_LOG_PROVIDER` to configure a separate level for Terraform CLI logging of external providers while testing (e.g. defined by the `TestCase` or `TestStep` type `ExternalProviders` field). | -| `TF_LOG_PROVIDER` | `TF_ACC_LOG` | Set the `TF_LOG_PROVIDER` environment variable used by Terraform CLI logging of external providers while testing (e.g. defined by the `TestCase` or `TestStep` type `ExternalProviders` field). If set, overrides `TF_ACC_LOG`. Use `TF_LOG_CORE` to configure a separate level for Terraform CLI logging of graph operations and other core functionality while testing. | - -#### Logging Output - -By default, there is no logging output when running the `go test` command. Use one of the below environment variables to output logs to the local filesystem or use the `go test` command `-v` (verbose) flag to view logging without writing file(s). - -| Environment Variable Name | Default | Description | -|---------------------------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `TF_ACC_LOG_PATH` | N/A | Set a file path for all logs during testing. Use `TF_LOG_PATH_MASK` to configure individual log files per test. | -| `TF_LOG_PATH_MASK` | N/A | Set a file path containing the string `%s`, which is replaced with the test name, to write a separate log file per test. Use `TF_ACC_LOG_PATH` to configure a single log file for all tests. | - -The logs associated with each test can output across incorrect files as each new test starts if the provider is using the Go standard library [`log` package](https://pkg.go.dev/log) for logging, acceptance testing that uses [`helper/resource.ParallelTest()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/helper/resource#ParallelTest), and `TF_LOG_PATH_MASK`. To resolve this issue, choose one of the following approaches: - -* Use [`terraform-plugin-log`](/terraform/plugin/log/writing) based logging. Each logger will be correctly associated with each test name output. -* Wrap testing execution so that each test is individually executed with `go test`. Since each `go test` process will have its own `log` package output handling, logging will be correctly associated with each test name output. -* Replace [`helper/resource.ParallelTest()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/helper/resource#ParallelTest) with [`helper/resource.Test()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/helper/resource#Test) and ensure [`(*testing.T).Parallel()`](https://pkg.go.dev/testing#T.Parallel) is not called in tests. This serializes all testing so each test will be associated with each test name output. - -## Troubleshooting - -This section lists common errors encountered during testing. - -### Unrecognized remote plugin message - -``` -terraform failed: exit status 1 - - stderr: - - Error: Failed to instantiate provider "random" to obtain schema: Unrecognized remote plugin message: --- FAIL: TestAccResourceID (4.28s) - - This usually means that the plugin is either invalid or simply - needs to be recompiled to support the latest protocol. -``` - -This error indicates that the provider server could not connect to Terraform Core. Verify that the output of `terraform version` is v0.12.26 or above. - -## Next Steps - -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 -build and verify real infrastructure. [Proceed to Test -Cases](/terraform/plugin/testing/acceptance-tests/testcase) diff --git a/website/docs/plugin/testing/acceptance-tests/plan-checks.mdx b/website/docs/plugin/testing/acceptance-tests/plan-checks.mdx deleted file mode 100644 index fa91a402f..000000000 --- a/website/docs/plugin/testing/acceptance-tests/plan-checks.mdx +++ /dev/null @@ -1,246 +0,0 @@ ---- -page_title: 'Plugin Development - Acceptance Testing: Plan Checks' -description: >- - Plan Checks are test assertions that can inspect a plan at different phases in a TestStep. The testing module - provides built-in Plan Checks for common use-cases, but custom Plan Checks can also be implemented. ---- - -# Plan Checks - -During the **Lifecycle (config)** and **Refresh** [modes](/terraform/plugin/testing/acceptance-tests/teststep#test-modes) of a `TestStep`, the testing framework will run `terraform plan` before and after certain operations. For example, the **Lifecycle (config)** mode will run a plan before the `terraform apply` phase, as well as a plan before and after the `terraform refresh` phase. - -These `terraform plan` operations results in a [plan file](/terraform/cli/commands/plan#out-filename) and can be represented by this [JSON format](/terraform/internals/json-format#plan-representation). - -A **plan check** is a test assertion that inspects the plan file at a specific phase during the current testing mode. Multiple plan checks can be run at each defined phase, all assertion errors returned are aggregated, reported as a test failure, and all test cleanup logic is executed. - -- Available plan phases for **Lifecycle (config)** mode are defined in the [`TestStep.ConfigPlanChecks`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/helper/resource#TestStep) struct -- Available plan phases for **Refresh** mode are defined in the [`TestStep.RefreshPlanChecks`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/helper/resource#TestStep) struct -- **Import** mode currently does not run any plan operations, and therefore does not support plan checks. - -## Built-in Plan Checks - -The `terraform-plugin-testing` module provides a package [`plancheck`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck) with built-in plan checks for common use-cases: - -| Check | Description | -|---|---| -| [`plancheck.ExpectEmptyPlan()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck#ExpectEmptyPlan) | Asserts the entire plan has no operations for apply. | -| [`plancheck.ExpectNonEmptyPlan()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck#ExpectNonEmptyPlan) | Asserts the entire plan contains at least one operation for apply. | -| [`plancheck.ExpectResourceAction(address, operation)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck#ExpectResourceAction) | Asserts the given resource has the specified operation for apply. | - -### Examples using `plancheck.ExpectResourceAction` - -One of the built-in plan checks, [`plancheck.ExpectResourceAction`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck#ExpectResourceAction), is useful for determining the exact action type a resource will under-go during, say, the `terraform apply` phase. - -Given the following example with the [random provider](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string), we have written a test that asserts that `random_string.one` will be destroyed and re-created when the `length` attribute is changed: - -```go -package example_test - -import ( - "testing" - - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/plancheck" -) - -func Test_Random_ForcesRecreate(t *testing.T) { - t.Parallel() - - resource.Test(t, resource.TestCase{ - ExternalProviders: map[string]resource.ExternalProvider{ - "random": { - Source: "registry.terraform.io/hashicorp/random", - }, - }, - Steps: []resource.TestStep{ - { - Config: `resource "random_string" "one" { - length = 16 - }`, - }, - { - Config: `resource "random_string" "one" { - length = 15 - }`, - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("random_string.one", plancheck.ResourceActionDestroyBeforeCreate), - }, - }, - }, - }, - }) -} -``` - -Another example with the [time provider](https://registry.terraform.io/providers/hashicorp/time/latest/docs/resources/offset) asserts that `time_offset.one` will be updated in-place when the `offset_days` attribute is changed: - -```go -package example_test - -import ( - "testing" - - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/plancheck" -) - -func Test_Time_UpdateInPlace(t *testing.T) { - t.Parallel() - - resource.Test(t, resource.TestCase{ - ExternalProviders: map[string]resource.ExternalProvider{ - "time": { - Source: "registry.terraform.io/hashicorp/time", - }, - }, - Steps: []resource.TestStep{ - { - Config: `resource "time_offset" "one" { - offset_days = 1 - }`, - }, - { - Config: `resource "time_offset" "one" { - offset_days = 2 - }`, - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("time_offset.one", plancheck.ResourceActionUpdate), - }, - }, - }, - }, - }) -} -``` - -Multiple plan checks can be combined if you want to assert multiple resource actions: -```go -package example_test - -import ( - "testing" - - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/plancheck" -) - -func Test_Time_UpdateInPlace_and_NoOp(t *testing.T) { - t.Parallel() - - resource.Test(t, resource.TestCase{ - ExternalProviders: map[string]resource.ExternalProvider{ - "time": { - Source: "registry.terraform.io/hashicorp/time", - }, - }, - Steps: []resource.TestStep{ - { - Config: `resource "time_offset" "one" { - offset_days = 1 - } - resource "time_offset" "two" { - offset_days = 1 - }`, - }, - { - Config: `resource "time_offset" "one" { - offset_days = 2 - } - resource "time_offset" "two" { - offset_days = 1 - }`, - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("time_offset.one", plancheck.ResourceActionUpdate), - plancheck.ExpectResourceAction("time_offset.two", plancheck.ResourceActionNoop), - }, - }, - }, - }, - }) -} -``` - -## Custom Plan Checks - -The package [`plancheck`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck) also provides the [`PlanCheck`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck#PlanCheck) interface, which can be implemented for a custom plan check. - -The [`plancheck.CheckPlanRequest`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck#CheckPlanRequest) contains the current plan file, parsed by the [terraform-json package](https://pkg.go.dev/github.com/hashicorp/terraform-json#Plan). - -Here is an example implementation of a plan check that asserts that every resource change is a no-op, aka, an empty plan: -```go -package example_test - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-testing/plancheck" -) - -var _ plancheck.PlanCheck = expectEmptyPlan{} - -type expectEmptyPlan struct{} - -func (e expectEmptyPlan) CheckPlan(ctx context.Context, req plancheck.CheckPlanRequest, resp *plancheck.CheckPlanResponse) { - var result error - - for _, rc := range req.Plan.ResourceChanges { - if !rc.Change.Actions.NoOp() { - result = errors.Join(result, fmt.Errorf("expected empty plan, but %s has planned action(s): %v", rc.Address, rc.Change.Actions)) - } - } - - resp.Error = result -} - -func ExpectEmptyPlan() plancheck.PlanCheck { - return expectEmptyPlan{} -} -``` - -And example usage: -```go -package example_test - -import ( - "context" - "errors" - "fmt" - "testing" - - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/plancheck" -) - -func Test_CustomPlanCheck_ExpectEmptyPlan(t *testing.T) { - t.Parallel() - - resource.Test(t, resource.TestCase{ - ExternalProviders: map[string]resource.ExternalProvider{ - "random": { - Source: "registry.terraform.io/hashicorp/random", - }, - }, - Steps: []resource.TestStep{ - { - Config: `resource "random_string" "one" { - length = 16 - }`, - }, - { - Config: `resource "random_string" "one" { - length = 16 - }`, - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - ExpectEmptyPlan(), - }, - }, - }, - }, - }) -} -``` diff --git a/website/docs/plugin/testing/acceptance-tests/sweepers.mdx b/website/docs/plugin/testing/acceptance-tests/sweepers.mdx deleted file mode 100644 index 2fff9834f..000000000 --- a/website/docs/plugin/testing/acceptance-tests/sweepers.mdx +++ /dev/null @@ -1,121 +0,0 @@ ---- -page_title: 'Plugin Development - Acceptance Testing: Sweepers' -description: >- - Acceptance tests provision and verify real infrastructure with Terraform's - testing framework. Sweepers clean up leftover infrastructure. ---- - -# Sweepers - -Acceptance tests in Terraform provision and verify real infrastructure using [Terraform's testing framework](/terraform/plugin/testing/acceptance-tests). Ideally all infrastructure created is then destroyed within the lifecycle of a test, however the reality is that there are several situations that can arise where resources created during a test are “leaked”. Leaked test resources are resources created by Terraform during a test, but Terraform either failed to destroy them as part of the test, or the test falsely reported all resources were destroyed after completing the test. Common causes are intermittent errors or failures in vendor APIs, or developer error in the resource code or test. - -To address the possibility of leaked resources, Terraform provides a mechanism called sweepers to cleanup leftover infrastructure. We will add a file to our folder structure that will invoke the sweeper helper. - -``` -terraform-plugin-example/ -├── provider.go -├── provider_test.go -├── example/ -│ ├── example_sweeper_test.go -│ ├── resource_example_compute.go -│ ├── resource_example_compute_test.go -``` - -**`example_sweeper_test.go`** - -```go -package example - -import ( - "testing" - - "github.com/hashicorp/terraform-plugin-testing/helper/resource" -) - -func TestMain(m *testing.M) { - resource.TestMain(m) -} - -// sharedClientForRegion returns a common provider client configured for the specified region -func sharedClientForRegion(region string) (any, error) { - ... - return client, nil -} -``` - -`resource.TestMain` is responsible for parsing the special test flags and invoking the sweepers. Sweepers should be added within the acceptance test file of a resource. - -**`resource_example_compute_test.go`** - -```go -package example - -import ( - "log" - "strings" - "testing" - - "github.com/hashicorp/terraform-plugin-testing/helper/resource" -) - -func init() { - resource.AddTestSweepers("example_compute", &resource.Sweeper{ - Name: "example_compute", - F: func (region string) error { - client, err := sharedClientForRegion(region) - if err != nil { - return fmt.Errorf("Error getting client: %s", err) - } - conn := client.(*ExampleClient) - - instances, err := conn.DescribeComputeInstances() - if err != nil { - return fmt.Errorf("Error getting instances: %s", err) - } - for _, instance := range instances { - if strings.HasPrefix(instance.Name, "test-acc") { - err := conn.DestroyInstance(instance.ID) - - if err != nil { - log.Printf("Error destroying %s during sweep: %s", instance.Name, err) - } - } - } - return nil - }, - }) -} -``` - -This example demonstrates adding a sweeper, it is important to note that the string passed to `resource.AddTestSweepers` is added to a map, this name must therefore be unique. Also note there needs to be a way of identifying resources created by Terraform during acceptance tests, a common practice is to prefix all resource names created during acceptance tests with `"test-acc"` or something similar. - -For more complex leaks, sweepers can also specify a list of sweepers that need to be run prior to the one being defined. - -**`resource_example_compute_disk_test.go`** - -```go -package example - -import ( - "testing" - - "github.com/hashicorp/terraform-plugin-testing/helper/resource" -) - -func init() { - resource.AddTestSweepers("example_compute_disk", &resource.Sweeper{ - Name: "example_compute_disk", - Dependencies: []string{"example_compute"} - ... - }) -} -``` - -The sweepers can be invoked with the common make target `sweep`: - -``` -$ make sweep -WARNING: This will destroy infrastructure. Use only in development accounts. -go test ... -... -``` diff --git a/website/docs/plugin/testing/acceptance-tests/testcase.mdx b/website/docs/plugin/testing/acceptance-tests/testcase.mdx deleted file mode 100644 index 98aa9586c..000000000 --- a/website/docs/plugin/testing/acceptance-tests/testcase.mdx +++ /dev/null @@ -1,356 +0,0 @@ ---- -page_title: 'Plugin Development - Acceptance Testing: TestCase' -description: |- - Acceptance tests are expressed in terms of Test Cases. Each Test Case - creates a set of resources then verifies the new infrastructure. ---- - -# Acceptance Tests: TestCases - -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` -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 -[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 -Resource under test is `Widget`. The parts of this test are explained below the -example. - -```go -package example - -// example.Widget represents a concrete Go type that represents an API resource -func TestAccExampleWidget_basic(t *testing.T) { - var widgetBefore, widgetAfter example.Widget - rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) - - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckExampleResourceDestroy, - Steps: []resource.TestStep{ - { - Config: testAccExampleResource(rName), - Check: resource.ComposeTestCheckFunc( - testAccCheckExampleResourceExists("example_widget.foo", &widgetBefore), - ), - }, - { - Config: testAccExampleResource_removedPolicy(rName), - Check: resource.ComposeTestCheckFunc( - testAccCheckExampleResourceExists("example_widget.foo", &widgetAfter), - ), - }, - }, - }) -} -``` - -## Creating Acceptance Tests Functions - -Terraform acceptance tests are declared with the naming pattern `TestAccXxx` -and with the standard Go test function signature of `func TestAccXxx(*testing.T)`. -Using the above test as an example: - -```go -// File: example/widget_test.go -package example - -func TestAccExampleWidget_basic(t *testing.T) { - // ... -} -``` - -Inside this function we invoke `resource.Test()` with the `*testing.T` input and -a new testcase object: - -```go -// File: example/widget_test.go -package example - -func TestAccExampleWidget_basic(t *testing.T) { - resource.Test(t, resource.TestCase{ - // ... - }) -} -``` - -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 -fail. A failed test will not halt or otherwise interrupt any other tests -currently running. - -## TestCase Reference API - -`TestCase` offers several fields for developers to add to customize and validate -each test, defined below. The source for `TestCase` can be viewed [here on -godoc.org](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/helper/resource#TestCase) - -### IsUnitTest - -**Type:** [bool](https://pkg.go.dev/builtin#bool) - -**Default:** `false` - -**Required:** no - -**IsUnitTest** allows a test to run regardless of the TF_ACC environment -variable. This should be used with care - only for fast tests on local resources -(e.g. remote state with a local backend) but can be used to increase confidence -in correct operation of Terraform without waiting for a full acceptance test -run. - -### PreCheck - -**Type:** `function` - -**Default:** `nil` - -**Required:** no - -**PreCheck** if non-nil, will be called before any test steps are executed. It -is commonly used to verify that required values exist for testing, such as -environment variables containing test keys that are used to configure the -Provider or Resource under test. - -**Example usage:** - -```go -// File: example/widget_test.go -package example - -func TestAccExampleWidget_basic(t *testing.T) { - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - // ... - }) -} - - -// testAccPreCheck validates the necessary test API keys exist -// in the testing environment -func testAccPreCheck(t *testing.T) { - if v := os.Getenv("EXAMPLE_KEY"); v == "" { - t.Fatal("EXAMPLE_KEY must be set for acceptance tests") - } - if v := os.Getenv("EXAMPLE_SECRET"); v == "" { - t.Fatal("EXAMPLE_SECRET must be set for acceptance tests") - } -} -``` - -### TerraformVersionChecks - -**Type:** [`[]tfversion.TerraformVersionCheck`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion#TerraformVersionCheck) - -**Default:** `nil` - -**Required:** no - -**TerraformVersionChecks** if non-nil, will be called after any defined PreChecks -but before any test steps are executed. The [Terraform Version Checks](/terraform/plugin/testing/acceptance-tests/tfversion-checks) -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. - -**Example usage:** - -```go -// File: example/widget_test.go -package example - -func TestAccExampleWidget_basic(t *testing.T) { - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_1_0), // built-in check from tfversion package - }, - // ... - }) -} - -``` - -### Providers - -**Type:** [`map[string]*schema.Provider`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema#Provider) - -**Required:** Yes - -**Providers** is a map of `*schema.Provider` values with `string` -keys, representing the Providers that will be under test. Only the Providers -included in this map will be loaded during the test, so any Provider included in -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. - -**Example usage:** (note the different files `widget_test.go` and `provider_test.go`) - -```go -// File: example/widget_test.go -package example - -func TestAccExampleWidget_basic(t *testing.T) { - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_1_0), - }, - Providers: testAccProviders, - // ... - }) -} - -// File: example/provider_test.go -package example - -var testAccProviders map[string]*schema.Provider -var testAccProvider *schema.Provider - -func init() { - testAccProvider = Provider() - testAccProviders = map[string]*schema.Provider{ - "example": testAccProvider, - } -} -``` - -### CheckDestroy - -**Type:** [TestCheckFunc](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/helper/resource#TestCheckFunc) - -**Default:** `nil` - -**Required:** no - -**CheckDestroy** is called after all test steps have been run and Terraform -has run `destroy` on the remaining state. This allows developers to ensure any -resource created is truly destroyed. This method receives the last known -Terraform state as input, and commonly uses infrastructure SDKs to query APIs -directly to verify the expected objects are no longer found, and should return -an error if any resources remain. - -**Example usage:** - -```go -// File: example/widget_test.go -package example - -func TestAccExampleWidget_basic(t *testing.T) { - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_1_0), - }, - Providers: testAccProviders, - CheckDestroy: testAccCheckExampleResourceDestroy, - // ... - }) -} - -// testAccCheckExampleResourceDestroy verifies the Widget -// has been destroyed -func testAccCheckExampleResourceDestroy(s *terraform.State) error { - // retrieve the connection established in Provider configuration - conn := testAccProvider.Meta().(*ExampleClient) - - // loop through the resources in state, verifying each widget - // is destroyed - for _, rs := range s.RootModule().Resources { - if rs.Type != "example_widget" { - continue - } - - // Retrieve our widget by referencing it's state ID for API lookup - request := &example.DescribeWidgets{ - IDs: []string{rs.Primary.ID}, - } - - response, err := conn.DescribeWidgets(request) - if err == nil { - if len(response.Widgets) > 0 && *response.Widgets[0].ID == rs.Primary.ID { - return fmt.Errorf("Widget (%s) still exists.", rs.Primary.ID) - } - - return nil - } - - // If the error is equivalent to 404 not found, the widget is destroyed. - // Otherwise return the error - if !strings.Contains(err.Error(), "Widget not found") { - return err - } - } - - return nil -} -``` - -### Steps - -**Type:** [`[]TestStep`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/helper/resource#TestStep) - -**Required:** yes - -**TestStep** is a single apply sequence of a test, done within the context of a -state. Multiple `TestStep`s can be sequenced in a Test to allow testing -potentially complex update logic and usage. Basic tests typically contain one to -two steps, to verify the resource can be created and subsequently updated, -depending on the properties of the resource. In general, simply create/destroy -tests will only need one step. - -`TestStep`s are covered in detail in [the next section, `TestSteps`](/terraform/plugin/testing/acceptance-tests/teststep). - -**Example usage:** - -```go -// File: example/widget_test.go -package example - -func TestAccExampleWidget_basic(t *testing.T) { - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_1_0), - }, - Providers: testAccProviders, - CheckDestroy: testAccCheckExampleResourceDestroy, - Steps: []resource.TestStep{ - { - Config: testAccExampleResource(rName), - Check: resource.ComposeTestCheckFunc( - testAccCheckExampleResourceExists("example_widget.foo", &widgetBefore), - ), - }, - { - Config: testAccExampleResource_removedPolicy(rName), - Check: resource.ComposeTestCheckFunc( - testAccCheckExampleResourceExists("example_widget.foo", &widgetAfter), - ), - }, - }, - }) -} -``` - -## Next Steps - -`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 -to create these scenarios by iterating on Terraform configurations. - -[1]: https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/helper/resource#TestCase - -[2]: /terraform/plugin/testing/acceptance-tests/teststep - -[3]: https://pkg.go.dev/testing#T diff --git a/website/docs/plugin/testing/acceptance-tests/teststep.mdx b/website/docs/plugin/testing/acceptance-tests/teststep.mdx deleted file mode 100644 index 0c66a9ff7..000000000 --- a/website/docs/plugin/testing/acceptance-tests/teststep.mdx +++ /dev/null @@ -1,302 +0,0 @@ ---- -page_title: 'Plugin Development - Acceptance Testing: TestStep' -description: |- - TestSteps represent the application of an actual Terraform configuration - file to a given state. ---- - -# Acceptance Tests: TestSteps - -`TestStep`s represent the application of an actual Terraform configuration file -to a given state. Each step requires a configuration as input and provides -developers several means of validating the behavior of the specific resource -under test. - -## Test Modes - -Terraform's test framework facilitates three distinct modes of acceptance tests, -_Lifecycle (config)_, _Import_ and _Refresh_. - -_Lifecycle (config)_ mode is the most common mode, and is used for testing plugins by -providing one or more configuration files with the same logic as would be used -when running `terraform apply`. - -_Import_ mode is used for testing resource functionality to import existing -infrastructure into a Terraform statefile, using the same logic as would be used -when running `terraform import`. - -_Refresh_ mode is used for testing resource functionality to refresh existing -infrastructure, using the same logic as would be used when running -`terraform refresh`. - -An acceptance test's mode is implicitly determined by the fields provided in the -`TestStep` definition. The applicable fields are defined in the [TestStep -Reference API](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/helper/resource#TestStep). - -## Steps - -`Steps` is a field within -[TestCase](/terraform/plugin/testing/acceptance-tests/testcase), the struct used -to construct acceptance tests. Each step represents a full `terraform apply` of -a given configuration language, followed by zero or more checks (defined later) -to verify the application. Each `Step` is applied in order, and require its own -configuration and optional check functions. - -Below is a code example of a lifecycle test that provides two `TestStep` structs: - -```go -package example - -// example.Widget represents a concrete Go type that represents an API resource -func TestAccExampleWidget_basic(t *testing.T) { - var widgetBefore, widgetAfter example.Widget - rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) - - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckExampleResourceDestroy, - Steps: []resource.TestStep{ - { - Config: testAccExampleResource(rName), - Check: resource.ComposeTestCheckFunc( - testAccCheckExampleResourceExists("example_widget.foo", &widgetBefore), - ), - }, - { - Config: testAccExampleResource_removedPolicy(rName), - Check: resource.ComposeTestCheckFunc( - testAccCheckExampleResourceExists("example_widget.foo", &widgetAfter), - ), - }, - }, - }) -} -``` - -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 -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 - -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` -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 lets you compose multiple TestCheckFunc functions into a -single check. As a user testing their provider, this lets you decompose your -checks into smaller pieces more easily, with individual methods for checking -specific attributes. Each check is ran in the order provided, and on failure the -entire `TestCase` is stopped, and Terraform attempts to destroy any resources -created. - -Example: - -```go -Steps: []resource.TestStep{ - { - Config: testAccExampleResource(rName), - Check: resource.ComposeTestCheckFunc( - // if testAccCheckExampleResourceExists fails to find the resource, - // the parent TestStep and TestCase fail - testAccCheckExampleResourceExists("example_widget.foo", &widgetBefore), - resource.TestCheckResourceAttr("example_widget.foo", "size", "expected size"), - ), - }, -}, -``` - -### ComposeAggregateTestCheckFunc - -ComposeAggregateTestCheckFunc lets you compose multiple TestCheckFunc functions -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 -attempts to destroy any resources created. - -Example: - -```go -Steps: []resource.TestStep{ - { - Config: testAccExampleResource(rName), - Check: resource.ComposeAggregateTestCheckFunc( - // if testAccCheckExampleResourceExists fails to find the resource, - // the following TestCheckResourceAttr is still run, with any errors aggregated - testAccCheckExampleResourceExists("example_widget.foo", &widgetBefore), - resource.TestCheckResourceAttr("example_widget.foo", "active", "true"), - ), - }, -}, -``` - -## Builtin 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 -the resulting state. Developers are encouraged to use as many as reasonable to -verify the behavior of the plugin/resource, and should combine them with the -above mentioned `ComposeTestCheckFunc` or `ComposeAggregateTestCheckFunc` -functions. - -Most builtin functions accept `name`, `key`, and/or `value` fields, derived from -the typical Terraform configuration stanzas: - -```hcl -resource "example_widget" "foo" { - active = true -} -``` - -Here the `name` represents the resource name in state (`example_widget.foo`), -the `key` represents the attribute to check (`active`), and `value` represents -the desired value to check against (`true`). In this case, an equality check -would be: - -```go -resource.TestCheckResourceAttr("example_widget.foo", "active", "true"), -``` - -The full list of functions can be seen in the [`helper/resource` package](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/helper/resource). Names for these begin with `TestCheck...` and `TestMatch...`. The most common checks for non-`TypeSet` attributes are below. - -| Function | Purpose | -| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | -| [`TestCheckResourceAttr(name string, key string, value string)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/helper/resource#TestCheckResourceAttr) | Value equality checks | -| [`TestMatchResourceAttr(name string, key string, regex *regexp.Regexp)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/helper/resource#TestMatchResourceAttr) | | -| Value regular expression checks | | -| [`TestCheckResourceAttrPair(nameFirst string, keyFirst string, nameSecond string, keySecond string)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/helper/resource#TestCheckResourceAttrPair) | Value equality across two attributes (usually in different resources) | -| [`TestCheckResourceAttrSet(name string, key string)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/helper/resource#TestCheckResourceAttrSet) | Passes if any value was set | -| [`TestCheckNoResourceAttr(name string, key string)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/helper/resource#TestCheckNoResourceAttr) | Passes if no value was set | - -For `TypeSet` attributes, there are some additional functions that accept a `*` placeholder in attribute keys for indexing into the set. - -| Function | Purpose | -| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | -| [`TestCheckTypeSetElemAttr(name string, key string, value string)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/helper/resource#TestCheckTypeSetElemAttr) | Value is contained in set | -| [`TestCheckTypeSetElemAttrPair(nameFirst string, keyFirst string, nameSecond string, keySecond string)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/helper/resource#TestCheckTypeSetElemAttrPair) | Value is contained in set from another attribute (usually in different resources) | -| [`TestCheckTypeSetElemNestedAttrs(name string, key string, values map[string]string)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/helper/resource#TestCheckTypeSetElemNestedAttrs) | Map of values is contained in set (usually checking multiple attributes of a block) | - -All of these functions also accept the below syntax in attribute keys to enable additional behaviors. - -| Syntax | Purpose | Example | -| ----------- | --------------------------------- | ------------------------------------------------------------------------------- | -| `.{NUMBER}` | List index | `TestCheckResourceAttr("example_widget.foo", "some_block.0", "first value")` | -| `.{KEY}` | Map key | `TestCheckResourceAttr("example_widget.foo", "some_map.some_key", "map value")` | -| `.#` | 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 - -The `Check` field of `TestStep` accepts any function of type -[TestCheckFunc](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/helper/resource#TestCheckFunc). -Developers are free to write their own `check` functions to create customized -validation functions for their plugin. Any function that matches the -`TestCheckFunc` function signature of `func(*terraform.State) error` can be used -individually, or with other `TestCheckFunc` functions with one of the above -Aggregate functions. - -It's common to write custom `TestCheckFunc` functions to validate resources were -created correctly by using SDKs directly to verify identity and properties of -resources. These functions can retrieve information by SDKs and provide the -results to other `TestCheckFunc` methods. The below example uses -`ComposeTestCheckFunc` to group a set of `TestCheckFunc` functions together. The -first function `testAccCheckExampleWidgetExists` uses the `Example` service SDK -directly, and queries it for the ID of the widget we have in state. Once found, -the result is stored into the `widget` struct declared at the beginning of the -test function. The next check function `testAccCheckExampleWidgetAttributes` -receives the updated `widget` and checks its attributes. The final check -`TestCheckResourceAttr` verifies that the same value is stored in state. - -```go -func TestAccExampleWidget_basic(t *testing.T) { - var widget example.WidgetDescription - - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckExampleWidgetDestroy, - Steps: []resource.TestStep{ - { - Config: testAccExampleWidgetConfig, - Check: resource.ComposeTestCheckFunc( - testAccCheckExampleWidgetExists("example_widget.bar", &widget), - testAccCheckExampleWidgetAttributes(&widget), - resource.TestCheckResourceAttr("example_widget.bar", "active", "true"), - ), - }, - }, - }) -} - -// testAccCheckExampleWidgetAttributes verifies attributes are set correctly by -// Terraform -func testAccCheckExampleWidgetAttributes(widget *example.WidgetDescription) resource.TestCheckFunc { - return func(s *terraform.State) error { - if *widget.active != true { - return fmt.Errorf("widget is not active") - } - - return nil - } -} - -// testAccCheckExampleWidgetExists uses the Example SDK directly to retrieve -// the Widget description, and stores it in the provided -// *example.WidgetDescription -func testAccCheckExampleWidgetExists(resourceName string, widget *example.WidgetDescription) resource.TestCheckFunc { - return func(s *terraform.State) error { - // retrieve the resource by name from state - rs, ok := s.RootModule().Resources[resourceName] - if !ok { - return fmt.Errorf("Not found: %s", resourceName) - } - - if rs.Primary.ID == "" { - return fmt.Errorf("Widget ID is not set") - } - - // retrieve the client from the test provider - client := testAccProvider.Meta().(*ExampleClient) - - response, err := client.DescribeWidgets(&example.DescribeWidgetsInput{ - WidgetIDs: []string{rs.Primary.ID}, - }) - - if err != nil { - return err - } - - // we expect only a single widget by this ID. If we find zero, or many, - // then we consider this an error - if len(response.WidgetDescriptions) != 1 || - *response.WidgetDescriptions[0].WidgetID != rs.Primary.ID { - return fmt.Errorf("Widget not found") - } - - // store the resulting widget in the *example.WidgetDescription pointer - *widget = *response.WidgetDescriptions[0] - return nil - } -} -``` - -## 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. diff --git a/website/docs/plugin/testing/acceptance-tests/tfversion-checks.mdx b/website/docs/plugin/testing/acceptance-tests/tfversion-checks.mdx deleted file mode 100644 index cb2865b7a..000000000 --- a/website/docs/plugin/testing/acceptance-tests/tfversion-checks.mdx +++ /dev/null @@ -1,259 +0,0 @@ ---- -page_title: 'Plugin Development - Acceptance Testing: Terraform Version Checks' -description: >- - Terraform Version Checks are generic checks defined at the TestCase level that check logic against the Terraform CLI version. The testing module - provides built-in Version Checks for common use-cases, but custom Version Checks can also be implemented. ---- - -# Terraform Version Checks - -**Terraform Version Checks** are generic checks defined at the TestCase level that check logic against the Terraform CLI version. The checks are executed at the beginning of the TestCase before any TestStep is executed. - -The Terraform CLI version is determined by the binary selected by the [`TF_ACC_TERRAFORM_PATH`](/terraform/plugin/testing/acceptance-tests#environment-variables) environment variable value, installed by the [`TF_ACC_TERRAFORM_VERSION`](/terraform/plugin/testing/acceptance-tests#environment-variables) value, or already existing based on the `PATH` environment variable. - -A **version check** will either return an error and fail the associated test, return a skip message and pass the associated test immediately by skipping, or it will return nothing and allow the associated test to run. - -## Built-in Version Checks and Variables - -The `terraform-plugin-testing` module provides a package [`tfversion`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion) with built-in version checks for common use-cases. There are three types of version checks: Skip Checks, Require Checks, and Collection Checks. - -## Version Variables - -The built-in checks in the `tfversion` package typically require the use of the [`github.com/hashicorp/go-version`](https://pkg.go.dev/github.com/hashicorp/go-version) module [`version.Version`](https://pkg.go.dev/github.com/hashicorp/go-version#Version) type. To simplify provider testing implementations, the `tfversion` package provides [built-in variables](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion#pkg-variables) for common use case versions, such as each released minor and major Terraform version. These follow the pattern of `Version{MAJOR}_{MINOR}_{PATCH}` with the major, minor, and patch version numbers, such as `Version1_2_0`. - -### Skip Version Checks - -Skip Version Checks will pass the associated test by skipping and provide a skip message if the detected Terraform CLI version satisfies the specified check criteria. - -| Check | Description | -|---------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------| -| [`tfversion.SkipAbove(maximumVersion)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion#SkipAbove) | Skips the test if the Terraform CLI version is above the given maximum. | -| [`tfversion.SkipBelow(minimumVersion)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion#SkipBelow) | Skips the test if the Terraform CLI version is below the given minimum. | -| [`tfversion.SkipBetween(minimumVersion, maximumVersion)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion#SkipBetween) | Skips the test if the Terraform CLI version is between the given minimum (inclusive) and maximum (exclusive). | -| [`tfversion.SkipIf(version)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion#SkipIf) | Skips the test if the Terraform CLI version matches the given version. | - -#### Example using `tfversion.SkipBetween` - -The built-in [`tfversion.SkipBetween`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion#SkipBetween) version check is useful for skipping all patch versions associated with a minor version. - -In the following example, we have written a test that skips all Terraform CLI patch versions associated with 0.14.0: - -```go -package example_test - -import ( - "testing" - - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/tfversion" -) - -func Test_Skip_TF14(t *testing.T) { - t.Parallel() - - resource.UnitTest(t, resource.TestCase{ - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "test": func() (tfprotov6.ProviderServer, error) { - return nil, nil - }, - }, - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBetween(tfversion.Version0_14_0, tfversion.Version0_15_0), - }, - Steps: []resource.TestStep{ - { - Config: `//example test config`, - }, - }, - }) -} -``` - -### Require Version Checks - -Require Version Checks will raise an error and fail the associated test if the detected Terraform CLI version does not satisfy the specified check requirements. - -| Check | Description | -|---------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------| -| [`tfversion.RequireAbove(minimumVersion)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion#RequireAbove) | Fails the test if the Terraform CLI version is below the given maximum. | -| [`tfversion.RequireBelow(maximumVersion)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion#RequireBelow) | Fails the test if the Terraform CLI version is above the given minimum. | -| [`tfversion.RequireBetween(minimumVersion, maximumVersion)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion#RequireBetween) | Fails the test if the Terraform CLI version is outside the given minimum (exclusive) and maximum (inclusive). | -| [`tfversion.RequireNot(version)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion#RequireNot) | Fails the test if the Terraform CLI version matches the given version. | - - -#### Example using `tfversion.RequireAbove` - -The built-in [`tfversion.RequireAbove`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion#RequireAbove) version check is useful for required tests that may use features only available in newer versions of the Terraform ClI. - -In the following example, the test Terraform configuration uses the `nullable` argument for an input variable, a feature that is only available in Terraform CLI versions `1.3.0` and above. The version check will fail the test with a specific error if the detected version is below `1.3.0`. - -```go -package example_test - -import ( - "testing" - - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/tfversion" -) - -func Test_Require_TF1_3(t *testing.T) { - t.Parallel() - - resource.UnitTest(t, resource.TestCase{ - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "test": func() (tfprotov6.ProviderServer, error) { - return nil, nil - }, - }, - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.RequireAbove(tfversion.Version1_3_0), - }, - Steps: []resource.TestStep{ - { - Config: `variable "a" { - nullable = true - default = "hello" - }`, - }, - }, - }) -} -``` - -### Collection Version Checks - -Collection Version Checks operate on multiple version checks and can be used to create more complex checks. - -[`tfversion.Any(TerraformVersionChecks...)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion#Any) will run the associated test by returning a nil error and empty skip message -if any of the given version sub-checks return a nil error and empty skip message. If none of the sub-checks return a nil error and empty skip message, then the check will return all sub-check errors and fail the associated test. -Otherwise, if none of the sub-checks return a non-nil error, the check will pass the associated test by skipping and return all sub-check skip messages. - -[`tfversion.All(TerraformVersionChecks...)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion#All) will either fail or skip the associated test if any of the given sub-checks return a non-nil error or non-empty skip message. The check will return the -first non-nil error or non-empty skip message from the given sub-checks in the order that they are given. Otherwise, if all sub-checks return a nil error and empty skip message, then the check will return a nil error and empty skip message and run the associated test. This check should only be -used in conjunction with `tfversion.Any()` as the behavior provided by this check is applied to the `TerraformVersionChecks` field by default. - -#### Example using `tfversion.Any` - -In the following example, the test will only run if either the Terraform CLI version is above `1.2.0` or if it's below `1.0.0` but not version `0.15.0`, otherwise an error will be returned. - -```go -package example_test - -import ( - "testing" - - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/tfversion" -) - -func Test_Any(t *testing.T) { - t.Parallel() - - resource.UnitTest(t, resource.TestCase{ - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature - return nil, nil - }, - }, - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.Any( - tfversion.All( - tfversion.RequireNot(tfversion.Version0_15_0), - tfversion.RequireBelow(tfversion.Version1_0_0), - ), - tfversion.RequireAbove(tfversion.Version1_2_0), - ), - }, - Steps: []resource.TestStep{ - { - Config: `//example test config`, - }, - }, - }) -} -``` - - -## Custom Version Checks - -The package [`tfversion`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion) also provides the [`TerraformVersionCheck`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion#TerraformVersionCheck) interface, which can be implemented for a custom version check. - -The [`tfversion.CheckTerraformVersionRequest`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion#CheckTerraformVersionRequest) has a `TerraformVersion` field of type [`*version.Version`](https://pkg.go.dev/github.com/hashicorp/go-version#Version) which contains the version of the Terraform CLI binary running the test. - -The [`tfversion.CheckTerraformVersionResponse`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion#CheckTerraformVersionResponse) has an `Error` field and a `Skip` field. The behavior of the version check depends on which field is populated. Populating the `Error` field will fail the associated test with the given error. -Populating the `Skip` field will pass the associated test by skipping the test with the given skip message. Only one of these fields should be populated. - -Here is an example implementation of a version check returns an error if the detected Terraform CLI version matches the given version: - -```go -package example_test - -import ( - "context" - "fmt" - - "github.com/hashicorp/go-version" -) - -// Ensure implementation satisfies the tfversion.TerraformVersionCheck interface. -var _ tfversion.TerraformVersionCheck = requireNotCheck{} - -// RequireNot will fail the test if the given version matches. -func RequireNot(v *version.Version) tfversion.TerraformVersionCheck { - return requireNotCheck{ - version: v, - } -} - -type requireNotCheck struct { - version *version.Version -} - -func (s requireNotCheck) CheckTerraformVersion(ctx context.Context, req tfversion.CheckTerraformVersionRequest, resp *tfversion.CheckTerraformVersionResponse) { - if req.TerraformVersion.Equal(s.version) { - resp.Error = fmt.Errorf("unexpected Terraform CLI version: %s", s.version) - } -} -``` - -And example usage: - -```go -package example_test - -import ( - "testing" - - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/tfversion" -) - -func Test_RequireNot(t *testing.T) { - t.Parallel() - - resource.UnitTest(t, resource.TestCase{ - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "test": func() (tfprotov6.ProviderServer, error) { - return nil, nil - }, - }, - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.RequireNot(tfversion.Version0_13_0), - }, - Steps: []resource.TestStep{ - { - Config: `//example test config`, - }, - }, - }) -} -``` diff --git a/website/docs/plugin/testing/index.mdx b/website/docs/plugin/testing/index.mdx deleted file mode 100644 index 34824c81e..000000000 --- a/website/docs/plugin/testing/index.mdx +++ /dev/null @@ -1,77 +0,0 @@ ---- -page_title: Plugin Development - Testing -description: |- - Learn how to write successful acceptance and unit tests for Terraform - plugins. ---- - -# Testing Terraform Plugins - -Here we cover information needed to write successful tests for Terraform -Plugins. Tests are a vital part of the Terraform ecosystem, verifying we can -deliver on our mission to safely and predictably create, change, and improve -infrastructure. Documentation for Terraform tests falls into categories -briefly described on this page. Each category has more detailed information on a separate page within the documentation. - --> **Note:** Recent versions of Terraform CLI also support [developer overrides in the CLI configuration](/terraform/cli/config/config-file#development-overrides-for-provider-developers), which can be useful for manually testing providers. The acceptance testing framework uses real Terraform CLI executions, so we only recommend developer overrides as a last resort option for missing functionality. - -## Acceptance Tests - -In order to -deliver on our promise to be safe and predictable, we need to be able to easily -and routinely verify that Terraform Plugins produce the expected outcome. The -most common usage of an acceptance test is in Terraform Providers, where each -Resource is tested with configuration files and the resulting infrastructure is -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 -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 -cases against the actual APIs, and verifies both local state and remote values -match. Acceptance tests require a network connection and often require -credentials to access an account for the given API. When writing and testing -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: - -```shell -$ make testacc -``` - -See [Acceptance Testing](/terraform/plugin/testing/acceptance-tests) to learn more. - -## Unit Tests - -Testing plugin code in small, isolated units is distinct from Acceptance Tests, -and does not require network connections. Unit tests are commonly used for -testing helper methods that expand or flatten API response data into data -structures for storage into state by Terraform. This section covers the -specifics of writing Unit Tests for Terraform Plugin code. - -For a given plugin, Unit Tests can be run from the root of the project by using -a common make task: - -```shell -$ make test -``` - -See [Unit Testing](/terraform/plugin/testing/unit-testing) to learn more. - -## Testing Patterns - -Terraform developers are encouraged to write acceptance tests that create real -resource to verify the behavior of plugins, ensuring a reliable and safe -way to manage infrastructure. In [Testing Patterns](/terraform/plugin/testing/testing-patterns) we cover -some basic acceptance tests that almost all resources should have to validate -not only the functionality of the resource, but that the resource behaves as -Terraform would expect. \ No newline at end of file diff --git a/website/docs/plugin/testing/migrating.mdx b/website/docs/plugin/testing/migrating.mdx deleted file mode 100644 index 5128f61b8..000000000 --- a/website/docs/plugin/testing/migrating.mdx +++ /dev/null @@ -1,80 +0,0 @@ ---- -page_title: 'Plugin Development: Migrating testing from SDKv2 to the testing module' -description: >- - Migrate your provider's acceptance testing dependencies from SDKv2 to the testing module. ---- - -# Overview - -This guide helps you migrate a Terraform provider's acceptance testing dependencies from SDKv2 to the plugin testing module. We recommend migrating to terraform-plugin-testing to take advantage of new features of the testing module and to avoid importing the SDKv2 for providers that are built on the plugin Framework. - -This guide provides information and examples for most common use cases, but it does not discuss every nuance of migration. You can ask additional migration questions in the [HashiCorp Discuss forum](https://discuss.hashicorp.com/c/terraform-providers/tf-plugin-sdk/43). To request additions or updates to this guide, submit issues or pull requests to the [`terraform-plugin-testing` repository](https://github.com/hashicorp/terraform-plugin-testing). - -## Migration steps - -Take the following steps when you migrate a provider's acceptance tests from SDKv2 to the testing module. - -Change all instances of the following Go import statements in `*_test.go` files: - -| Original Import | Migrated Import | -|---|---| -| `github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest` | `github.com/hashicorp/terraform-plugin-testing/helper/acctest` | -| `github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource` | `github.com/hashicorp/terraform-plugin-testing/helper/resource` | -| `github.com/hashicorp/terraform-plugin-sdk/v2/terraform` | `github.com/hashicorp/terraform-plugin-testing/terraform` | - -Change all instances of the following in **non-test** `*.go` files: - -| Original Reference | Migrated Reference | -|---|---| -| `github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource.NonRetryableError` | `github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry.NonRetryableError` | -| `github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource.NotFoundError` | `github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry.NotFoundError` | -| `github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource.PrefixedUniqueId` | `github.com/hashicorp/terraform-plugin-sdk/v2/helper/id.PrefixedUniqueId` | -| `github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource.RetryableError` | `github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry.RetryableError` | -| `github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource.RetryContext` | `github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry.RetryContext` | -| `github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource.RetryError` | `github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry.RetryError` | -| `github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource.RetryFunc` | `github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry.RetryFunc` | -| `github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource.StateChangeConf` | `github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry.StateChangeConf` | -| `github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource.StateRefreshFunc` | `github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry.StateRefreshFunc` | -| `github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource.TimeoutError` | `github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry.TimeoutError` | -| `github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource.UnexpectedStateError` | `github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry.UnexpectedStateError` | -| `github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource.UniqueId` | `github.com/hashicorp/terraform-plugin-sdk/v2/helper/id.UniqueId` | -| `github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource.UniqueIdPrefix` | `github.com/hashicorp/terraform-plugin-sdk/v2/helper/id.UniqueIdPrefix` | -| `github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource.UniqueIDSuffixLength` | `github.com/hashicorp/terraform-plugin-sdk/v2/helper/id.UniqueIDSuffixLength` | - -Get and download the latest version of terraform-plugin-testing: - -```shell -$ go get github.com/hashicorp/terraform-plugin-testing@latest -``` - -Clean up `go.mod`: - -```shell -$ go mod tidy -``` - -Verify that the tests are working as expected. - -## Troubleshooting - -### flag redefined Panic - -This panic occurs when your provider code imports both the `github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource` and `github.com/hashicorp/terraform-plugin-testing/helper/resource` packages because they contain a duplicate `TestMain` function: - -```text -panic: XXX flag redefined: sweep - -goroutine 1 [running]: -flag.(*FlagSet).Var(0x14000030240, {0x10132b6d8, 0x140002219c0}, {0x10103ad88, 0x5}, {0x10105d47b, 0x29}) - /usr/local/go/src/flag/flag.go:982 +0x2a4 -flag.(*FlagSet).StringVar(...) - /usr/local/go/src/flag/flag.go:847 -flag.(*FlagSet).String(0x1400031fb98?, {0x10103ad88, 0x5}, {0x0, 0x0}, {0x10105d47b, 0x29}) - /usr/local/go/src/flag/flag.go:860 +0x98 -flag.String(...) - /usr/local/go/src/flag/flag.go:867 -github.com/hashicorp/terraform-plugin-testing/helper/resource.init() - /XXX/go/pkg/mod/github.com/hashicorp/terraform-plugin-testing@v1.1.0/helper/resource/testing.go:53 +0x44 -``` - -Remove imports of `github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource` to resolve the issue. terraform-plugin-sdk version 2.26.0 introduced separate packages, [`github.com/hashicorp/terraform-plugin-sdk/v2/helper/id`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-sdk/v2/helper/id) and [`github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry), which contain all non-testing functionality. diff --git a/website/docs/plugin/testing/testing-patterns.mdx b/website/docs/plugin/testing/testing-patterns.mdx deleted file mode 100644 index 985a42288..000000000 --- a/website/docs/plugin/testing/testing-patterns.mdx +++ /dev/null @@ -1,388 +0,0 @@ ---- -page_title: Plugin Development - Testing Patterns -description: |- - Testing Patterns covers essential acceptance test patterns to implement for - Terraform resources. ---- - -# Testing Patterns - -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 -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. - -## Table of Contents - -- [Built-in Patterns](#built-in-patterns) -- [Basic test to verify attributes](#basic-test-to-verify-attributes) -- [Update test verify configuration changes](#update-test-verify-configuration-changes) -- [Expecting errors or non-empty plans](#expecting-errors-or-non-empty-plans) -- [Regression tests](#regression-tests) - -## 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 -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. - -Each TestCase will run any [PreCheck][3] function provided before running the -test, and then any [CheckDestroy][4] functions after the test concludes. These -functions allow developers to verify the state of the resource and test before -and after it runs. - -When a test is ran, Terraform runs plan, apply, refresh, and then final plan for -each [TestStep][5] in the TestCase. If the last plan results in a non-empty -plan, Terraform will exit with an error. This enables developers to ensure that -configurations apply cleanly. In the case of introducing regression tests or -otherwise testing specific error behavior, TestStep offers a boolean field -[ExpectNonEmptyPlan][6] as well [ExpectError][7] regex field to specify ways the -test framework can handle expected failures. If these properties are omitted and -either a non-empty plan occurs or an error encountered, Terraform will fail the -test. - -After all TestSteps have been ran, Terraform then runs destroy, and ends by -running any CheckDestroy function provided. - -[Back to top](#table-of-contents) - -## Basic test to verify attributes - -The most basic resource acceptance test should use what is likely to be a common -configuration for the resource under test, and verify that Terraform can -correctly create the resource, and that resources attributes are what Terraform -expects them to be. At a high level, the first basic test for a resource should -establish the following: - -- Terraform can plan and apply a common resource configuration without error. -- Verify the expected attributes are saved to state, and contain the values - expected. -- Verify the values in the remote API/Service for the resource match - what is stored in state. -- Verify that a subsequent terraform plan does not produce - a diff/change. - -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]. - -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. - -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 -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. - -Here’s an example test, with in-line comments to demonstrate the key parts of a -basic test. - -```go -package example - -// example.Widget represents a concrete Go type that represents an API resource -func TestAccExampleWidget_basic(t *testing.T) { - var widget example.Widget - - // generate a random name for each widget test run, to avoid - // collisions from multiple concurrent tests. - // the acctest package includes many helpers such as RandStringFromCharSet - // See https://pkg.go.dev/github.com/hashicorp/terraform-plugin-sdk/helper/acctest - rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) - - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckExampleResourceDestroy, - Steps: []resource.TestStep{ - { - // 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), - ), - }, - }, - }) -} - -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(` -resource "example_widget" "foo" { - active = true - name = "%s" -}`, name) -} -``` - -This example covers all the items needed for a basic test, and will be -referenced or added to in the other test cases to come. - -[Back to top](#table-of-contents) - -## Update test verify configuration changes - -A basic test covers a simple configuration that should apply successfully and -with no follow up differences in state. To verify a resource correctly applies -updates, the second most common test found is an extension of the basic test, -that simply applies another `TestStep` with a modified version of the original -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. - -```go -func TestAccExampleWidget_update(t *testing.T) { - var widget example.Widget - rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) - - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckExampleResourceDestroy, - Steps: []resource.TestStep{ - { - // 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), - ), - }, - { - // 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), - ), - }, - }, - }) -} - -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(` -resource "example_widget" "foo" { - active = false - name = "%s" -}`, name) -} -``` - -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. - -[Back to top](#table-of-contents) - -## Expecting errors or non-empty plans - -The number of acceptance tests for a given resource typically start small with -the basic and update scenarios covered. Other tests should be added to -demonstrate common expected configurations or behavior scenarios for a given -resource, such as typical updates or changes to configuration, or exercising -logic that uses polling for updates such as an autoscaling group adding or -draining instances. - -It is possible for scenarios to exist where a valid configuration (no errors -during `plan`) would result in a non-empty `plan` after successfully running -`terraform apply`. This is typically due to a valid but otherwise -misconfiguration of the resource, and is generally undesirable. Occasionally it -is useful to intentionally create this scenario in an early `TestStep` in order -to demonstrate correcting the state with proper configuration in a follow-up -`TestStep`. Normally a `TestStep` that results in a non-empty plan would fail -the test after apply, however developers can use the `ExpectNonEmptyPlan` -attribute to prevent failure and allow the `TestCase` to continue: - -```go -func TestAccExampleWidget_expectPlan(t *testing.T) { - var widget example.Widget - rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) - - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckExampleResourceDestroy, - Steps: []resource.TestStep{ - { - // 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), - ), - ExpectNonEmptyPlan: true, - }, - { - // apply the complete configuration - Config: testAccExampleResourceComplete(rName), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("example_widget.foo", "name", rName), - ), - }, - }, - }) -} -``` - -In addition to `ExpectNonEmptyPlan`, `TestStep` also exposes an `ExpectError` -hook, allowing developers to test configuration that they expect to produce an -error, such as configuration that fails schema validators: - -```go -func TestAccExampleWidget_expectError(t *testing.T) { - var widget example.Widget - rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) - - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckExampleResourceDestroy, - Steps: []resource.TestStep{ - { - // use a configuration that we expect to fail a validator - // on the resource Name attribute, which only allows alphanumeric - // characters - Config: testAccExampleResourceError(rName + "*$%%^"), - // No check function is given because we expect this configuration - // to fail before any infrastructure is created - ExpectError: regexp.MustCompile("Widget names may only contain alphanumeric characters"), - }, - }, - }) -} -``` - -`ExpectError` expects a valid regular expression, and the error message must -match in order to consider the error as expected and allow the test to pass. If -the regular expression does not match, the `TestStep` fails explaining that the -configuration did not produce the error expected. - -[Back to top](#table-of-contents) - -## Regression tests - -As resources are put into use, issues can arise as bugs that need to be fixed -and released in a new version. Developers are encouraged to introduce regression -tests that demonstrate not only any bugs reported, but that code modified to -address any bug is verified as fixing the issues. These regression tests should -be named and documented appropriately to identify the issue(s) they demonstrate -fixes for. When possible the documentation for a regression test should include -a link to the original bug report. - -An ideal bug fix would include at least 2 commits to source control: - -A single commit introducing the regression test, verifying the issue(s) 1 or -more commits that modify code to fix the issue(s) - -This allows other developers to independently verify that a regression test -indeed reproduces the issue by checking out the source at that commit first, and -then advancing the revisions to evaluate the fix. - -[Back to top](#table-of-contents) - -# Conclusion - -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 -have using their plugins, and ensure that Terraform remains a world-class tool -for safely managing infrastructure. - -[1]: /terraform/plugin/testing - -[2]: /terraform/plugin/testing/acceptance-tests/testcase - -[3]: /terraform/plugin/testing/acceptance-tests/testcase#precheck - -[4]: /terraform/plugin/testing/acceptance-tests/testcase#checkdestroy - -[5]: /terraform/plugin/testing/acceptance-tests/teststep - -[6]: https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/helper/resource#TestStep.ExpectNonEmptyPlan - -[7]: https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/helper/resource#TestStep.ExpectError - -[8]: /terraform/plugin/testing/acceptance-tests/teststep#check-functions - -[9]: /terraform/plugin/testing/acceptance-tests/teststep#builtin-check-functions \ No newline at end of file diff --git a/website/docs/plugin/testing/unit-testing.mdx b/website/docs/plugin/testing/unit-testing.mdx deleted file mode 100644 index c8b8690c1..000000000 --- a/website/docs/plugin/testing/unit-testing.mdx +++ /dev/null @@ -1,93 +0,0 @@ ---- -page_title: Plugin Development - Unit Testing -description: |- - Unit tests are commonly used for testing helper methods that expand or - flatten API responses into data structures that Terraform stores as state. ---- - -# Unit Testing - -Testing plugin code in small, isolated units is distinct from Acceptance Tests, -and does not require network connections. Unit tests are commonly used for -testing helper methods that expand or flatten API responses into data structures -for storage into state by Terraform. This section covers the specifics of -writing Unit Tests for Terraform Plugin code. - -The procedure for writing unit tests for Terraform follows the same setup and -conventions of writing any Go unit tests. We recommend naming tests to follow -the same convention as our acceptance tests, `Test_`. For more -information on Go tests, see the [official Golang docs on testing](https://pkg.go.dev/testing). - -Below is an example unit test used in flattening AWS security group rules, -demonstrating a typical `flattener` type method that's commonly used to convert -structures returned from APIs into data structures used by Terraform in saving -to state. This example is truncated for brevity, but you can see the full test in the -[aws/structure_test.go in the Terraform AWS Provider -repository on GitHub](https://github.com/hashicorp/terraform-provider-aws/blob/f22ae122d8407672bd38951f80a2813b8b9af683/aws/structure_test.go#L930-L1027) - -```go -func TestFlattenSecurityGroups(t *testing.T) { - cases := []struct { - ownerId *string - pairs []*ec2.UserIdGroupPair - expected []*GroupIdentifier - }{ - // simple, no user id included - { - ownerId: aws.String("user1234"), - pairs: []*ec2.UserIdGroupPair{ - &ec2.UserIdGroupPair{ - GroupId: aws.String("sg-12345"), - }, - }, - expected: []*GroupIdentifier{ - &GroupIdentifier{ - GroupId: aws.String("sg-12345"), - }, - }, - }, - // include the owner id, but keep it consitent with the same account. Tests - // EC2 classic situation - { - ownerId: aws.String("user1234"), - pairs: []*ec2.UserIdGroupPair{ - &ec2.UserIdGroupPair{ - GroupId: aws.String("sg-12345"), - UserId: aws.String("user1234"), - }, - }, - expected: []*GroupIdentifier{ - &GroupIdentifier{ - GroupId: aws.String("sg-12345"), - }, - }, - }, - - // include the owner id, but from a different account. This is reflects - // EC2 Classic when referring to groups by name - { - ownerId: aws.String("user1234"), - pairs: []*ec2.UserIdGroupPair{ - &ec2.UserIdGroupPair{ - GroupId: aws.String("sg-12345"), - GroupName: aws.String("somegroup"), // GroupName is only included in Classic - UserId: aws.String("user4321"), - }, - }, - expected: []*GroupIdentifier{ - &GroupIdentifier{ - GroupId: aws.String("sg-12345"), - GroupName: aws.String("user4321/somegroup"), - }, - }, - }, - } - - for _, c := range cases { - out := flattenSecurityGroups(c.pairs, c.ownerId) - if !reflect.DeepEqual(out, c.expected) { - t.Fatalf("Error matching output and expected: %#v vs %#v", out, c.expected) - } - } -} -``` diff --git a/website/img/.gitkeep b/website/img/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/website/package-lock.json b/website/package-lock.json deleted file mode 100644 index d2aa00e2f..000000000 --- a/website/package-lock.json +++ /dev/null @@ -1,2726 +0,0 @@ -{ - "name": "terraform-plugin-testing-docs-preview", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "terraform-plugin-testing-docs-preview", - "devDependencies": { - "@hashicorp/platform-content-conformance": "^0.0.10", - "next": "^12.1.0" - }, - "engines": { - "npm": ">=7.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.12.9", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.9.tgz", - "integrity": "sha512-gTXYh3M5wb7FRXQy+FErKFAv90BnlOuNn1QkCK2lREoPAjrQCO49+HVSrFoe5uakFAF5eenS75KbO2vQiLrTMQ==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.12.5", - "@babel/helper-module-transforms": "^7.12.1", - "@babel/helpers": "^7.12.5", - "@babel/parser": "^7.12.7", - "@babel/template": "^7.12.7", - "@babel/traverse": "^7.12.9", - "@babel/types": "^7.12.7", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.1", - "json5": "^2.1.2", - "lodash": "^4.17.19", - "resolve": "^1.3.2", - "semver": "^5.4.1", - "source-map": "^0.5.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.20.14", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.14.tgz", - "integrity": "sha512-AEmuXHdcD3A52HHXxaTmYlb8q/xMEhoRP67B3T4Oq7lbmSoqroMZzjnGj3+i1io3pdnF8iBYVu4Ilj+c4hBxYg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.20.7", - "@jridgewell/gen-mapping": "^0.3.2", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", - "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", - "dev": true, - "dependencies": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", - "dev": true, - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", - "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.20.11", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.11.tgz", - "integrity": "sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-simple-access": "^7.20.2", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/helper-validator-identifier": "^7.19.1", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.20.10", - "@babel/types": "^7.20.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", - "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.20.13", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.13.tgz", - "integrity": "sha512-nzJ0DWCL3gB5RCXbUO3KIMMsBY2Eqbx8mBpKGE/02PgyRQFcPQLbkQ1vyy596mZLaP+dAfD+R4ckASzNVmW3jg==", - "dev": true, - "dependencies": { - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.20.13", - "@babel/types": "^7.20.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.20.15", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.15.tgz", - "integrity": "sha512-DI4a1oZuf8wC+oAJA9RW6ga3Zbe8RZFt7kD9i4qAspz3I/yHet1VvC3DiSy/fsUvv5pvJuNPh0LPOdCcqinDPg==", - "dev": true, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-proposal-object-rest-spread": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.12.1.tgz", - "integrity": "sha512-s6SowJIjzlhx8o7lsFx5zmY4At6CTtDvgNQDdPzkBQucle58A6b/TTeEBYtyDgmcXjUTM+vE8YOGHZzzbc/ioA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.0", - "@babel/plugin-transform-parameters": "^7.12.1" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.1.tgz", - "integrity": "sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.20.7.tgz", - "integrity": "sha512-WiWBIkeHKVOSYPO0pWkxGPfKeWrCJyD3NJ53+Lrp/QMSZbsVPovrVl2aWZ19D/LTVnaDv5Ap7GJ/B2CTOZdrfA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters/node_modules/@babel/helper-plugin-utils": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", - "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", - "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.20.13", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.13.tgz", - "integrity": "sha512-kMJXfF0T6DIS9E8cgdLCSAL+cuCK+YEZHWiLK0SXpTo8YRj5lpJu3CDNKiIBCne4m9hhTIqUg6SYTAI39tAiVQ==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.7", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.20.13", - "@babel/types": "^7.20.7", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.7.tgz", - "integrity": "sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@hashicorp/platform-content-conformance": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/@hashicorp/platform-content-conformance/-/platform-content-conformance-0.0.10.tgz", - "integrity": "sha512-vXLbd2w9phS4JfFyh17jCiyu+LXVonTfb7WEUK2eMlOL/wxe2umyJvEQaJNzD5bwyYC8LuXGA5JkbnPXnU5ZQg==", - "dev": true, - "dependencies": { - "find-up": "^6.3.0", - "flat": "^5.0.2", - "globby": "^13.1.2", - "mdast-util-to-string": "^3.1.0", - "remark": "12.0.1", - "remark-mdx": "^1.6.22", - "unified-lint-rule": "^2.1.1", - "unist-util-stringify-position": "^3.0.2", - "unist-util-visit": "^4.1.1", - "vfile": "^5.3.6", - "vfile-matter": "^4.0.0", - "vfile-reporter": "^7.0.4", - "vfile-reporter-json": "^3.2.0", - "vfile-statistics": "^2.0.0", - "yaml": "^2.1.3", - "yargs": "^17.4.1", - "zod": "^3.19.1" - }, - "bin": { - "hc-content": "dist/cli.js" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "dev": true, - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", - "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - }, - "node_modules/@mdx-js/util": { - "version": "1.6.22", - "resolved": "https://registry.npmjs.org/@mdx-js/util/-/util-1.6.22.tgz", - "integrity": "sha512-H1rQc1ZOHANWBvPcW+JpGwr+juXSxM8Q8YCkm3GhZd8REu1fHR3z99CErO1p9pkcfcxZnMdIZdIsXkOHY0NilA==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@next/env": { - "version": "12.3.4", - "resolved": "https://registry.npmjs.org/@next/env/-/env-12.3.4.tgz", - "integrity": "sha512-H/69Lc5Q02dq3o+dxxy5O/oNxFsZpdL6WREtOOtOM1B/weonIwDXkekr1KV5DPVPr12IHFPrMrcJQ6bgPMfn7A==", - "dev": true - }, - "node_modules/@next/swc-android-arm-eabi": { - "version": "12.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.3.4.tgz", - "integrity": "sha512-cM42Cw6V4Bz/2+j/xIzO8nK/Q3Ly+VSlZJTa1vHzsocJRYz8KT6MrreXaci2++SIZCF1rVRCDgAg5PpqRibdIA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-android-arm64": { - "version": "12.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-12.3.4.tgz", - "integrity": "sha512-5jf0dTBjL+rabWjGj3eghpLUxCukRhBcEJgwLedewEA/LJk2HyqCvGIwj5rH+iwmq1llCWbOky2dO3pVljrapg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-arm64": { - "version": "12.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.3.4.tgz", - "integrity": "sha512-DqsSTd3FRjQUR6ao0E1e2OlOcrF5br+uegcEGPVonKYJpcr0MJrtYmPxd4v5T6UCJZ+XzydF7eQo5wdGvSZAyA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "12.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-12.3.4.tgz", - "integrity": "sha512-PPF7tbWD4k0dJ2EcUSnOsaOJ5rhT3rlEt/3LhZUGiYNL8KvoqczFrETlUx0cUYaXe11dRA3F80Hpt727QIwByQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-freebsd-x64": { - "version": "12.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.3.4.tgz", - "integrity": "sha512-KM9JXRXi/U2PUM928z7l4tnfQ9u8bTco/jb939pdFUHqc28V43Ohd31MmZD1QzEK4aFlMRaIBQOWQZh4D/E5lQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm-gnueabihf": { - "version": "12.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.3.4.tgz", - "integrity": "sha512-3zqD3pO+z5CZyxtKDTnOJ2XgFFRUBciOox6EWkoZvJfc9zcidNAQxuwonUeNts6Xbm8Wtm5YGIRC0x+12YH7kw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "12.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.3.4.tgz", - "integrity": "sha512-kiX0vgJGMZVv+oo1QuObaYulXNvdH/IINmvdZnVzMO/jic/B8EEIGlZ8Bgvw8LCjH3zNVPO3mGrdMvnEEPEhKA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "12.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.3.4.tgz", - "integrity": "sha512-EETZPa1juczrKLWk5okoW2hv7D7WvonU+Cf2CgsSoxgsYbUCZ1voOpL4JZTOb6IbKMDo6ja+SbY0vzXZBUMvkQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "12.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.3.4.tgz", - "integrity": "sha512-4csPbRbfZbuWOk3ATyWcvVFdD9/Rsdq5YHKvRuEni68OCLkfy4f+4I9OBpyK1SKJ00Cih16NJbHE+k+ljPPpag==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "12.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.3.4.tgz", - "integrity": "sha512-YeBmI+63Ro75SUiL/QXEVXQ19T++58aI/IINOyhpsRL1LKdyfK/35iilraZEFz9bLQrwy1LYAR5lK200A9Gjbg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "12.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.3.4.tgz", - "integrity": "sha512-Sd0qFUJv8Tj0PukAYbCCDbmXcMkbIuhnTeHm9m4ZGjCf6kt7E/RMs55Pd3R5ePjOkN7dJEuxYBehawTR/aPDSQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-ia32-msvc": { - "version": "12.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.3.4.tgz", - "integrity": "sha512-rt/vv/vg/ZGGkrkKcuJ0LyliRdbskQU+91bje+PgoYmxTZf/tYs6IfbmgudBJk6gH3QnjHWbkphDdRQrseRefQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "12.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.4.tgz", - "integrity": "sha512-DQ20JEfTBZAgF8QCjYfJhv2/279M6onxFjdG/+5B0Cyj00/EdBxiWb2eGGFgQhrBbNv/lsvzFbbi0Ptf8Vw/bg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@swc/helpers": { - "version": "0.4.11", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.11.tgz", - "integrity": "sha512-rEUrBSGIoSFuYxwBYtlUFMlE2CwGhmW+w9355/5oduSw8e5h2+Tj4UrAGNNgP9915++wj5vkQo0UuOBqOAq4nw==", - "dev": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/mdast": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz", - "integrity": "sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==", - "dev": true, - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@types/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-dPWnWsf+kzIG140B8z2w3fr5D03TLWbOAFQl45xUpI3vcizeXriNR5VYkWZ+WTMsUHqZ9Xlt3hrxGNANFyNQfw==", - "dev": true - }, - "node_modules/@types/unist": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", - "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==", - "dev": true - }, - "node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/bail": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz", - "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001452", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001452.tgz", - "integrity": "sha512-Lkp0vFjMkBB3GTpLR8zk4NwW5EdRdnitwYJHDOOKIU85x4ckYCPQ+9WlVvSVClHxVReefkUMtWZH2l9KGlD51w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - } - ] - }, - "node_modules/ccount": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-1.1.0.tgz", - "integrity": "sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/character-entities": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", - "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-html4": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-1.1.4.tgz", - "integrity": "sha512-HRcDxZuZqMx3/a+qrzxdBKBPUpxWEq9xw2OPZ3a/174ihfrQKVsFhqtthBInFy1zZ9GgZyFXOatNujm8M+El3g==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-legacy": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", - "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-reference-invalid": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", - "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/collapse-white-space": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.6.tgz", - "integrity": "sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "node_modules/fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", - "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", - "dev": true, - "dependencies": { - "locate-path": "^7.1.0", - "path-exists": "^5.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true, - "bin": { - "flat": "cli.js" - } - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/globby": { - "version": "13.1.3", - "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.3.tgz", - "integrity": "sha512-8krCNHXvlCgHDpegPzleMq07yMYTO2sXKASmZmquEYWEmCx6J5UTRbp5RwMJkTJGtcQ44YpiUYUiN0b9mzy8Bw==", - "dev": true, - "dependencies": { - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.11", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "node_modules/is-alphabetical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", - "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-alphanumeric": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-alphanumeric/-/is-alphanumeric-1.0.0.tgz", - "integrity": "sha512-ZmRL7++ZkcMOfDuWZuMJyIVLr2keE1o/DeNWh1EmgqGhUcV+9BIVsx0BcSBOHTZqzjs4+dISzr2KAeBEWGgXeA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-alphanumerical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", - "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", - "dev": true, - "dependencies": { - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "engines": { - "node": ">=4" - } - }, - "node_modules/is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", - "dev": true, - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-decimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", - "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-hexadecimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", - "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-whitespace-character": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz", - "integrity": "sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-word-character": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-word-character/-/is-word-character-1.0.4.tgz", - "integrity": "sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/locate-path": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", - "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", - "dev": true, - "dependencies": { - "p-locate": "^6.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "node_modules/longest-streak": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-2.0.4.tgz", - "integrity": "sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "peer": true, - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/markdown-escapes": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.4.tgz", - "integrity": "sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/markdown-table": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz", - "integrity": "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==", - "dev": true, - "dependencies": { - "repeat-string": "^1.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/mdast-util-compact": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-compact/-/mdast-util-compact-2.0.1.tgz", - "integrity": "sha512-7GlnT24gEwDrdAwEHrU4Vv5lLWrEer4KOkAiKT9nYstsTad7Oc1TwqT2zIMKRdZF7cTuaf+GA1E4Kv7jJh8mPA==", - "dev": true, - "dependencies": { - "unist-util-visit": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-compact/node_modules/unist-util-is": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", - "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-compact/node_modules/unist-util-visit": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz", - "integrity": "sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^4.0.0", - "unist-util-visit-parents": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-compact/node_modules/unist-util-visit-parents": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz", - "integrity": "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-string": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.1.1.tgz", - "integrity": "sha512-tGvhT94e+cVnQt8JWE9/b3cUQZWS732TJxXHktvP+BYo62PpYD53Ls/6cC60rW21dW+txxiM4zMdc6abASvZKA==", - "dev": true, - "dependencies": { - "@types/mdast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", - "dev": true, - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/next": { - "version": "12.3.4", - "resolved": "https://registry.npmjs.org/next/-/next-12.3.4.tgz", - "integrity": "sha512-VcyMJUtLZBGzLKo3oMxrEF0stxh8HwuW976pAzlHhI3t8qJ4SROjCrSh1T24bhrbjw55wfZXAbXPGwPt5FLRfQ==", - "dev": true, - "dependencies": { - "@next/env": "12.3.4", - "@swc/helpers": "0.4.11", - "caniuse-lite": "^1.0.30001406", - "postcss": "8.4.14", - "styled-jsx": "5.0.7", - "use-sync-external-store": "1.2.0" - }, - "bin": { - "next": "dist/bin/next" - }, - "engines": { - "node": ">=12.22.0" - }, - "optionalDependencies": { - "@next/swc-android-arm-eabi": "12.3.4", - "@next/swc-android-arm64": "12.3.4", - "@next/swc-darwin-arm64": "12.3.4", - "@next/swc-darwin-x64": "12.3.4", - "@next/swc-freebsd-x64": "12.3.4", - "@next/swc-linux-arm-gnueabihf": "12.3.4", - "@next/swc-linux-arm64-gnu": "12.3.4", - "@next/swc-linux-arm64-musl": "12.3.4", - "@next/swc-linux-x64-gnu": "12.3.4", - "@next/swc-linux-x64-musl": "12.3.4", - "@next/swc-win32-arm64-msvc": "12.3.4", - "@next/swc-win32-ia32-msvc": "12.3.4", - "@next/swc-win32-x64-msvc": "12.3.4" - }, - "peerDependencies": { - "fibers": ">= 3.1.0", - "node-sass": "^6.0.0 || ^7.0.0", - "react": "^17.0.2 || ^18.0.0-0", - "react-dom": "^17.0.2 || ^18.0.0-0", - "sass": "^1.3.0" - }, - "peerDependenciesMeta": { - "fibers": { - "optional": true - }, - "node-sass": { - "optional": true - }, - "sass": { - "optional": true - } - } - }, - "node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", - "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", - "dev": true, - "dependencies": { - "p-limit": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse-entities": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", - "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", - "dev": true, - "dependencies": { - "character-entities": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "character-reference-invalid": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-hexadecimal": "^1.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/path-exists": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", - "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.4.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", - "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - } - ], - "dependencies": { - "nanoid": "^3.3.4", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "dev": true, - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "dev": true, - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" - }, - "peerDependencies": { - "react": "^18.2.0" - } - }, - "node_modules/remark": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/remark/-/remark-12.0.1.tgz", - "integrity": "sha512-gS7HDonkdIaHmmP/+shCPejCEEW+liMp/t/QwmF0Xt47Rpuhl32lLtDV1uKWvGoq+kxr5jSgg5oAIpGuyULjUw==", - "dev": true, - "dependencies": { - "remark-parse": "^8.0.0", - "remark-stringify": "^8.0.0", - "unified": "^9.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-mdx": { - "version": "1.6.22", - "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-1.6.22.tgz", - "integrity": "sha512-phMHBJgeV76uyFkH4rvzCftLfKCr2RZuF+/gmVcaKrpsihyzmhXjA0BEMDaPTXG5y8qZOKPVo83NAOX01LPnOQ==", - "dev": true, - "dependencies": { - "@babel/core": "7.12.9", - "@babel/helper-plugin-utils": "7.10.4", - "@babel/plugin-proposal-object-rest-spread": "7.12.1", - "@babel/plugin-syntax-jsx": "7.12.1", - "@mdx-js/util": "1.6.22", - "is-alphabetical": "1.0.4", - "remark-parse": "8.0.3", - "unified": "9.2.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-mdx/node_modules/unified": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.0.tgz", - "integrity": "sha512-vx2Z0vY+a3YoTj8+pttM3tiJHCwY5UFbYdiWrwBEbHmK8pvsPj2rtAX2BFfgXen8T39CJWblWRDT4L5WGXtDdg==", - "dev": true, - "dependencies": { - "bail": "^1.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^2.0.0", - "trough": "^1.0.0", - "vfile": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-mdx/node_modules/unist-util-stringify-position": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", - "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.2" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-mdx/node_modules/vfile": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz", - "integrity": "sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^2.0.0", - "vfile-message": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-mdx/node_modules/vfile-message": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz", - "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-parse": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-8.0.3.tgz", - "integrity": "sha512-E1K9+QLGgggHxCQtLt++uXltxEprmWzNfg+MxpfHsZlrddKzZ/hZyWHDbK3/Ap8HJQqYJRXP+jHczdL6q6i85Q==", - "dev": true, - "dependencies": { - "ccount": "^1.0.0", - "collapse-white-space": "^1.0.2", - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-whitespace-character": "^1.0.0", - "is-word-character": "^1.0.0", - "markdown-escapes": "^1.0.0", - "parse-entities": "^2.0.0", - "repeat-string": "^1.5.4", - "state-toggle": "^1.0.0", - "trim": "0.0.1", - "trim-trailing-lines": "^1.0.0", - "unherit": "^1.0.4", - "unist-util-remove-position": "^2.0.0", - "vfile-location": "^3.0.0", - "xtend": "^4.0.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-stringify": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-8.1.1.tgz", - "integrity": "sha512-q4EyPZT3PcA3Eq7vPpT6bIdokXzFGp9i85igjmhRyXWmPs0Y6/d2FYwUNotKAWyLch7g0ASZJn/KHHcHZQ163A==", - "dev": true, - "dependencies": { - "ccount": "^1.0.0", - "is-alphanumeric": "^1.0.0", - "is-decimal": "^1.0.0", - "is-whitespace-character": "^1.0.0", - "longest-streak": "^2.0.1", - "markdown-escapes": "^1.0.0", - "markdown-table": "^2.0.0", - "mdast-util-compact": "^2.0.0", - "parse-entities": "^2.0.0", - "repeat-string": "^1.5.4", - "state-toggle": "^1.0.0", - "stringify-entities": "^3.0.0", - "unherit": "^1.0.4", - "xtend": "^4.0.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", - "dev": true, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "dev": true, - "dependencies": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", - "dev": true, - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/slash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/state-toggle": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz", - "integrity": "sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/stringify-entities": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-3.1.0.tgz", - "integrity": "sha512-3FP+jGMmMV/ffZs86MoghGqAoqXAdxLrJP4GUdrDN1aIScYih5tuIO3eF4To5AJZ79KDZ8Fpdy7QJnK8SsL1Vg==", - "dev": true, - "dependencies": { - "character-entities-html4": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "xtend": "^4.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/strip-ansi": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", - "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/styled-jsx": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.0.7.tgz", - "integrity": "sha512-b3sUzamS086YLRuvnaDigdAewz1/EFYlHpYBP5mZovKEdQQOIIYq8lApylub3HHZ6xFjV051kkGU7cudJmrXEA==", - "dev": true, - "engines": { - "node": ">= 12.0.0" - }, - "peerDependencies": { - "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/trim": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz", - "integrity": "sha512-YzQV+TZg4AxpKxaTHK3c3D+kRDCGVEE7LemdlQZoQXn0iennk10RsIoY6ikzAqJTc9Xjl9C1/waHom/J86ziAQ==", - "dev": true - }, - "node_modules/trim-trailing-lines": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.4.tgz", - "integrity": "sha512-rjUWSqnfTNrjbB9NQWfPMH/xRK1deHeGsHoVfpxJ++XeYXE0d6B1En37AHfw3jtfTU7dzMzZL2jjpe8Qb5gLIQ==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/trough": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz", - "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", - "dev": true - }, - "node_modules/unherit": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz", - "integrity": "sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ==", - "dev": true, - "dependencies": { - "inherits": "^2.0.0", - "xtend": "^4.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/unified": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz", - "integrity": "sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==", - "dev": true, - "dependencies": { - "bail": "^1.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^2.0.0", - "trough": "^1.0.0", - "vfile": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unified-lint-rule": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/unified-lint-rule/-/unified-lint-rule-2.1.1.tgz", - "integrity": "sha512-vsLHyLZFstqtGse2gvrGwasOmH8M2y+r2kQMoDSWzSqUkQx2MjHjvZuGSv5FUaiv4RQO1bHRajy7lSGp7XWq5A==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "trough": "^2.0.0", - "unified": "^10.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unified-lint-rule/node_modules/bail": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", - "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/unified-lint-rule/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/unified-lint-rule/node_modules/trough": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz", - "integrity": "sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/unified-lint-rule/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unified/node_modules/unist-util-stringify-position": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", - "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.2" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unified/node_modules/vfile": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz", - "integrity": "sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^2.0.0", - "vfile-message": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unified/node_modules/vfile-message": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz", - "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-is": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.0.tgz", - "integrity": "sha512-Glt17jWwZeyqrFqOK0pF1Ded5U3yzJnFr8CG1GMjCWTp9zDo2p+cmD6pWbZU8AgM5WU3IzRv6+rBwhzsGh6hBQ==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-remove-position": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-2.0.1.tgz", - "integrity": "sha512-fDZsLYIe2uT+oGFnuZmy73K6ZxOPG/Qcm+w7jbEjaFcJgbQ6cqjs/eSPzXhsmGpAsWPkqZM9pYjww5QTn3LHMA==", - "dev": true, - "dependencies": { - "unist-util-visit": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-remove-position/node_modules/unist-util-is": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", - "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-remove-position/node_modules/unist-util-visit": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz", - "integrity": "sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^4.0.0", - "unist-util-visit-parents": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-remove-position/node_modules/unist-util-visit-parents": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz", - "integrity": "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/use-sync-external-store": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", - "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", - "dev": true, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-location": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-3.2.0.tgz", - "integrity": "sha512-aLEIZKv/oxuCDZ8lkJGhuhztf/BW4M+iHdCwglA/eWc+vtuRFJj8EtgceYFX4LRjOhCAAiNHsKGssC6onJ+jbA==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-matter": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/vfile-matter/-/vfile-matter-4.0.1.tgz", - "integrity": "sha512-ZeACdaxCOxhePpoLO4A5y/VgI9EuWBXu+sUk65aQ7lXBZDFg7X0tuOzigLJUtsQzazFt6K2m9SdlDxZdfL5vVg==", - "dev": true, - "dependencies": { - "is-buffer": "^2.0.0", - "vfile": "^5.0.0", - "yaml": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-reporter": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/vfile-reporter/-/vfile-reporter-7.0.5.tgz", - "integrity": "sha512-NdWWXkv6gcd7AZMvDomlQbK3MqFWL1RlGzMn++/O2TI+68+nqxCPTvLugdOtfSzXmjh+xUyhp07HhlrbJjT+mw==", - "dev": true, - "dependencies": { - "@types/supports-color": "^8.0.0", - "string-width": "^5.0.0", - "supports-color": "^9.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile": "^5.0.0", - "vfile-message": "^3.0.0", - "vfile-sort": "^3.0.0", - "vfile-statistics": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-reporter-json": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/vfile-reporter-json/-/vfile-reporter-json-3.3.0.tgz", - "integrity": "sha512-/zgRtjxQ2UGJn+HViiZ7+nIXtUzkkXFQum3BmaS/bSyr10P0X41ETRqqwMJ95RtbKUah3m7pKb6oS1eZeXXHzQ==", - "dev": true, - "dependencies": { - "vfile": "^5.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-reporter/node_modules/supports-color": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.3.1.tgz", - "integrity": "sha512-knBY82pjmnIzK3NifMo3RxEIRD9E0kIzV4BKcyTZ9+9kWgLMxd4PrsTSMoFQUabgRBbF8KOLRDCyKgNV+iK44Q==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/vfile-sort": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/vfile-sort/-/vfile-sort-3.0.1.tgz", - "integrity": "sha512-1os1733XY6y0D5x0ugqSeaVJm9lYgj0j5qdcZQFyxlZOSy1jYarL77lLyb5gK4Wqr1d5OxmuyflSO3zKyFnTFw==", - "dev": true, - "dependencies": { - "vfile": "^5.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-statistics": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/vfile-statistics/-/vfile-statistics-2.0.1.tgz", - "integrity": "sha512-W6dkECZmP32EG/l+dp2jCLdYzmnDBIw6jwiLZSER81oR5AHRcVqL+k3Z+pfH1R73le6ayDkJRMk0sutj1bMVeg==", - "dev": true, - "dependencies": { - "vfile": "^5.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/wrap-ansi/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, - "engines": { - "node": ">=0.4" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/yaml": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz", - "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, - "node_modules/yargs": { - "version": "17.6.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", - "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", - "dev": true, - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "3.20.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.20.6.tgz", - "integrity": "sha512-oyu0m54SGCtzh6EClBVqDDlAYRz4jrVtKwQ7ZnsEmMI9HnzuZFj8QFwAY1M5uniIYACdGvv0PBWPF2kO0aNofA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} diff --git a/website/package.json b/website/package.json deleted file mode 100644 index ab6a339e6..000000000 --- a/website/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "terraform-plugin-testing-docs-preview", - "private": "true", - "scripts": { - "build": "./scripts/website-build.sh", - "content-check": "hc-content --config base-docs" - }, - "devDependencies": { - "@hashicorp/platform-content-conformance": "^0.0.10", - "next": "^12.1.0" - }, - "engines": { - "npm": ">=7.0.0" - } -} diff --git a/website/scripts/should-build.sh b/website/scripts/should-build.sh deleted file mode 100644 index b653682c5..000000000 --- a/website/scripts/should-build.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: MPL-2.0 - - -###################################################### -# NOTE: This file is managed by the Digital Team's # -# Terraform configuration @ hashicorp/mktg-terraform # -###################################################### - -# This is run during the website build step to determine if we should skip the build or not. -# More information: https://vercel.com/docs/platform/projects#ignored-build-step - -if [[ "$VERCEL_GIT_COMMIT_REF" == "stable-website" ]] ; then - # Proceed with the build if the branch is stable-website - echo "✅ - Build can proceed" - exit 1; -else - # Check for differences in the website directory - git diff --quiet HEAD^ HEAD ./ -fi \ No newline at end of file diff --git a/website/scripts/website-build.sh b/website/scripts/website-build.sh deleted file mode 100644 index 88fb9028b..000000000 --- a/website/scripts/website-build.sh +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: MPL-2.0 - -###################################################### -# NOTE: This file is managed by the Digital Team's # -# Terraform configuration @ hashicorp/mktg-terraform # -###################################################### - -# Repo which we are cloning and executing npm run build:deploy-preview within -REPO_TO_CLONE=dev-portal -# Set the subdirectory name for the base project -PREVIEW_DIR=website-preview -# The directory we want to clone the project into -CLONE_DIR=website-preview -# The product for which we are building the deploy preview -PRODUCT=terraform-plugin-testing -# Preview mode, controls the UI rendered (either the product site or developer). Can be `io` or `developer` -PREVIEW_MODE=developer - -# Get the git branch of the commit that triggered the deploy preview -# This will power remote image assets in local and deploy previews -CURRENT_GIT_BRANCH=$VERCEL_GIT_COMMIT_REF - -# This is where content files live, relative to the website-preview dir. If omitted, "../content" will be used -LOCAL_CONTENT_DIR=../docs - -from_cache=false - -if [ -d "$PREVIEW_DIR" ]; then - echo "$PREVIEW_DIR found" - CLONE_DIR="$PREVIEW_DIR-tmp" - from_cache=true -fi - -# Clone the base project, if needed -echo "⏳ Cloning the $REPO_TO_CLONE repo, this might take a while..." -git clone --depth=1 "https://github.com/hashicorp/$REPO_TO_CLONE.git" "$CLONE_DIR" - -if [ "$from_cache" = true ]; then - echo "Setting up $PREVIEW_DIR" - cp -R "./$CLONE_DIR/." "./$PREVIEW_DIR" -fi - -# cd into the preview directory project -cd "$PREVIEW_DIR" - -# Run the build:deploy-preview start script -PREVIEW_FROM_REPO=$PRODUCT \ -IS_CONTENT_PREVIEW=true \ -PREVIEW_MODE=$PREVIEW_MODE \ -REPO=$PRODUCT \ -HASHI_ENV=project-preview \ -LOCAL_CONTENT_DIR=$LOCAL_CONTENT_DIR \ -CURRENT_GIT_BRANCH=$CURRENT_GIT_BRANCH \ -npm run build:deploy-preview \ No newline at end of file diff --git a/website/scripts/website-start.sh b/website/scripts/website-start.sh deleted file mode 100644 index e7dec6b3e..000000000 --- a/website/scripts/website-start.sh +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: MPL-2.0 - -###################################################### -# NOTE: This file is managed by the Digital Team's # -# Terraform configuration @ hashicorp/mktg-terraform # -###################################################### - -# Repo which we are cloning and executing npm run build:deploy-preview within -REPO_TO_CLONE=dev-portal -# Set the subdirectory name for the dev-portal app -PREVIEW_DIR=website-preview -# The product for which we are building the deploy preview -PRODUCT=terraform-plugin-testing -# Preview mode, controls the UI rendered (either the product site or developer). Can be `io` or `developer` -PREVIEW_MODE=developer - -# Get the git branch of the commit that triggered the deploy preview -# This will power remote image assets in local and deploy previews -CURRENT_GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) - -# This is where content files live, relative to the website-preview dir. If omitted, "../content" will be used -LOCAL_CONTENT_DIR=../docs - -should_pull=true - -# Clone the dev-portal project, if needed -if [ ! -d "$PREVIEW_DIR" ]; then - echo "⏳ Cloning the $REPO_TO_CLONE repo, this might take a while..." - git clone --depth=1 https://github.com/hashicorp/$REPO_TO_CLONE.git "$PREVIEW_DIR" - should_pull=false -fi - -cd "$PREVIEW_DIR" - -# If the directory already existed, pull to ensure the clone is fresh -if [ "$should_pull" = true ]; then - git pull origin main -fi - -# Run the dev-portal content-repo start script -REPO=$PRODUCT \ -PREVIEW_FROM_REPO=$PRODUCT \ -LOCAL_CONTENT_DIR=$LOCAL_CONTENT_DIR \ -CURRENT_GIT_BRANCH=$CURRENT_GIT_BRANCH \ -PREVIEW_MODE=$PREVIEW_MODE \ -npm run start:local-preview \ No newline at end of file