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