From e46c64fb08f5b58fe75766d960183fb5ec2f3679 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Thu, 31 Aug 2023 07:15:12 +0100 Subject: [PATCH] Add Option to use Terraform Configuration in TestStep (#153) * Adding ConfigDirectory to TestStep (#150) * Add teststep.Config interface so that logic that examines the contents of TestStep.Config or TestStep.Directory can be encapsulated in teststep.config struct (#150) * Update TestStep.Validate() to use teststep.Config interface implementation (#150) * Switching to using a teststep.ConfigurationRequest struct for use in instantiating implementations of teststep.Config (#150) * Refactoring to use type implementing teststep.Config interface (#150) * Refactoring testStepNewImportState() to use type implementing teststep.Config interface for applied configuration (#150) * Switching to using type implementing teststep.Config interface (#150) * Moved TestStep.configHasProviderBlock(), TestStep.configHasTerraformBlock and TestStep.mergedConfig() to configuration.HasProviderBlock(), configuration.hasTerraformBlock and configuration.MergedConfig(), respectively (#150) * Fix validation error message to include ConfigDirectory (#150) * Added testCaseProviderConfig and testStepProviderConfig as fields on configuration struct to simplify processing during GetRaw() and the equivalent function(s) for processing Directory and File (#150) * Add initial implementation of copying files from ConfigDirectory into wd.baseDir (#150) * Switch to using WriteDirectory() method on configuration to make management of writing test case provider config or test step provider config simpler when handling copying of files from configuration.directory (#150) * Adding tests to verify that the ExternalProviders specified in a TestCase or TestStep are being used when TestStep.ConfigDirectory is specified (#150) * Extending implementation of HasProviderBlock() to include detection of provider blocks in configuration files within the configuration directory (#150) * Moving writing of raw config or copy of config directory files internally to configuration struct. (#150) * Removing writing of configuration files for external providers (contained within testCaseProviderConfig and testStepProvider config fields) when processing config directory as the expectation is that external providers will be specified directly in the terraform configuration files within the config directory (#150) * Adding HasConfigurationFiles() func to Config interface to be able to validate that when ConfigDirectory is defined that ExternalProviders cannot be specified for either TestCase or TestStep (#150) * Reinstating TestStep.mergedConfig() method as terraform and provider blocks are only written when using TestStep.Config. The expectation is that when using TestStep.ConfigDirectoy, the terraform files within the configuration directory will specify the terraform and/or provider blocks as necessary (#150) * Adding a couple of tests to verify behaviour when using multiple terraform configuration files in TestStep.ConfigDirectory (#150) * Switching to using TestStepConfigFunc type for TestStep.ConfigDirectory (#150) * Adding acceptance test coverage for StaticDirectory(), TestNameDirectory() and TestStepDirectory() (#150) * Adding Exec() method to TestStepConfigFunc type and removing ExecuteTestStepConfigFunc (#150) * Adding ConfigVariables to allow defining of Terraform variables within a TestStep (#150) * Adding docs for config directory and variables (#150) * Adding tests for TestStepConfigFunc.Exec() method (#150) * Adding tests for configVariable and Variables.Write() func (#150) * Adding copyright headers (#150) * Unmarshalling before comparing (#150) * Apply suggestions from code review Co-authored-by: Brian Flad * Adding TestName to TestStepConfigRequest and updating TestStepDirectory() func to use TestName (#150) * Switching to supplying t.Name() to TestStepConfigRequest (#150) * Moving all test fixtures for ConfigDirectory to resource/testdata * Marshalling Variables directly (#150) * Adding comment to explain the usage of Unwrap() (#150) * Adding FloatVariable and IntegerVariable and removing NumberVariable (#150) * Changing ConfigurationRequest fields Directory and Raw to *string to make it clear that they are optional (#150) * Adding Validate() func to ConfigurationRequest along with test coverage (#150) * Adding configurationDirectory and configurationString types (#150) * Configuration() now returns the Config interface to accommodate the different types that could be returned (e.g., configurationDirectory, configurationString) * Renaming raw.go to string.go to match type defined therein (#150) * Adding support for TestStep.ConfigFile field (#150) * Updating docs for config pkg (#150) * Adding documentation into teststep/config.go and tests for exported methods/funcs in teststep/config.go (#150) * Includes refactoring of Configuration func to remove error returned as this was always nil and the returned Config interface needs to be checked for nil in any case. * Adding checks for absolute filepath in configurationDirectory and configurationFile HasProviderBlock, HasTerraformBlock and Write methods (#150) * Adding test coverage for configurationDirectory and configurationFile HasProviderBlock method * Adding tests configurationDirectory, configurationFile and configurationString HasTerraformBlock methods (#150) * Adding test coverage for error conditions and configurationDirectory, configurationFile and configurationString Write methods (#150) * Adding empty dir for tests to git (#150) * Adding a page on usage of Terraform configuration to the website docs (#150) * Adding changelog entries (#150) * Adding test to demonstrate that the TestStep number is being used dynamically to determine the directory containing the Terraform configuration (#150) * Removing unneeded elsif conditional (#150) * Updating docs (#150) * Removing any configuration or variables files from previous test steps prior to copy configuration and variables for current test step (#150) * Updating docs (#150) * Apply suggestions from code review Co-authored-by: Brian Flad * Update .changes/unreleased/FEATURES-20230728-152822.yaml Co-authored-by: Brian Flad * Removing unneeded changelog entries (#150) * Reusing stepNumber variable (#150) --------- Co-authored-by: Brian Flad --- .../unreleased/FEATURES-20230728-143814.yaml | 6 + .../unreleased/FEATURES-20230728-152737.yaml | 6 + .../unreleased/FEATURES-20230728-152822.yaml | 6 + .../unreleased/FEATURES-20230728-152917.yaml | 6 + config/config.go | 38 + config/directory.go | 63 + config/directory_test.go | 54 + config/doc.go | 6 + config/file.go | 63 + config/file_test.go | 54 + config/variable.go | 326 ++++++ config/variable_test.go | 372 ++++++ go.mod | 3 +- go.sum | 6 +- helper/resource/testcase_validate.go | 32 +- helper/resource/testcase_validate_test.go | 38 +- .../random.tf | 19 + .../random.tf | 19 + .../provider.tf | 4 + .../random.tf | 8 + .../terraform.tf | 11 + .../provider.tf | 4 + .../random.tf | 8 + .../terraform.tf | 11 + .../vars.tf | 10 + .../random.tf | 19 + .../vars.tf | 10 + .../provider.tf | 4 + .../random.tf | 8 + .../terraform.tf | 11 + .../provider.tf | 4 + .../random.tf | 8 + .../terraform.tf | 11 + .../random.tf | 19 + .../vars.tf | 10 + .../1/random.tf | 19 + .../1/random.tf | 19 + .../1/provider.tf | 4 + .../1/random.tf | 8 + .../1/terraform.tf | 11 + .../1/provider.tf | 4 + .../1/random.tf | 8 + .../1/terraform.tf | 11 + .../1/vars.tf | 10 + .../1/random.tf | 19 + .../1/vars.tf | 10 + .../1/provider.tf | 4 + .../1/random.tf | 8 + .../1/terraform.tf | 11 + .../1/provider_1.tf | 4 + .../1/random_1.tf | 8 + .../1/terraform_1.tf | 11 + .../2/provider_2.tf | 4 + .../2/random_2.tf | 8 + .../2/terraform_2.tf | 11 + .../1/provider.tf | 4 + .../1/random.tf | 8 + .../1/terraform.tf | 11 + .../1/vars.tf | 10 + .../1/provider_1.tf | 4 + .../1/random_1.tf | 8 + .../1/terraform_1.tf | 11 + .../1/vars_1.tf | 10 + .../2/provider_2.tf | 4 + .../2/random2.tf | 8 + .../2/terraform_2.tf | 11 + .../2/vars_2.tf | 10 + .../1/random_1.tf | 19 + .../2/random_2.tf | 19 + .../1/random.tf | 19 + .../1/vars.tf | 10 + .../random.tf | 19 + .../random.tf | 19 + .../random.tf | 27 + .../random.tf | 27 + .../1/random.tf | 19 + .../1/random.tf | 19 + .../1/random.tf | 27 + .../1/random.tf | 27 + .../random.tf | 4 + .../1/random.tf | 4 + .../random.tf | 4 + .../1/random.tf | 4 + .../testdata/fixtures/random_id/random.tf | 4 + .../fixtures/random_password_3.2.0/random.tf | 19 + .../provider.tf | 4 + .../random.tf | 8 + .../terraform.tf | 11 + .../provider.tf | 4 + .../random.tf | 8 + .../terraform.tf | 11 + .../vars.tf | 10 + .../random_password_3.2.0_vars/random.tf | 19 + .../random_password_3.2.0_vars/vars.tf | 10 + .../random.tf | 27 + .../fixtures/random_password_3.5.1/random.tf | 19 + .../provider.tf | 4 + .../random.tf | 8 + .../terraform.tf | 11 + .../provider.tf | 4 + .../random.tf | 8 + .../terraform.tf | 11 + .../vars.tf | 10 + .../random_password_3.5.1_vars/random.tf | 19 + .../random_password_3.5.1_vars/vars.tf | 10 + .../random.tf | 27 + helper/resource/testing.go | 53 +- helper/resource/testing_new.go | 175 ++- helper/resource/testing_new_config.go | 61 +- helper/resource/testing_new_import_state.go | 25 +- helper/resource/teststep_providers.go | 28 +- helper/resource/teststep_providers_test.go | 1038 +++++++++++++++-- helper/resource/teststep_validate.go | 117 +- helper/resource/teststep_validate_test.go | 276 ++++- internal/plugintest/util.go | 9 +- internal/plugintest/working_dir.go | 68 +- internal/teststep/config.go | 241 ++++ internal/teststep/config_test.go | 277 +++++ internal/teststep/directory.go | 94 ++ internal/teststep/directory_test.go | 576 +++++++++ internal/teststep/file.go | 94 ++ internal/teststep/file_test.go | 504 ++++++++ internal/teststep/string.go | 61 + internal/teststep/string_test.go | 252 ++++ .../teststep/testdata/empty_dir/.gitignore | 4 + internal/teststep/testdata/empty_file/main.tf | 3 + .../main.tf | 8 + .../main.tf | 6 + .../main.tf | 8 + .../main.tf | 6 + .../testdata/provider_meta_attribute/main.tf | 6 + .../provider_object_attribute/main.tf | 10 + .../provider_string_attribute/main.tf | 8 + internal/teststep/testdata/random/random.tf | 19 + .../random_multiple_files/provider.tf | 4 + .../testdata/random_multiple_files/random.tf | 8 + .../random_multiple_files/terraform.tf | 11 + .../teststep/testdata/terraform_block/main.tf | 8 + .../testdata/terraform_meta_attribute/main.tf | 6 + .../terraform_object_attribute/main.tf | 10 + .../terraform_string_attribute/main.tf | 8 + website/data/plugin-testing-nav-data.json | 4 + .../acceptance-tests/configuration.mdx | 277 +++++ .../testing/acceptance-tests/teststep.mdx | 7 +- 144 files changed, 6262 insertions(+), 209 deletions(-) create mode 100644 .changes/unreleased/FEATURES-20230728-143814.yaml create mode 100644 .changes/unreleased/FEATURES-20230728-152737.yaml create mode 100644 .changes/unreleased/FEATURES-20230728-152822.yaml create mode 100644 .changes/unreleased/FEATURES-20230728-152917.yaml create mode 100644 config/config.go create mode 100644 config/directory.go create mode 100644 config/directory_test.go create mode 100644 config/doc.go create mode 100644 config/file.go create mode 100644 config/file_test.go create mode 100644 config/variable.go create mode 100644 config/variable_test.go create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory/random.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist/random.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_MultipleFiles/provider.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_MultipleFiles/random.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_MultipleFiles/terraform.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_MultipleFiles_Vars/provider.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_MultipleFiles_Vars/random.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_MultipleFiles_Vars/terraform.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_MultipleFiles_Vars/vars.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_Vars/random.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_AttributeDoesNotExist_Vars/vars.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_MultipleFiles/provider.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_MultipleFiles/random.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_MultipleFiles/terraform.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_MultipleFiles_Vars/provider.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_MultipleFiles_Vars/random.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_MultipleFiles_Vars/terraform.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_Vars/random.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestNameDirectory_Vars/vars.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory/1/random.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist/1/random.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_MultipleFiles/1/provider.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_MultipleFiles/1/random.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_MultipleFiles/1/terraform.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_MultipleFiles_Vars/1/provider.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_MultipleFiles_Vars/1/random.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_MultipleFiles_Vars/1/terraform.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_MultipleFiles_Vars/1/vars.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_Vars/1/random.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_AttributeDoesNotExist_Vars/1/vars.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles/1/provider.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles/1/random.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles/1/terraform.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_StepNotHardcoded/1/provider_1.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_StepNotHardcoded/1/random_1.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_StepNotHardcoded/1/terraform_1.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_StepNotHardcoded/2/provider_2.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_StepNotHardcoded/2/random_2.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_StepNotHardcoded/2/terraform_2.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars/1/provider.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars/1/random.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars/1/terraform.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars/1/vars.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars_StepNotHardcoded/1/provider_1.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars_StepNotHardcoded/1/random_1.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars_StepNotHardcoded/1/terraform_1.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars_StepNotHardcoded/1/vars_1.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars_StepNotHardcoded/2/provider_2.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars_StepNotHardcoded/2/random2.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars_StepNotHardcoded/2/terraform_2.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles_Vars_StepNotHardcoded/2/vars_2.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_StepNotHardcoded/1/random_1.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_StepNotHardcoded/2/random_2.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_Vars/1/random.tf create mode 100644 helper/resource/testdata/TestTest_ConfigDirectory_TestStepDirectory_Vars/1/vars.tf create mode 100644 helper/resource/testdata/TestTest_ConfigFile_TestNameFile/random.tf create mode 100644 helper/resource/testdata/TestTest_ConfigFile_TestNameFile_AttributeDoesNotExist/random.tf create mode 100644 helper/resource/testdata/TestTest_ConfigFile_TestNameFile_AttributeDoesNotExist_Vars/random.tf create mode 100644 helper/resource/testdata/TestTest_ConfigFile_TestNameFile_Vars/random.tf create mode 100644 helper/resource/testdata/TestTest_ConfigFile_TestStepFile/1/random.tf create mode 100644 helper/resource/testdata/TestTest_ConfigFile_TestStepFile_AttributeDoesNotExist/1/random.tf create mode 100644 helper/resource/testdata/TestTest_ConfigFile_TestStepFile_AttributeDoesNotExist_Vars/1/random.tf create mode 100644 helper/resource/testdata/TestTest_ConfigFile_TestStepFile_Vars/1/random.tf create mode 100644 helper/resource/testdata/TestTest_TestStep_ProviderFactories_ConfigDirectory_TestNameDirectory/random.tf create mode 100644 helper/resource/testdata/TestTest_TestStep_ProviderFactories_ConfigDirectory_TestStepDirectory/1/random.tf create mode 100644 helper/resource/testdata/TestTest_TestStep_ProviderFactories_ConfigFile_TestNameFile/random.tf create mode 100644 helper/resource/testdata/TestTest_TestStep_ProviderFactories_ConfigFile_TestStepFile/1/random.tf create mode 100644 helper/resource/testdata/fixtures/random_id/random.tf create mode 100644 helper/resource/testdata/fixtures/random_password_3.2.0/random.tf create mode 100644 helper/resource/testdata/fixtures/random_password_3.2.0_multiple_files/provider.tf create mode 100644 helper/resource/testdata/fixtures/random_password_3.2.0_multiple_files/random.tf create mode 100644 helper/resource/testdata/fixtures/random_password_3.2.0_multiple_files/terraform.tf create mode 100644 helper/resource/testdata/fixtures/random_password_3.2.0_multiple_files_vars/provider.tf create mode 100644 helper/resource/testdata/fixtures/random_password_3.2.0_multiple_files_vars/random.tf create mode 100644 helper/resource/testdata/fixtures/random_password_3.2.0_multiple_files_vars/terraform.tf create mode 100644 helper/resource/testdata/fixtures/random_password_3.2.0_multiple_files_vars/vars.tf create mode 100644 helper/resource/testdata/fixtures/random_password_3.2.0_vars/random.tf create mode 100644 helper/resource/testdata/fixtures/random_password_3.2.0_vars/vars.tf create mode 100644 helper/resource/testdata/fixtures/random_password_3.2.0_vars_single_file/random.tf create mode 100644 helper/resource/testdata/fixtures/random_password_3.5.1/random.tf create mode 100644 helper/resource/testdata/fixtures/random_password_3.5.1_multiple_files/provider.tf create mode 100644 helper/resource/testdata/fixtures/random_password_3.5.1_multiple_files/random.tf create mode 100644 helper/resource/testdata/fixtures/random_password_3.5.1_multiple_files/terraform.tf create mode 100644 helper/resource/testdata/fixtures/random_password_3.5.1_multiple_files_vars/provider.tf create mode 100644 helper/resource/testdata/fixtures/random_password_3.5.1_multiple_files_vars/random.tf create mode 100644 helper/resource/testdata/fixtures/random_password_3.5.1_multiple_files_vars/terraform.tf create mode 100644 helper/resource/testdata/fixtures/random_password_3.5.1_multiple_files_vars/vars.tf create mode 100644 helper/resource/testdata/fixtures/random_password_3.5.1_vars/random.tf create mode 100644 helper/resource/testdata/fixtures/random_password_3.5.1_vars/vars.tf create mode 100644 helper/resource/testdata/fixtures/random_password_3.5.1_vars_single_file/random.tf create mode 100644 internal/teststep/config.go create mode 100644 internal/teststep/config_test.go create mode 100644 internal/teststep/directory.go create mode 100644 internal/teststep/directory_test.go create mode 100644 internal/teststep/file.go create mode 100644 internal/teststep/file_test.go create mode 100644 internal/teststep/string.go create mode 100644 internal/teststep/string_test.go create mode 100644 internal/teststep/testdata/empty_dir/.gitignore create mode 100644 internal/teststep/testdata/empty_file/main.tf create mode 100644 internal/teststep/testdata/provider_block_quoted_with_attributes/main.tf create mode 100644 internal/teststep/testdata/provider_block_quoted_without_attributes/main.tf create mode 100644 internal/teststep/testdata/provider_block_unquoted_with_attributes/main.tf create mode 100644 internal/teststep/testdata/provider_block_unquoted_without_attributes/main.tf create mode 100644 internal/teststep/testdata/provider_meta_attribute/main.tf create mode 100644 internal/teststep/testdata/provider_object_attribute/main.tf create mode 100644 internal/teststep/testdata/provider_string_attribute/main.tf create mode 100644 internal/teststep/testdata/random/random.tf create mode 100644 internal/teststep/testdata/random_multiple_files/provider.tf create mode 100644 internal/teststep/testdata/random_multiple_files/random.tf create mode 100644 internal/teststep/testdata/random_multiple_files/terraform.tf create mode 100644 internal/teststep/testdata/terraform_block/main.tf create mode 100644 internal/teststep/testdata/terraform_meta_attribute/main.tf create mode 100644 internal/teststep/testdata/terraform_object_attribute/main.tf create mode 100644 internal/teststep/testdata/terraform_string_attribute/main.tf create mode 100644 website/docs/plugin/testing/acceptance-tests/configuration.mdx diff --git a/.changes/unreleased/FEATURES-20230728-143814.yaml b/.changes/unreleased/FEATURES-20230728-143814.yaml new file mode 100644 index 000000000..05b935fda --- /dev/null +++ b/.changes/unreleased/FEATURES-20230728-143814.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'config: Introduced new `config` package which contains interfaces and helper + functions for working with native Terraform configuration and variables' +time: 2023-07-28T14:38:14.006499+01:00 +custom: + Issue: "153" diff --git a/.changes/unreleased/FEATURES-20230728-152737.yaml b/.changes/unreleased/FEATURES-20230728-152737.yaml new file mode 100644 index 000000000..231f35995 --- /dev/null +++ b/.changes/unreleased/FEATURES-20230728-152737.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'helper/resource: Added `TestStep.ConfigDirectory` to allow specifying a directory + containing Terraform configuration for use during acceptance tests' +time: 2023-07-28T15:27:37.944964+01:00 +custom: + Issue: "153" diff --git a/.changes/unreleased/FEATURES-20230728-152822.yaml b/.changes/unreleased/FEATURES-20230728-152822.yaml new file mode 100644 index 000000000..d47d27ee5 --- /dev/null +++ b/.changes/unreleased/FEATURES-20230728-152822.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'helper/resource: Added `TestStep.ConfigFile` to allow specifying a file containing + Terraform configuration for use during acceptance tests' +time: 2023-07-28T15:28:22.204411+01:00 +custom: + Issue: "153" diff --git a/.changes/unreleased/FEATURES-20230728-152917.yaml b/.changes/unreleased/FEATURES-20230728-152917.yaml new file mode 100644 index 000000000..5284c9469 --- /dev/null +++ b/.changes/unreleased/FEATURES-20230728-152917.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'helper/resource: Added `TestStep.ConfigVariables` to allow specifying Terraform variables + for use with Terraform configuration during acceptance tests' +time: 2023-07-28T15:29:17.02183+01:00 +custom: + Issue: "153" 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/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..22225cc75 --- /dev/null +++ b/config/directory_test.go @@ -0,0 +1,54 @@ +// 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 { + name, testCase := name, testCase + + 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..c5cd49ac3 --- /dev/null +++ b/config/file_test.go @@ -0,0 +1,54 @@ +// 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 { + name, testCase := name, testCase + + 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..76c2a5110 --- /dev/null +++ b/config/variable.go @@ -0,0 +1,326 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package config + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "reflect" + + "golang.org/x/exp/constraints" +) + +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 constraints.Float](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 constraints.Integer](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..cdec9b7f1 --- /dev/null +++ b/config/variable_test.go @@ -0,0 +1,372 @@ +// 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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/go.mod b/go.mod index d6606b69e..db2b9687d 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 github.com/zclconf/go-cty v1.13.3 golang.org/x/crypto v0.12.0 + golang.org/x/exp v0.0.0-20230809150735-7b3493d9a819 ) require ( @@ -46,7 +47,7 @@ require ( github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - golang.org/x/mod v0.10.0 // indirect + golang.org/x/mod v0.11.0 // indirect golang.org/x/net v0.11.0 // indirect golang.org/x/sys v0.11.0 // indirect golang.org/x/text v0.12.0 // indirect diff --git a/go.sum b/go.sum index c6d69dba6..ceb1aba67 100644 --- a/go.sum +++ b/go.sum @@ -123,8 +123,10 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -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/exp v0.0.0-20230809150735-7b3493d9a819 h1:EDuYyU/MkFXllv9QF9819VlI9a4tzGuCbhG0ExK9o1U= +golang.org/x/exp v0.0.0-20230809150735-7b3493d9a819/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= +golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 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..a3aa09b59 100644 --- a/helper/resource/testcase_validate_test.go +++ b/helper/resource/testcase_validate_test.go @@ -14,6 +14,42 @@ 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 { + name, test := name, test + + 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() @@ -153,7 +189,7 @@ func TestTestCaseValidate(t *testing.T) { 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_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 4a7b7c427..2c40fc396 100644 --- a/helper/resource/testing.go +++ b/helper/resource/testing.go @@ -23,6 +23,7 @@ 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/terraform" "github.com/hashicorp/terraform-plugin-testing/tfversion" @@ -490,12 +491,60 @@ 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 + + // 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. @@ -788,7 +837,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, diff --git a/helper/resource/testing_new.go b/helper/resource/testing_new.go index e91cf6322..96ad3eec0 100644 --- a/helper/resource/testing_new.go +++ b/helper/resource/testing_new.go @@ -16,8 +16,10 @@ import ( 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/logging" "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" "github.com/hashicorp/terraform-plugin-testing/terraform" ) @@ -92,8 +94,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, @@ -120,7 +130,7 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest // 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 +139,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 +183,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 +195,55 @@ 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 + + // 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: step.providerConfig(ctx, hasProviderBlock), + TestStepConfigRequest: config.TestStepConfigRequest{ + StepNumber: stepIndex + 1, + TestName: t.Name(), + }, + }.Exec() + + testStepConfig = teststep.Configuration(confRequest) - err := wd.SetConfig(ctx, providerCfg) + err = wd.SetConfig(ctx, testStepConfig, step.ConfigVariables) if err != nil { logging.HelperResourceError(ctx, @@ -214,7 +276,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, stepIndex) if step.ExpectError != nil { logging.HelperResourceDebug(ctx, "Checking TestStep ExpectError") if err == nil { @@ -289,10 +351,10 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest continue } - if step.Config != "" { + 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) if step.ExpectError != nil { logging.HelperResourceDebug(ctx, "Checking TestStep ExpectError") @@ -326,7 +388,44 @@ 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 := step.mergedConfig(ctx, c, hasTerraformBlock, hasProviderBlock) + + confRequest := teststep.PrepareConfigurationRequest{ + Directory: step.ConfigDirectory, + File: step.ConfigFile, + Raw: mergedConfig, + TestStepConfigRequest: config.TestStepConfigRequest{ + StepNumber: stepIndex + 1, + TestName: t.Name(), + }, + }.Exec() + + appliedCfg = teststep.Configuration(confRequest) logging.HelperResourceDebug(ctx, "Finished TestStep") @@ -370,7 +469,7 @@ func planIsEmpty(plan *tfjson.Plan) bool { 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) error { t.Helper() // Build the state. The state is just the resource with an ID. There @@ -379,14 +478,64 @@ func testIDRefresh(ctx context.Context, t testing.T, c TestCase, wd *plugintest. 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) } + defer func() { - err = wd.SetConfig(ctx, step.Config) + confRequest := teststep.PrepareConfigurationRequest{ + Directory: step.ConfigDirectory, + File: step.ConfigFile, + Raw: step.providerConfig(ctx, hasProviderBlock), + 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) } diff --git a/helper/resource/testing_new_config.go b/helper/resource/testing_new_config.go index 13ed84cc4..5df394d94 100644 --- a/helper/resource/testing_new_config.go +++ b/helper/resource/testing_new_config.go @@ -11,16 +11,71 @@ import ( 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/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 { +func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugintest.WorkingDir, step TestStep, providers *providerFactories, stepIndex int) error { t.Helper() - err := wd.SetConfig(ctx, step.mergedConfig(ctx, c)) + 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 := step.mergedConfig(ctx, c, hasTerraformBlock, hasProviderBlock) + + 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 setting config: %w", err) } @@ -268,7 +323,7 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint // this fails. If refresh isn't read-only, then this will have // caught a different bug. if idRefreshCheck != nil { - if err := testIDRefresh(ctx, t, c, wd, step, idRefreshCheck, providers); err != nil { + if err := testIDRefresh(ctx, t, c, wd, step, idRefreshCheck, providers, stepIndex); err != nil { return fmt.Errorf( "[ERROR] Test: ID-only test failed: %s", err) } diff --git a/helper/resource/testing_new_import_state.go b/helper/resource/testing_new_import_state.go index c8f0ff20e..7dbc0b800 100644 --- a/helper/resource/testing_new_import_state.go +++ b/helper/resource/testing_new_import_state.go @@ -12,15 +12,29 @@ import ( "github.com/google/go-cmp/cmp" "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/internal/logging" "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" ) -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, wd *plugintest.WorkingDir, step TestStep, cfg teststep.Config, providers *providerFactories, stepIndex int) error { t.Helper() + configRequest := teststep.PrepareConfigurationRequest{ + Directory: step.ConfigDirectory, + File: step.ConfigFile, + Raw: step.Config, + TestStepConfigRequest: config.TestStepConfigRequest{ + StepNumber: stepIndex + 1, + TestName: t.Name(), + }, + }.Exec() + + testStepConfig := teststep.Configuration(configRequest) + if step.ResourceName == "" { t.Fatal("ResourceName is required for an import state test") } @@ -28,6 +42,7 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest // get state from check sequence var state *terraform.State var err error + err = runProviderCommand(ctx, t, func() error { state, err = getState(ctx, t, wd) if err != nil { @@ -79,11 +94,11 @@ 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 == "" { + if testStepConfig == nil { logging.HelperResourceTrace(ctx, "Using prior TestStep Config for import") - step.Config = cfg - if step.Config == "" { + testStepConfig = cfg + if testStepConfig == nil { t.Fatal("Cannot import state with no specified config") } } @@ -98,7 +113,7 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest defer importWd.Close() } - err = importWd.SetConfig(ctx, step.Config) + err = importWd.SetConfig(ctx, testStepConfig, step.ConfigVariables) if err != nil { t.Fatalf("Error setting test config: %s", err) } diff --git a/helper/resource/teststep_providers.go b/helper/resource/teststep_providers.go index 9b759bde0..1e2aa843e 100644 --- a/helper/resource/teststep_providers.go +++ b/helper/resource/teststep_providers.go @@ -6,24 +6,9 @@ package resource import ( "context" "fmt" - "regexp" "strings" ) -var configProviderBlockRegex = regexp.MustCompile(`provider "?[a-zA-Z0-9_-]+"? {`) - -// 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 {") -} - // mergedConfig prepends any necessary terraform configuration blocks to the // TestStep Config. // @@ -31,21 +16,26 @@ 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) string { 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() } if testCase.hasProviders(ctx) { - config.WriteString(testCase.providerConfig(ctx, s.configHasProviderBlock(ctx))) + config.WriteString(testCase.providerConfig(ctx, configHasProviderBlock)) } else { - config.WriteString(s.providerConfig(ctx, s.configHasProviderBlock(ctx))) + config.WriteString(s.providerConfig(ctx, configHasProviderBlock)) } config.WriteString(s.Config) diff --git a/helper/resource/teststep_providers_test.go b/helper/resource/teststep_providers_test.go index a2b1f5a82..d88f5d68e 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" @@ -18,133 +19,28 @@ import ( "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" ) -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) { 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{ @@ -528,6 +424,7 @@ provider "test" {} resource "test_test" "test" {} `, }, + configHasProviderBlock: true, expected: ` terraform { required_providers { @@ -560,6 +457,7 @@ provider test {} resource "test_test" "test" {} `, }, + configHasProviderBlock: true, expected: ` terraform { required_providers { @@ -599,6 +497,7 @@ terraform { resource "test_test" "test" {} `, }, + configHasTerraformBlock: true, expected: ` terraform { required_providers { @@ -761,7 +660,7 @@ resource "test_test" "test" {} t.Run(name, func(t *testing.T) { t.Parallel() - got := testCase.testStep.mergedConfig(context.Background(), testCase.testCase) + got := testCase.testStep.mergedConfig(context.Background(), testCase.testCase, testCase.configHasTerraformBlock, testCase.configHasProviderBlock) if diff := cmp.Diff(strings.TrimSpace(got), strings.TrimSpace(testCase.expected)); diff != "" { t.Errorf("unexpected difference: %s", diff) @@ -2505,6 +2404,919 @@ func TestTest_TestStep_ProviderFactories_Import_External_With_Data_Source(t *tes }) } +func TestTest_ConfigDirectory_StaticDirectory(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ + { + ConfigDirectory: config.StaticDirectory(`testdata/fixtures/random_password_3.5.1`), + Check: TestCheckResourceAttrSet("random_password.test", "id"), + }, + }, + }) +} + +func TestTest_ConfigDirectory_StaticDirectory_Vars(t *testing.T) { + t.Parallel() + + 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_ConfigDirectory_StaticDirectory_VarsMissing(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ + { + 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_ConfigDirectory_TestNameDirectory(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ + { + ConfigDirectory: config.TestNameDirectory(), + Check: TestCheckResourceAttrSet("random_password.test", "id"), + }, + }, + }) +} + +func TestTest_ConfigDirectory_TestNameDirectory_Vars(t *testing.T) { + t.Parallel() + + 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"), + }, + }, + }) +} + +func TestTest_ConfigDirectory_TestStepDirectory(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ + { + ConfigDirectory: config.TestStepDirectory(), + Check: TestCheckResourceAttrSet("random_password.test", "id"), + }, + }, + }) +} + +// 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{ + { + 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_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_ConfigDirectory_StaticDirectory_MultipleFiles(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + 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{ + { + 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{ + { + ConfigDirectory: config.TestNameDirectory(), + Check: TestCheckResourceAttrSet("random_password.test", "id"), + }, + }, + }) +} + +func TestTest_ConfigDirectory_TestNameDirectory_MultipleFiles_Vars(t *testing.T) { + t.Parallel() + + 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"), + }, + }, + }) +} + +func TestTest_ConfigDirectory_TestStepDirectory_MultipleFiles(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ + { + ConfigDirectory: config.TestStepDirectory(), + Check: TestCheckResourceAttrSet("random_password.test", "id"), + }, + }, + }) +} + +// 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() + + 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_ConfigDirectory_TestStepDirectory_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), + }, + Check: TestCheckResourceAttrSet("random_password.test", "id"), + }, + }, + }) +} + +// 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() + + 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")), + }, + }, + }) +} + +// 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(t, TestCase{ + Steps: []TestStep{ + { + ConfigDirectory: config.StaticDirectory(`testdata/fixtures/random_password_3.2.0`), + ExpectError: regexp.MustCompile(`.*An argument named "numeric" is not expected here.`), + }, + }, + }) +} + +// 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() + + 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.`), + }, + }, + }) +} + +// 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(t, TestCase{ + Steps: []TestStep{ + { + ConfigDirectory: config.TestNameDirectory(), + ExpectError: regexp.MustCompile(`.*An argument named "numeric" is not expected here.`), + }, + }, + }) +} + +// 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() + + 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.`), + }, + }, + }) +} + +// 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{ + { + 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{ + { + 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.`), + }, + }, + }) +} + +// 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{ + { + 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.`), + }, + }, + }) +} + +// 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.`), + }, + }, + }) +} + +// 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() + + 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.`), + }, + }, + }) +} + +// 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() + + Test(t, TestCase{ + Steps: []TestStep{ + { + ConfigDirectory: config.TestStepDirectory(), + ExpectError: regexp.MustCompile(`.*An argument named "numeric" is not expected here.`), + }, + }, + }) +} + +// 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{ + 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_id`), + Check: TestCheckResourceAttrSet("random_id.test", "id"), + }, + }, + }) +} + +func TestTest_TestStep_ProviderFactories_ConfigDirectory_TestNameDirectory(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_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.TestNameDirectory(), + Check: TestCheckResourceAttrSet("random_id.test", "id"), + }, + }, + }) +} + +func TestTest_TestStep_ProviderFactories_ConfigDirectory_TestStepDirectory(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_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.TestStepDirectory(), + Check: TestCheckResourceAttrSet("random_id.test", "id"), + }, + }, + }) +} + +func TestTest_ConfigFile_StaticFile(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ + { + ConfigFile: config.StaticFile(`testdata/fixtures/random_password_3.5.1/random.tf`), + Check: TestCheckResourceAttrSet("random_password.test", "id"), + }, + }, + }) +} + +func TestTest_ConfigFile_StaticFile_Vars(t *testing.T) { + t.Parallel() + + 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"), + }, + }, + }) +} + +func TestTest_ConfigFile_StaticFile_VarsMissing(t *testing.T) { + t.Parallel() + + 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_ConfigFile_TestNameFile(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ + { + ConfigFile: config.TestNameFile("random.tf"), + Check: TestCheckResourceAttrSet("random_password.test", "id"), + }, + }, + }) +} + +func TestTest_ConfigFile_TestNameFile_Vars(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ + { + ConfigFile: config.TestNameFile("random.tf"), + ConfigVariables: config.Variables{ + "length": config.IntegerVariable(8), + "numeric": config.BoolVariable(false), + }, + Check: TestCheckResourceAttrSet("random_password.test", "id"), + }, + }, + }) +} + +func TestTest_ConfigFile_TestStepFile(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ + { + ConfigFile: config.TestStepFile("random.tf"), + Check: TestCheckResourceAttrSet("random_password.test", "id"), + }, + }, + }) +} + +func TestTest_ConfigFile_TestStepFile_Vars(t *testing.T) { + t.Parallel() + + 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"), + }, + }, + }) +} + +// 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() + + 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.`), + }, + }, + }) +} + +// 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{ + 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{ + { + 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{ + { + 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{ + { + ConfigFile: config.TestStepFile("random.tf"), + ExpectError: regexp.MustCompile(`.*An argument named "numeric" is not expected here.`), + }, + }, + }) +} + +// 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() + + 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_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{ + { + ConfigFile: config.StaticFile(`testdata/fixtures/random_id/random.tf`), + Check: TestCheckResourceAttrSet("random_id.test", "id"), + }, + }, + }) +} + +func TestTest_TestStep_ProviderFactories_ConfigFile_TestNameFile(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_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{ + { + ConfigFile: config.TestNameFile("random.tf"), + Check: TestCheckResourceAttrSet("random_id.test", "id"), + }, + }, + }) +} + +func TestTest_TestStep_ProviderFactories_ConfigFile_TestStepFile(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_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{ + { + ConfigFile: config.TestStepFile("random.tf"), + Check: TestCheckResourceAttrSet("random_id.test", "id"), + }, + }, + }) +} + func setTimeForTest(t time.Time) func() { return func() { getTimeForTest = func() time.Time { diff --git a/helper/resource/teststep_validate.go b/helper/resource/teststep_validate.go index b1da44ad4..cd90ed5cc 100644 --- a/helper/resource/teststep_validate.go +++ b/helper/resource/teststep_validate.go @@ -7,41 +7,89 @@ 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 or TestStep.ConfigDirectory. + 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, + Raw: s.Config, + 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 + } + } + + if cfgHasProviders { + return true, nil } - return false + return false, nil } // validate ensures the TestStep is valid based on the following criteria: @@ -67,14 +115,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 +153,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 +179,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 +205,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 +218,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 } diff --git a/helper/resource/teststep_validate_test.go b/helper/resource/teststep_validate_test.go index cf910c576..d47200926 100644 --- a/helper/resource/teststep_validate_test.go +++ b/helper/resource/teststep_validate_test.go @@ -12,11 +12,50 @@ 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-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 { + name, test := name, test + + 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 +101,26 @@ func TestTestStepHasProviders(t *testing.T) { }, } + var stepIndex int + for name, test := range tests { name, test := name, test 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 +129,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 +188,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 +195,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, }, @@ -158,11 +284,35 @@ func TestTestStepValidate(t *testing.T) { }, "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 +320,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 +356,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 +405,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"), }, @@ -238,8 +458,28 @@ func TestTestStepValidate(t *testing.T) { 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"), }, @@ -251,7 +491,19 @@ func TestTestStepValidate(t *testing.T) { 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/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..a6e580812 100644 --- a/internal/plugintest/working_dir.go +++ b/internal/plugintest/working_dir.go @@ -5,7 +5,6 @@ package plugintest import ( "context" - "encoding/json" "fmt" "os" "path/filepath" @@ -13,13 +12,14 @@ import ( "github.com/hashicorp/terraform-exec/tfexec" tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-plugin-testing/config" "github.com/hashicorp/terraform-plugin-testing/internal/logging" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" ) const ( - ConfigFileName = "terraform_plugin_test.tf" - ConfigFileNameJSON = ConfigFileName + ".json" - PlanFileName = "tfplan" + ConfigFileName = "terraform_plugin_test.tf" + PlanFileName = "tfplan" ) // WorkingDir represents a distinct working directory that can be used for @@ -82,29 +82,71 @@ 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" { + 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 } - if err := os.Remove(rmFilename); err != nil && !os.IsNotExist(err) { - return fmt.Errorf("unable to remove %q: %w", rmFilename, 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 + } } - err := os.WriteFile(outFilename, bCfg, 0700) + + //Write configuration variables + err = vars.Write(wd.baseDir) + if err != nil { return err } - wd.configFilename = outFilename // Changing configuration invalidates any saved plan. err = wd.ClearPlan(ctx) + if err != nil { return err } + return nil } diff --git a/internal/teststep/config.go b/internal/teststep/config.go new file mode 100644 index 000000000..6bd94d3a3 --- /dev/null +++ b/internal/teststep/config.go @@ -0,0 +1,241 @@ +// 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" +) + +var ( + providerConfigBlockRegex = regexp.MustCompile(`provider "?[a-zA-Z0-9_-]+"? {`) + terraformConfigBlockRegex = regexp.MustCompile(`terraform {`) +) + +// 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 +} + +// 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) 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 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..5a5415cb7 --- /dev/null +++ b/internal/teststep/config_test.go @@ -0,0 +1,277 @@ +// 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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..0126e82aa --- /dev/null +++ b/internal/teststep/directory.go @@ -0,0 +1,94 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package teststep + +import ( + "context" + "os" + "path/filepath" +) + +var _ Config = configurationDirectory{} + +type configurationDirectory struct { + directory 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 +} + +// 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 + } + + return nil +} diff --git a/internal/teststep/directory_test.go b/internal/teststep/directory_test.go new file mode 100644 index 000000000..73247225d --- /dev/null +++ b/internal/teststep/directory_test.go @@ -0,0 +1,576 @@ +// 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-unquoted-with-attributes": { + configDirectory: configurationDirectory{ + directory: "testdata/provider_block_unquoted_with_attributes", + }, + expected: true, + }, + "provider-block-quoted-without-attributes": { + configDirectory: configurationDirectory{ + directory: "testdata/provider_block_quoted_without_attributes", + }, + expected: true, + }, + "provider-block-unquoted-without-attributes": { + configDirectory: configurationDirectory{ + directory: "testdata/provider_block_unquoted_without_attributes", + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + 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-unquoted-with-attributes": { + configDirectory: configurationDirectory{ + directory: "testdata/provider_block_unquoted_with_attributes", + }, + expected: true, + }, + "provider-block-quoted-without-attributes": { + configDirectory: configurationDirectory{ + directory: "testdata/provider_block_quoted_without_attributes", + }, + expected: true, + }, + "provider-block-unquoted-without-attributes": { + configDirectory: configurationDirectory{ + directory: "testdata/provider_block_unquoted_without_attributes", + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + 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, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + 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, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + 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{ + "testdata/empty_dir", + }, + }, + "dir-single-file": { + configDirectory: configurationDirectory{ + "testdata/random", + }, + }, + "dir-multiple-files": { + configDirectory: configurationDirectory{ + "testdata/random_multiple_files", + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + 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{ + "testdata/empty_dir", + }, + }, + "dir-single-file": { + configDirectory: configurationDirectory{ + "testdata/random", + }, + }, + "dir-multiple-files": { + configDirectory: configurationDirectory{ + "testdata/random_multiple_files", + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + 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) + } + } + } + }) + } +} + +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..6de3f0752 --- /dev/null +++ b/internal/teststep/file.go @@ -0,0 +1,94 @@ +// 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 +} + +// 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 +} + +// 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) + } + + err := copyFile(configFile, dest) + + if err != nil { + return err + } + + return nil +} diff --git a/internal/teststep/file_test.go b/internal/teststep/file_test.go new file mode 100644 index 000000000..f5ef18ddc --- /dev/null +++ b/internal/teststep/file_test.go @@ -0,0 +1,504 @@ +// 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 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-unquoted-with-attributes": { + configFile: configurationFile{ + file: "testdata/provider_block_unquoted_with_attributes/main.tf", + }, + expected: true, + }, + "provider-block-quoted-without-attributes": { + configFile: configurationFile{ + file: "testdata/provider_block_quoted_without_attributes/main.tf", + }, + expected: true, + }, + "provider-block-unquoted-without-attributes": { + configFile: configurationFile{ + file: "testdata/provider_block_unquoted_without_attributes/main.tf", + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + 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-unquoted-with-attributes": { + configFile: configurationFile{ + file: "testdata/provider_block_unquoted_with_attributes/main.tf", + }, + expected: true, + }, + "provider-block-quoted-without-attributes": { + configFile: configurationFile{ + file: "testdata/provider_block_quoted_without_attributes/main.tf", + }, + expected: true, + }, + "provider-block-unquoted-without-attributes": { + configFile: configurationFile{ + file: "testdata/provider_block_unquoted_without_attributes/main.tf", + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + 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, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + 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, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + 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{ + "testdata/random/random.tf", + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + 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{ + "testdata/random/random.tf", + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + 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) + } + } + }) + } +} diff --git a/internal/teststep/string.go b/internal/teststep/string.go new file mode 100644 index 000000000..4143b484d --- /dev/null +++ b/internal/teststep/string.go @@ -0,0 +1,61 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package teststep + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" +) + +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 +} diff --git a/internal/teststep/string_test.go b/internal/teststep/string_test.go new file mode 100644 index 000000000..c587e3df5 --- /dev/null +++ b/internal/teststep/string_test.go @@ -0,0 +1,252 @@ +// 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-unquoted-with-attributes": { + 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-unquoted-without-attributes": { + configRaw: configurationString{ + raw: ` +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, 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, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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/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_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_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_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_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_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/website/data/plugin-testing-nav-data.json b/website/data/plugin-testing-nav-data.json index 1616b0935..b65c9fecd 100644 --- a/website/data/plugin-testing-nav-data.json +++ b/website/data/plugin-testing-nav-data.json @@ -40,6 +40,10 @@ { "title": "Terraform JSON Paths", "path": "acceptance-tests/tfjson-paths" + }, + { + "title": "Terraform Configuration", + "path": "acceptance-tests/configuration" } ] }, diff --git a/website/docs/plugin/testing/acceptance-tests/configuration.mdx b/website/docs/plugin/testing/acceptance-tests/configuration.mdx new file mode 100644 index 000000000..f18c2a940 --- /dev/null +++ b/website/docs/plugin/testing/acceptance-tests/configuration.mdx @@ -0,0 +1,277 @@ +--- +page_title: 'Plugin Development - Acceptance Testing: Terraform Configuration' +description: >- + Terraform Configuration specifies the configuration to be used during an acceptance test at the TestStep level. Terraform variables define the values to be used + in conjunction with Terraform configuration. +--- + +# Terraform Configuration + +The configuration used during the execution of an acceptance test can be specified at the [TestStep](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/helper/resource#TestStep) level by populating one of the following mutually exclusive fields: + +* [TestStep.Config](#teststep-config) +* [TestStep.ConfigDirectory](#teststep-configdirectory) +* [TestStep.ConfigFile](#teststep-configfile) + +Terraform configuration can be used in conjunction with Terraform variables defined via [TestStep.ConfigVariables](/terraform/plugin/testing/acceptance-tests/configuration#teststep-configvariables). + +## TestStep Config + +The `TestStep.Config` field accepts a string containing valid Terraform configuration. + +In the following example, the `Config` field specifies a resource which is used in combination with `ExternalProviders` to specify the version and source for the provider: + +```go +func TestAccResourcePassword_UpgradeFromVersion3_2_0(t *testing.T) { + resource.Test(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + "random": { + VersionConstraint: "3.2.0", + Source: "hashicorp/random", + }, + }, + Config: `resource "random_password" "min" { + length = 12 + override_special = "!#@" + min_lower = 2 + min_upper = 3 + min_special = 1 + min_numeric = 4 + }`, + +``` + +## TestStep ConfigDirectory + +The `TestStep.ConfigDirectory` field accepts a [TestStepConfigFunc](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/config#TestStepConfigFunc) which is a function that accepts a [TestStepConfigRequest](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/config#TestStepConfigRequest) and returns a string containing a path to a directory containing Terraform configuration files. The path can be a relative or absolute path. + +There are helper methods available for generating a [TestStepConfigFunc](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/config#TestStepConfigFunc) including: + +* [StaticDirectory(directory string)](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/config#StaticDirectory) +* [TestNameDirectory()](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/config#TestNameDirectory) +* [TestStepDirectory()](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/config#TestStepDirectory) + +~> **Note**: `TestStep.ExternalProviders` cannot be specified when using ConfigDirectory. It is expected that [required_providers](/terraform/language/providers/requirements#requiring-providers) are defined within the configuration files. + +Custom functions can be written and used in the `TestStep.ConfigDirectory` field as long as the function is a [TestStepConfigFunc](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/config#TestStepConfigFunc) type. + +### StaticDirectory + +The [StaticDirectory(directory string)](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/config#StaticDirectory) function accepts a string specifying a path to a directory containing Terraform configuration. + +For example: + +```go +func Test_ConfigDirectory_StaticDirectory(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ + { + ConfigDirectory: config.StaticDirectory(`testdata/directory_containing_config`), + /* ... */ + }, + }, + }) +} +``` + +In this instance, the testing configuration is expected to be in the `testdata/directory_containing_config` directory relative to the file containing the `Test_ConfigDirectory_StaticDirectory` test. + +### TestNameDirectory + +The [TestNameDirectory()](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/config#TestNameDirectory) function will use the name of the executing test to specify a path to a directory containing Terraform configuration. + +For example: + +```go +func Test_ConfigDirectory_TestNameDirectory(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ + { + ConfigDirectory: config.TestNameDirectory(), + /* ... */ + }, + }, + }) +} +``` + +In this instance, the testing configuration is expected to be in the `testdata/Test_ConfigDirectory_TestNameDirectory` directory relative to the file containing the `Test_ConfigDirectory_TestNameDirectory` test. + +### TestStepDirectory + +The [TestStepDirectory()](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/config#TestStepDirectory) function will use the name of the executing test and the current test step number to specify a path to a directory containing Terraform configuration. + +For example: + +```go +func Test_ConfigDirectory_TestStepDirectory(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ + { + ConfigDirectory: config.TestStepDirectory(), + /* ... */ + }, + }, + }) +} +``` + +In this instance, because this is the first test step in the test, the testing configuration is expected to be in the `testdata/Test_ConfigDirectory_TestStepDirectory/1` directory relative to the file containing the `Test_ConfigDirectory_TestStepDirectory` test. + +## TestStep ConfigFile + +The `TestStep.ConfigFile` field accepts a [TestStepConfigFunc](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/config#TestStepConfigFunc) which is a function that accepts a [TestStepConfigRequest](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/config#TestStepConfigRequest) and returns a string containing a path to a file containing Terraform configuration. The path can be a relative or absolute path. + +There are helper methods available for generating a [TestStepConfigFunc](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/config#TestStepConfigFunc) including: + +* [StaticFile(file string)](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/config#StaticFile) +* [TestNameFile(file string)](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/config#TestNameFile) +* [TestStepFile(file string)](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/config#TestStepFile) + +~> **Note**: `TestStep.ExternalProviders` cannot be specified when using `ConfigFile`. It is expected that [required_providers](/terraform/language/providers/requirements#requiring-providers) are defined within the configuration file. + +Custom functions can be written and used in the `TestStep.ConfigFile` field as long as the function is a [TestStepConfigFunc](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/config#TestStepConfigFunc) type. + +### StaticFile + +The [StaticFile(file string)](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/config#Staticfile) function accepts a string specifying a path to a file containing Terraform configuration. + +For example: + +```go +func Test_ConfigFile_StaticFile(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ + { + ConfigFile: config.StaticFile(`testdata/directory_containing_config/main.tf`), + /* ... */ + }, + }, + }) +} +``` + +In this instance, the testing configuration is expected to be in the `testdata/directory_containing_config/main.tf` file relative to the file containing the `Test_ConfigFile_StaticFile` test. + +### TestNameFile + +The [TestNameFile(file string)](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/config#TestNameFile) function will use the name of the executing test to specify a path to a file containing Terraform configuration. + +For example: + +```go +func Test_ConfigFile_TestNameFile(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ + { + ConfigFile: config.TestNameFile("main.tf"), + /* ... */ + }, + }, + }) +} +``` + +In this instance, the testing configuration is expected to be in the `testdata/Test_ConfigFile_TestNameFile` directory relative to the file containing the `Test_ConfigFile_TestNameFile` test. + +### TestStepFile + +The [TestStepFile(file string)](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/config#TestStepFile) function will use the name of the executing test and the current test step number to specify a path to a file containing Terraform configuration. + +For example: + +```go +func Test_ConfigFile_TestStepFile(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ + { + ConfigFile: config.TestStepFile("main.tf"), + /* ... */ + }, + }, + }) +} +``` + +In this instance, because this is the first test step in the test, the testing configuration is expected to be in the `testdata/Test_ConfigFile_TestStepFile/1/main.tf` file relative to the file containing the `Test_ConfigDirectory_TestNameDirectory` test. + +## TestStep ConfigVariables + +[Terraform input variables](https://developer.hashicorp.com/terraform/language/values/variables) allow customization of a Terraform configuration without altering the configuration itself. + +The `TestStep.ConfigVariables` field accepts a [Variables](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/config#Variables) type which is a key-value map of string to [Variable](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/config#Variable). + +The following functions return types implementing [Variable](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/config#Variable) that correlate with the [Terraform type constraints](https://developer.hashicorp.com/terraform/language/values/variables#type-constraints): + +* [BoolVariable(value bool)](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/config#BoolVariable) +* [FloatVariable[T constraints.Float](value T)](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/config#FloatVariable) +* [IntegerVariable[T constraints.Integer](value T)](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/config#IntegerVariable) +* [func ListVariable(value ...Variable)](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/config#ListVariable) +* [MapVariable(value map[string]Variable)](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/config#MapVariable) +* [ObjectVariable(value map[string]Variable)](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/config#ObjectVariable) +* [SetVariable(value ...Variable)](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/config#SetVariable) +* [StringVariable(value string)](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/config#StringVariable) +* [TupleVariable(value ...Variable)](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/config#TupleVariable) + +The following example shows the usage of `TestStep.ConfigVariables` in conjunction with `TestStep.ConfigFile`: + +```go +func Test_ConfigFile_TestNameFile(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ + { + ConfigFile: config.TestNameFile("random.tf"), + ConfigVariables: config.Variables{ + "length": config.IntegerVariable(8), + "numeric": config.BoolVariable(false), + }, + /* ... */ + }, + }, + }) +} +``` + +The configuration would be expected to be in the `testdata/Test_ConfigFile_TestNameFile/random.tf` file, for example: + +```terraform +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/website/docs/plugin/testing/acceptance-tests/teststep.mdx b/website/docs/plugin/testing/acceptance-tests/teststep.mdx index 0c66a9ff7..5f820fa07 100644 --- a/website/docs/plugin/testing/acceptance-tests/teststep.mdx +++ b/website/docs/plugin/testing/acceptance-tests/teststep.mdx @@ -19,7 +19,12 @@ _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`. +when running `terraform apply`. Configuration is supplied by specifying +[TestStep.Config](/terraform/plugin/testing/acceptance-tests/configuration#teststep-config), +[TestStep.ConfigDirectory](/terraform/plugin/testing/acceptance-tests/configuration#teststep-configdirectory), or +[TestStep.ConfigFile](/terraform/plugin/testing/acceptance-tests/configuration#teststep-configfile). +Variables for use with configuration are defined by specifying +[TestStep.ConfigVariables](/terraform/plugin/testing/acceptance-tests/configuration#teststep-configvariables). _Import_ mode is used for testing resource functionality to import existing infrastructure into a Terraform statefile, using the same logic as would be used