diff --git a/CHANGELOG.md b/CHANGELOG.md index b508bf2695..5bf98acc49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ NEW FEATURES: * Added support for `override_resource`, `override_data` and `override_module` blocks in testing framework. ([#1499](https://github.com/opentofu/opentofu/pull/1499)) * Variables and Locals allowed in module sources and backend configurations (with limitations) ([#1718](https://github.com/opentofu/opentofu/pull/1718)) * Added support to new .tofu extensions to allow tofu-specific overrides of .tf files ([#1738](https://github.com/opentofu/opentofu/pull/1738)) +* Added support for `mock_provider`, `mock_resource` and `mock_data` blocks in testing framework. ([#1772](https://github.com/opentofu/opentofu/pull/1772)) ENHANCEMENTS: * Added `tofu test -json` types to website Machine-Readable UI documentation. ([#1408](https://github.com/opentofu/opentofu/issues/1408)) diff --git a/internal/command/e2etest/test_test.go b/internal/command/e2etest/test_test.go index ae81d4f474..1623a90063 100644 --- a/internal/command/e2etest/test_test.go +++ b/internal/command/e2etest/test_test.go @@ -52,8 +52,8 @@ func TestMultipleRunBlocks(t *testing.T) { } } -func TestOverrides(t *testing.T) { - // This test fetches "local" and "random" providers. +func TestMocksAndOverrides(t *testing.T) { + // This test fetches providers from registry. skipIfCannotAccessNetwork(t) tf := e2e.NewBinary(t, tofuBin, filepath.Join("testdata", "overrides-in-tests")) @@ -76,7 +76,7 @@ func TestOverrides(t *testing.T) { if stderr != "" { t.Errorf("unexpected stderr output on 'test':\n%s", stderr) } - if !strings.Contains(stdout, "11 passed, 0 failed") { + if !strings.Contains(stdout, "12 passed, 0 failed") { t.Errorf("output doesn't have expected success string:\n%s", stdout) } } diff --git a/internal/command/e2etest/testdata/overrides-in-tests/main.tf b/internal/command/e2etest/testdata/overrides-in-tests/main.tf index f8cf1847e1..61b726a86a 100644 --- a/internal/command/e2etest/testdata/overrides-in-tests/main.tf +++ b/internal/command/e2etest/testdata/overrides-in-tests/main.tf @@ -57,3 +57,28 @@ module "rand_count" { source = "./rand" } + +resource "aws_s3_bucket" "test" { + bucket = "must not be used anyway" +} + +data "aws_s3_bucket" "test" { + bucket = "must not be used anyway" +} + +provider "local" { + alias = "aliased" +} + +resource "local_file" "mocked" { + provider = local.aliased + filename = "mocked.txt" + content = "I am mocked file, do not create me please" +} + +data "local_file" "maintf" { + provider = local.aliased + filename = "main.tf" +} + +resource "random_pet" "cat" {} diff --git a/internal/command/e2etest/testdata/overrides-in-tests/main.tftest.hcl b/internal/command/e2etest/testdata/overrides-in-tests/main.tftest.hcl index 4d761f460f..e74f99b2d0 100644 --- a/internal/command/e2etest/testdata/overrides-in-tests/main.tftest.hcl +++ b/internal/command/e2etest/testdata/overrides-in-tests/main.tftest.hcl @@ -221,3 +221,67 @@ run "check_for_each_n_count_overridden" { error_message = "Mocked random integer should be 101" } } + +# ensures non-aliased provider is mocked by default +mock_provider "aws" { + mock_resource "aws_s3_bucket" { + defaults = { + arn = "arn:aws:s3:::mocked" + } + } + + mock_data "aws_s3_bucket" { + defaults = { + bucket_domain_name = "mocked.com" + } + } +} + +# ensures non-aliased provider works as intended +# and aliased one is mocked +mock_provider "local" { + alias = "aliased" +} + +# ensures we can use this provider in run's providers block +# to use mocked one only for a specific test +mock_provider "random" { + alias = "for_pets" + + mock_resource "random_pet" { + defaults = { + id = "my lovely cat" + } + } +} + +run "check_mock_providers" { + assert { + condition = resource.aws_s3_bucket.test.arn == "arn:aws:s3:::mocked" + error_message = "aws s3 bucket resource doesn't have mocked values" + } + + assert { + condition = data.aws_s3_bucket.test.bucket_domain_name == "mocked.com" + error_message = "aws s3 bucket data doesn't have mocked values" + } + + assert { + condition = !fileexists(local_file.mocked.filename) + error_message = "file should not be created due to provider being mocked" + } + + assert { + condition = data.local_file.maintf.content != file("main.tf") + error_message = "file should not be read due to provider being mocked" + } + + providers = { + random = random.for_pets + } + + assert { + condition = resource.random_pet.cat.id == "my lovely cat" + error_message = "providers block in run should allow replacing real providers by mocked" + } +} diff --git a/internal/configs/config.go b/internal/configs/config.go index 291b4f355c..a6b1a95d66 100644 --- a/internal/configs/config.go +++ b/internal/configs/config.go @@ -928,6 +928,8 @@ func (c *Config) transformProviderConfigsForTest(run *TestRun, file *TestFile) ( // 3b. If the run has no override configuration, we copy all the providers // from the test file into `next`, overriding all providers with name // collisions from the original config. + // 3c. Copy all mock providers from the test file to the `next`, overriding + // providers with name collisions from the original config. // 4. We then modify the original configuration so that the providers it // holds are the combination specified by the original config, the test // file and the run file. @@ -951,7 +953,7 @@ func (c *Config) transformProviderConfigsForTest(run *TestRun, file *TestFile) ( for _, ref := range run.Providers { - testProvider, ok := file.Providers[ref.InParent.String()] + testProvider, ok := file.getTestProviderOrMock(ref.InParent.String()) if !ok { // Then this reference was invalid as we didn't have the // specified provider in the parent. This should have been @@ -966,13 +968,15 @@ func (c *Config) transformProviderConfigsForTest(run *TestRun, file *TestFile) ( } next[ref.InChild.String()] = &Provider{ - Name: ref.InChild.Name, - NameRange: ref.InChild.NameRange, - Alias: ref.InChild.Alias, - AliasRange: ref.InChild.AliasRange, - Version: testProvider.Version, - Config: testProvider.Config, - DeclRange: testProvider.DeclRange, + Name: ref.InChild.Name, + NameRange: ref.InChild.NameRange, + Alias: ref.InChild.Alias, + AliasRange: ref.InChild.AliasRange, + Version: testProvider.Version, + Config: testProvider.Config, + DeclRange: testProvider.DeclRange, + IsMocked: testProvider.IsMocked, + MockResources: testProvider.MockResources, } } @@ -984,6 +988,18 @@ func (c *Config) transformProviderConfigsForTest(run *TestRun, file *TestFile) ( } } + for _, mp := range file.MockProviders { + next[mp.moduleUniqueKey()] = &Provider{ + Name: mp.Name, + NameRange: mp.NameRange, + Alias: mp.Alias, + AliasRange: mp.AliasRange, + DeclRange: mp.DeclRange, + IsMocked: true, + MockResources: mp.MockResources, + } + } + c.Module.ProviderConfigs = next return func() { diff --git a/internal/configs/hcl2shim/mock_value_composer.go b/internal/configs/hcl2shim/mock_value_composer.go index 743cac696b..cd27368873 100644 --- a/internal/configs/hcl2shim/mock_value_composer.go +++ b/internal/configs/hcl2shim/mock_value_composer.go @@ -1,8 +1,10 @@ package hcl2shim import ( + "cmp" "fmt" "math/rand" + "slices" "strings" "github.com/opentofu/opentofu/internal/configs/configschema" @@ -10,30 +12,27 @@ import ( "github.com/zclconf/go-cty/cty" ) -// ComposeMockValueBySchema composes mock value based on schema configuration. It uses -// configuration value as a baseline and populates null values with provided defaults. -// If the provided defaults doesn't contain needed fields, ComposeMockValueBySchema uses -// its own defaults. ComposeMockValueBySchema fails if schema contains dynamic types. -func ComposeMockValueBySchema(schema *configschema.Block, config cty.Value, defaults map[string]cty.Value) ( - cty.Value, tfdiags.Diagnostics) { - return mockValueComposer{}.composeMockValueBySchema(schema, config, defaults) +// MockValueComposer provides different ways to generate mock values based on +// config schema, attributes, blocks and cty types in general. +type MockValueComposer struct { + rand *rand.Rand } -type mockValueComposer struct { - getMockStringOverride func() string -} - -func (mvc mockValueComposer) getMockString() string { - f := getRandomAlphaNumString - - if mvc.getMockStringOverride != nil { - f = mvc.getMockStringOverride +func NewMockValueComposer(seed int64) MockValueComposer { + return MockValueComposer{ + rand: rand.New(rand.NewSource(seed)), //nolint:gosec // It doesn't need to be secure. } - - return f() } -func (mvc mockValueComposer) composeMockValueBySchema(schema *configschema.Block, config cty.Value, defaults map[string]cty.Value) (cty.Value, tfdiags.Diagnostics) { +// ComposeBySchema composes mock value based on schema configuration. It uses +// configuration value as a baseline and populates null values with provided defaults. +// If the provided defaults doesn't contain needed fields, ComposeBySchema uses +// its own defaults. ComposeBySchema fails if schema contains dynamic types. +// ComposeBySchema produces the same result with the given input values (seed and func arguments). +// It does so by traversing schema attributes, blocks and data structure elements / fields +// in a stable way by sorting keys or elements beforehand. Then, randomized values match +// between multiple ComposeBySchema calls, because seed and random sequences are the same. +func (mvc MockValueComposer) ComposeBySchema(schema *configschema.Block, config cty.Value, defaults map[string]cty.Value) (cty.Value, tfdiags.Diagnostics) { var configMap map[string]cty.Value var diags tfdiags.Diagnostics @@ -73,7 +72,7 @@ func (mvc mockValueComposer) composeMockValueBySchema(schema *configschema.Block return cty.ObjectVal(mockValues), diags } -func (mvc mockValueComposer) composeMockValueForAttributes(schema *configschema.Block, configMap map[string]cty.Value, defaults map[string]cty.Value) (map[string]cty.Value, tfdiags.Diagnostics) { +func (mvc MockValueComposer) composeMockValueForAttributes(schema *configschema.Block, configMap map[string]cty.Value, defaults map[string]cty.Value) (map[string]cty.Value, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics addPotentialDefaultsWarning := func(key, description string) { @@ -90,7 +89,10 @@ func (mvc mockValueComposer) composeMockValueForAttributes(schema *configschema. impliedTypes := schema.ImpliedType().AttributeTypes() - for k, attr := range schema.Attributes { + // Stable order is important here so random values match its fields between function calls. + for _, kv := range mapToSortedSlice(schema.Attributes) { + k, attr := kv.k, kv.v + // If the value present in configuration - just use it. if cv, ok := configMap[k]; ok && !cv.IsNull() { mockAttrs[k] = cv @@ -141,14 +143,17 @@ func (mvc mockValueComposer) composeMockValueForAttributes(schema *configschema. return mockAttrs, diags } -func (mvc mockValueComposer) composeMockValueForBlocks(schema *configschema.Block, configMap map[string]cty.Value, defaults map[string]cty.Value) (map[string]cty.Value, tfdiags.Diagnostics) { +func (mvc MockValueComposer) composeMockValueForBlocks(schema *configschema.Block, configMap map[string]cty.Value, defaults map[string]cty.Value) (map[string]cty.Value, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics mockBlocks := make(map[string]cty.Value) impliedTypes := schema.ImpliedType().AttributeTypes() - for k, block := range schema.BlockTypes { + // Stable order is important here so random values match its fields between function calls. + for _, kv := range mapToSortedSlice(schema.BlockTypes) { + k, block := kv.k, kv.v + // Checking if the config value really present for the block. // It should be non-null and non-empty collection. @@ -213,12 +218,12 @@ func (mvc mockValueComposer) composeMockValueForBlocks(schema *configschema.Bloc // to compose each value from the block's inner collection. It recursevily calls // composeMockValueBySchema to proceed with all the inner attributes and blocks // the same way so all the nested blocks follow the same logic. -func (mvc mockValueComposer) getMockValueForBlock(targetType cty.Type, configVal cty.Value, block *configschema.Block, defaults map[string]cty.Value) (cty.Value, tfdiags.Diagnostics) { +func (mvc MockValueComposer) getMockValueForBlock(targetType cty.Type, configVal cty.Value, block *configschema.Block, defaults map[string]cty.Value) (cty.Value, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics switch { case targetType.IsObjectType(): - mockBlockVal, moreDiags := mvc.composeMockValueBySchema(block, configVal, defaults) + mockBlockVal, moreDiags := mvc.ComposeBySchema(block, configVal, defaults) diags = diags.Append(moreDiags) if moreDiags.HasErrors() { return cty.NilVal, diags @@ -231,10 +236,11 @@ func (mvc mockValueComposer) getMockValueForBlock(targetType cty.Type, configVal var iterator = configVal.ElementIterator() + // Stable order is important here so random values match its fields between function calls. for iterator.Next() { _, blockConfigV := iterator.Element() - mockBlockVal, moreDiags := mvc.composeMockValueBySchema(block, blockConfigV, defaults) + mockBlockVal, moreDiags := mvc.ComposeBySchema(block, blockConfigV, defaults) diags = diags.Append(moreDiags) if moreDiags.HasErrors() { return cty.NilVal, diags @@ -254,10 +260,11 @@ func (mvc mockValueComposer) getMockValueForBlock(targetType cty.Type, configVal var iterator = configVal.ElementIterator() + // Stable order is important here so random values match its fields between function calls. for iterator.Next() { blockConfigK, blockConfigV := iterator.Element() - mockBlockVal, moreDiags := mvc.composeMockValueBySchema(block, blockConfigV, defaults) + mockBlockVal, moreDiags := mvc.ComposeBySchema(block, blockConfigV, defaults) diags = diags.Append(moreDiags) if moreDiags.HasErrors() { return cty.NilVal, diags @@ -280,7 +287,7 @@ func (mvc mockValueComposer) getMockValueForBlock(targetType cty.Type, configVal // getMockValueByType tries to generate mock cty.Value based on provided cty.Type. // It will return non-ok response if it encounters dynamic type. -func (mvc mockValueComposer) getMockValueByType(t cty.Type) (cty.Value, bool) { +func (mvc MockValueComposer) getMockValueByType(t cty.Type) (cty.Value, bool) { var v cty.Value // just to be sure for cases when the logic below misses something @@ -309,8 +316,11 @@ func (mvc mockValueComposer) getMockValueByType(t cty.Type) (cty.Value, bool) { case t.IsObjectType(): objVals := make(map[string]cty.Value) - // populate the object with mock values - for k, at := range t.AttributeTypes() { + // Populate the object with mock values. Stable order is important here + // so random values match its fields between function calls. + for _, kv := range mapToSortedSlice(t.AttributeTypes()) { + k, at := kv.k, kv.v + if t.AttributeOptional(k) { continue } @@ -335,19 +345,41 @@ func (mvc mockValueComposer) getMockValueByType(t cty.Type) (cty.Value, bool) { return v, true } -func getRandomAlphaNumString() string { +func (mvc MockValueComposer) getMockString() string { const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" const minLength, maxLength = 4, 16 - length := rand.Intn(maxLength-minLength) + minLength //nolint:gosec // It doesn't need to be secure. + length := mvc.rand.Intn(maxLength-minLength) + minLength b := strings.Builder{} b.Grow(length) for i := 0; i < length; i++ { - b.WriteByte(chars[rand.Intn(len(chars))]) //nolint:gosec // It doesn't need to be secure. + b.WriteByte(chars[mvc.rand.Intn(len(chars))]) } return b.String() } + +type keyValue[K cmp.Ordered, V any] struct { + k K + v V +} + +// mapToSortedSlice makes it possible to iterate over map in a stable manner. +func mapToSortedSlice[K cmp.Ordered, V any](m map[K]V) []keyValue[K, V] { + keys := make([]K, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + + slices.Sort(keys) + + s := make([]keyValue[K, V], 0, len(m)) + for _, k := range keys { + s = append(s, keyValue[K, V]{k, m[k]}) + } + + return s +} diff --git a/internal/configs/hcl2shim/mock_value_composer_test.go b/internal/configs/hcl2shim/mock_value_composer_test.go index 120f2e9b70..f4590506a6 100644 --- a/internal/configs/hcl2shim/mock_value_composer_test.go +++ b/internal/configs/hcl2shim/mock_value_composer_test.go @@ -1,13 +1,15 @@ package hcl2shim import ( - "strings" "testing" "github.com/opentofu/opentofu/internal/configs/configschema" "github.com/zclconf/go-cty/cty" ) +// TestComposeMockValueBySchema ensures different configschema.Block values +// processed correctly (lists, maps, objects, etc). Also, it should ensure that +// the resulting values are equal given the same set of inputs (seed, schema, etc). func TestComposeMockValueBySchema(t *testing.T) { t.Parallel() @@ -83,13 +85,13 @@ func TestComposeMockValueBySchema(t *testing.T) { config: cty.NilVal, wantVal: cty.ObjectVal(map[string]cty.Value{ "required-only": cty.NullVal(cty.String), - "required-computed": cty.StringVal("aaaaaaaa"), + "required-computed": cty.StringVal("xNmGyAVmNkB4"), "optional": cty.NullVal(cty.String), - "optional-computed": cty.StringVal("aaaaaaaa"), - "computed-only": cty.StringVal("aaaaaaaa"), + "optional-computed": cty.StringVal("6zQu0"), + "computed-only": cty.StringVal("l3INvNSQT"), "sensitive-optional": cty.NullVal(cty.String), "sensitive-required": cty.NullVal(cty.String), - "sensitive-computed": cty.StringVal("aaaaaaaa"), + "sensitive-computed": cty.StringVal("ionwj3qrsh4xyC9"), }), }, "diff-props-in-single-block-attributes": { @@ -166,13 +168,13 @@ func TestComposeMockValueBySchema(t *testing.T) { wantVal: cty.ObjectVal(map[string]cty.Value{ "nested": cty.ObjectVal(map[string]cty.Value{ "required-only": cty.NullVal(cty.String), - "required-computed": cty.StringVal("aaaaaaaa"), + "required-computed": cty.StringVal("xNmGyAVmNkB4"), "optional": cty.NullVal(cty.String), - "optional-computed": cty.StringVal("aaaaaaaa"), - "computed-only": cty.StringVal("aaaaaaaa"), + "optional-computed": cty.StringVal("6zQu0"), + "computed-only": cty.StringVal("l3INvNSQT"), "sensitive-optional": cty.NullVal(cty.String), "sensitive-required": cty.NullVal(cty.String), - "sensitive-computed": cty.StringVal("aaaaaaaa"), + "sensitive-computed": cty.StringVal("ionwj3qrsh4xyC9"), }), }), }, @@ -208,10 +210,18 @@ func TestComposeMockValueBySchema(t *testing.T) { Nesting: configschema.NestingList, Block: configschema.Block{ Attributes: map[string]*configschema.Attribute{ - "field": { + "num": { Type: cty.Number, Computed: true, }, + "str1": { + Type: cty.String, + Computed: true, + }, + "str2": { + Type: cty.String, + Computed: true, + }, }, }, }, @@ -223,7 +233,9 @@ func TestComposeMockValueBySchema(t *testing.T) { wantVal: cty.ObjectVal(map[string]cty.Value{ "nested": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ - "field": cty.NumberIntVal(0), + "num": cty.NumberIntVal(0), + "str1": cty.StringVal("l3INvNSQT"), + "str2": cty.StringVal("6zQu0"), }), }), }), @@ -235,10 +247,18 @@ func TestComposeMockValueBySchema(t *testing.T) { Nesting: configschema.NestingSet, Block: configschema.Block{ Attributes: map[string]*configschema.Attribute{ - "field": { + "num": { Type: cty.Number, Computed: true, }, + "str1": { + Type: cty.String, + Computed: true, + }, + "str2": { + Type: cty.String, + Computed: true, + }, }, }, }, @@ -250,7 +270,9 @@ func TestComposeMockValueBySchema(t *testing.T) { wantVal: cty.ObjectVal(map[string]cty.Value{ "nested": cty.SetVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ - "field": cty.NumberIntVal(0), + "num": cty.NumberIntVal(0), + "str1": cty.StringVal("l3INvNSQT"), + "str2": cty.StringVal("6zQu0"), }), }), }), @@ -262,10 +284,18 @@ func TestComposeMockValueBySchema(t *testing.T) { Nesting: configschema.NestingMap, Block: configschema.Block{ Attributes: map[string]*configschema.Attribute{ - "field": { + "num": { Type: cty.Number, Computed: true, }, + "str1": { + Type: cty.String, + Computed: true, + }, + "str2": { + Type: cty.String, + Computed: true, + }, }, }, }, @@ -279,7 +309,9 @@ func TestComposeMockValueBySchema(t *testing.T) { wantVal: cty.ObjectVal(map[string]cty.Value{ "nested": cty.MapVal(map[string]cty.Value{ "somelabel": cty.ObjectVal(map[string]cty.Value{ - "field": cty.NumberIntVal(0), + "num": cty.NumberIntVal(0), + "str1": cty.StringVal("l3INvNSQT"), + "str2": cty.StringVal("6zQu0"), }), }), }), @@ -304,8 +336,9 @@ func TestComposeMockValueBySchema(t *testing.T) { }, "obj": { Type: cty.Object(map[string]cty.Type{ - "fieldNum": cty.Number, - "fieldStr": cty.String, + "fieldNum": cty.Number, + "fieldStr1": cty.String, + "fieldStr2": cty.String, }), Computed: true, Optional: true, @@ -326,7 +359,12 @@ func TestComposeMockValueBySchema(t *testing.T) { Computed: true, Optional: true, }, - "str": { + "str1": { + Type: cty.String, + Computed: true, + Optional: true, + }, + "str2": { Type: cty.String, Computed: true, Optional: true, @@ -359,21 +397,23 @@ func TestComposeMockValueBySchema(t *testing.T) { }), wantVal: cty.ObjectVal(map[string]cty.Value{ "num": cty.NumberIntVal(0), - "str": cty.StringVal("aaaaaaaa"), + "str": cty.StringVal("xNmGyAVmNkB4"), "bool": cty.False, "obj": cty.ObjectVal(map[string]cty.Value{ - "fieldNum": cty.NumberIntVal(0), - "fieldStr": cty.StringVal("aaaaaaaa"), + "fieldNum": cty.NumberIntVal(0), + "fieldStr1": cty.StringVal("l3INvNSQT"), + "fieldStr2": cty.StringVal("6zQu0"), }), "list": cty.ListValEmpty(cty.String), "nested": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "num": cty.NumberIntVal(0), - "str": cty.StringVal("aaaaaaaa"), + "str1": cty.StringVal("mCp2gObD"), + "str2": cty.StringVal("iOtQNQsLiFD5"), "bool": cty.False, "obj": cty.ObjectVal(map[string]cty.Value{ "fieldNum": cty.NumberIntVal(0), - "fieldStr": cty.StringVal("aaaaaaaa"), + "fieldStr": cty.StringVal("ionwj3qrsh4xyC9"), }), "list": cty.ListValEmpty(cty.String), }), @@ -443,12 +483,12 @@ func TestComposeMockValueBySchema(t *testing.T) { wantVal: cty.ObjectVal(map[string]cty.Value{ "useConfigValue": cty.StringVal("iAmFromConfig"), "useDefaultsValue": cty.StringVal("iAmFromDefaults"), - "generateMockValue": cty.StringVal("aaaaaaaa"), + "generateMockValue": cty.StringVal("l3INvNSQT"), "nested": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "useConfigValue": cty.StringVal("iAmFromConfig"), "useDefaultsValue": cty.StringVal("iAmFromDefaults"), - "generateMockValue": cty.StringVal("aaaaaaaa"), + "generateMockValue": cty.StringVal("6zQu0"), }), }), }), @@ -456,21 +496,13 @@ func TestComposeMockValueBySchema(t *testing.T) { }, } - const mockStringLength = 8 - - mvc := mockValueComposer{ - getMockStringOverride: func() string { - return strings.Repeat("a", mockStringLength) - }, - } - for name, test := range tests { test := test t.Run(name, func(t *testing.T) { t.Parallel() - gotVal, gotDiags := mvc.composeMockValueBySchema(test.schema, test.config, test.defaults) + gotVal, gotDiags := NewMockValueComposer(42).ComposeBySchema(test.schema, test.config, test.defaults) switch { case test.wantError && !gotDiags.HasErrors(): t.Fatalf("Expected error in diags, but none returned") diff --git a/internal/configs/module.go b/internal/configs/module.go index e88e8f4364..7a11554dc5 100644 --- a/internal/configs/module.go +++ b/internal/configs/module.go @@ -68,6 +68,13 @@ type Module struct { StaticEvaluator *StaticEvaluator } +// GetProviderConfig uses name and alias to find the respective Provider configuration. +func (m *Module) GetProviderConfig(name, alias string) (*Provider, bool) { + tp := &Provider{Name: name, Alias: alias} + p, ok := m.ProviderConfigs[tp.moduleUniqueKey()] + return p, ok +} + // File describes the contents of a single configuration file. // // Individual files are not usually used alone, but rather combined together diff --git a/internal/configs/provider.go b/internal/configs/provider.go index a100239304..43aa12d6de 100644 --- a/internal/configs/provider.go +++ b/internal/configs/provider.go @@ -37,6 +37,11 @@ type Provider struct { // export this so providers don't need to be re-resolved. // This same field is also added to the ProviderConfigRef struct. providerType addrs.Provider + + // IsMocked indicates if this provider has been mocked. It is used in + // testing framework to instantiate test provider wrapper. + IsMocked bool + MockResources []*MockResource } func decodeProviderBlock(block *hcl.Block) (*Provider, hcl.Diagnostics) { diff --git a/internal/configs/test_file.go b/internal/configs/test_file.go index 6201fae6c4..002d476d1e 100644 --- a/internal/configs/test_file.go +++ b/internal/configs/test_file.go @@ -71,6 +71,10 @@ type TestFile struct { // Underlying modules shouldn't be called. OverrideModules []*OverrideModule + // MockProviders is a map of providers that should be mocked. It is merged + // with Providers map to use later when instantiating provider instance. + MockProviders map[string]*MockProvider + VariablesDeclRange hcl.Range } @@ -87,6 +91,30 @@ func (file *TestFile) Validate() tfdiags.Diagnostics { return diags } +func (file *TestFile) getTestProviderOrMock(addr string) (*Provider, bool) { + testProvider, ok := file.Providers[addr] + if ok { + return testProvider, true + } + + mockProvider, ok := file.MockProviders[addr] + if ok { + p := &Provider{ + Name: mockProvider.Name, + NameRange: mockProvider.NameRange, + Alias: mockProvider.Alias, + AliasRange: mockProvider.AliasRange, + DeclRange: mockProvider.DeclRange, + IsMocked: true, + MockResources: mockProvider.MockResources, + } + + return p, true + } + + return nil, false +} + // TestRun represents a single run block within a test file. // // Each run block represents a single OpenTofu command to be executed and a set @@ -254,9 +282,9 @@ func (r OverrideResource) getBlockName() string { case addrs.DataResourceMode: return blockNameOverrideData case addrs.InvalidResourceMode: - return "invalid" + panic("BUG: invalid resource mode in override resource") default: - return "invalid" + panic("BUG: undefined resource mode in override resource: " + r.Mode.String()) } } @@ -273,6 +301,60 @@ type OverrideModule struct { Outputs map[string]cty.Value } +const blockNameMockProvider = "mock_provider" + +// MockProvider represents mocked provider block. It partially matches +// the Provider configuration block (name, alias) and includes additional +// mocking data (mock resources). +type MockProvider struct { + // Fields below are copied from configs.Provider struct: + + Name string + NameRange hcl.Range + Alias string + AliasRange *hcl.Range // nil if no alias set + + DeclRange hcl.Range + + // Fields below are specific to configs.MockProvider: + + MockResources []*MockResource +} + +// moduleUniqueKey is copied from Provider.moduleUniqueKey +func (p *MockProvider) moduleUniqueKey() string { + if p.Alias != "" { + return fmt.Sprintf("%s.%s", p.Name, p.Alias) + } + return p.Name +} + +const ( + blockNameMockResource = "mock_resource" + blockNameMockData = "mock_data" +) + +// MockResource represents mocked resource. It is similar to OverrideResource, +// except all the resources with the same type should be overridden (mocked). +type MockResource struct { + Mode addrs.ResourceMode + Type string + Defaults map[string]cty.Value +} + +func (r MockResource) getBlockName() string { + switch r.Mode { + case addrs.ManagedResourceMode: + return blockNameMockResource + case addrs.DataResourceMode: + return blockNameMockData + case addrs.InvalidResourceMode: + panic("BUG: invalid resource mode in mock resource") + default: + panic("BUG: undefined resource mode in mock resource: " + r.Mode.String()) + } +} + func loadTestFile(body hcl.Body) (*TestFile, hcl.Diagnostics) { var diags hcl.Diagnostics @@ -280,7 +362,8 @@ func loadTestFile(body hcl.Body) (*TestFile, hcl.Diagnostics) { diags = append(diags, contentDiags...) tf := TestFile{ - Providers: make(map[string]*Provider), + Providers: make(map[string]*Provider), + MockProviders: make(map[string]*MockProvider), } for _, block := range content.Blocks { @@ -340,6 +423,24 @@ func loadTestFile(body hcl.Body) (*TestFile, hcl.Diagnostics) { tf.OverrideModules = append(tf.OverrideModules, overrideMod) } + case blockNameMockProvider: + mockProvider, mockProviderDiags := decodeMockProviderBlock(block) + diags = append(diags, mockProviderDiags...) + + if !mockProviderDiags.HasErrors() { + k := mockProvider.moduleUniqueKey() + + if _, ok := tf.MockProviders[k]; ok { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicated `mock_provider` block", + Detail: fmt.Sprintf("It is not allowed to have multiple `mock_provider` blocks with the same address: `%v`.", k), + Subject: mockProvider.DeclRange.Ptr(), + }) + } else { + tf.MockProviders[k] = mockProvider + } + } } } @@ -734,6 +835,108 @@ func decodeOverrideModuleBlock(block *hcl.Block) (*OverrideModule, hcl.Diagnosti return mod, diags } +// Some code of decodeMockProviderBlock function was copied from decodeProviderBlock. +func decodeMockProviderBlock(block *hcl.Block) (*MockProvider, hcl.Diagnostics) { + var diags hcl.Diagnostics + + content, moreDiags := block.Body.Content(mockProviderBlockSchema) + diags = append(diags, moreDiags...) + + // Provider names must be localized. Produce an error with a message + // indicating the action the user can take to fix this message if the local + // name is not localized. + name := block.Labels[0] + nameDiags := checkProviderNameNormalized(name, block.DefRange) + diags = append(diags, nameDiags...) + if nameDiags.HasErrors() { + // If the name is invalid then we mustn't produce a result because + // downstreams could try to use it as a provider type and then crash. + return nil, diags + } + + provider := &MockProvider{ + Name: name, + NameRange: block.LabelRanges[0], + DeclRange: block.DefRange, + } + + if attr, exists := content.Attributes["alias"]; exists { + valDiags := gohcl.DecodeExpression(attr.Expr, nil, &provider.Alias) + diags = append(diags, valDiags...) + provider.AliasRange = attr.Expr.Range().Ptr() + + if !hclsyntax.ValidIdentifier(provider.Alias) { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid mock provider configuration alias", + Detail: fmt.Sprintf("An alias must be a valid name. %s", badIdentifierDetail), + Subject: provider.AliasRange, + }) + } + } + + var ( + managedResources = make(map[string]struct{}) + dataResources = make(map[string]struct{}) + ) + + for _, block := range content.Blocks { + res, resDiags := decodeMockResourceBlock(block) + diags = append(diags, resDiags...) + if resDiags.HasErrors() { + continue + } + + resources := managedResources + if res.Mode == addrs.DataResourceMode { + resources = dataResources + } + + if _, ok := resources[res.Type]; ok { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("Duplicated `%v` block", res.getBlockName()), + Detail: fmt.Sprintf("`%v.%v` is already defined in `mock_provider` block.", res.getBlockName(), res.Type), + Subject: provider.DeclRange.Ptr(), + }) + continue + } + + resources[res.Type] = struct{}{} + + provider.MockResources = append(provider.MockResources, res) + } + + return provider, diags +} + +func decodeMockResourceBlock(block *hcl.Block) (*MockResource, hcl.Diagnostics) { + var mode addrs.ResourceMode + + switch block.Type { + case blockNameMockResource: + mode = addrs.ManagedResourceMode + case blockNameMockData: + mode = addrs.DataResourceMode + default: + panic("BUG: unsupported block type for mock resource: " + block.Type) + } + + res := &MockResource{ + Mode: mode, + Type: block.Labels[0], + } + + content, diags := block.Body.Content(mockResourceBlockSchema) + + if attr, exists := content.Attributes["defaults"]; exists { + v, moreDiags := parseObjectAttrWithNoVariables(attr) + res.Defaults, diags = v, append(diags, moreDiags...) + } + + return res, diags +} + func parseObjectAttrWithNoVariables(attr *hcl.Attribute) (map[string]cty.Value, hcl.Diagnostics) { attrVal, valDiags := attr.Expr.Value(nil) diags := valDiags @@ -821,6 +1024,10 @@ var testFileSchema = &hcl.BodySchema{ { Type: blockNameOverrideModule, }, + { + Type: blockNameMockProvider, + LabelNames: []string{"name"}, + }, }, } @@ -898,3 +1105,32 @@ var overrideModuleBlockSchema = &hcl.BodySchema{ }, }, } + +//nolint:gochecknoglobals // To follow existing code style. +var mockProviderBlockSchema = &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "alias", + Required: false, + }, + }, + Blocks: []hcl.BlockHeaderSchema{ + { + Type: blockNameMockResource, + LabelNames: []string{"type"}, + }, + { + Type: blockNameMockData, + LabelNames: []string{"type"}, + }, + }, +} + +//nolint:gochecknoglobals // To follow existing code style. +var mockResourceBlockSchema = &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "defaults", + }, + }, +} diff --git a/internal/tofu/eval_context_builtin.go b/internal/tofu/eval_context_builtin.go index 53db8bf7fb..d21159c141 100644 --- a/internal/tofu/eval_context_builtin.go +++ b/internal/tofu/eval_context_builtin.go @@ -145,6 +145,18 @@ func (ctx *BuiltinEvalContext) InitProvider(addr addrs.AbsProviderConfig) (provi return nil, err } + if ctx.Evaluator != nil && ctx.Evaluator.Config != nil && ctx.Evaluator.Config.Module != nil { + // If an aliased provider is mocked, we use providerForTest wrapper. + // We cannot wrap providers.Factory itself, because factories don't support aliases. + pc, ok := ctx.Evaluator.Config.Module.GetProviderConfig(addr.Provider.Type, addr.Alias) + if ok && pc.IsMocked { + p, err = newProviderForTest(p, pc.MockResources) + if err != nil { + return nil, err + } + } + } + log.Printf("[TRACE] BuiltinEvalContext: Initialized %q provider for %s", addr.String(), addr) ctx.ProviderCache[key] = p diff --git a/internal/tofu/node_resource_abstract_instance.go b/internal/tofu/node_resource_abstract_instance.go index 72a55854a5..918d1c731f 100644 --- a/internal/tofu/node_resource_abstract_instance.go +++ b/internal/tofu/node_resource_abstract_instance.go @@ -681,7 +681,7 @@ func (n *NodeAbstractResourceInstance) plan( var keyData instances.RepetitionData resource := n.Addr.Resource.Resource - provider, providerSchema, err := n.getProviderWithPlannedChange(ctx, n.ResolvedProvider, plannedChange) + provider, providerSchema, err := n.getProvider(ctx, n.ResolvedProvider) if err != nil { return nil, nil, keyData, diags.Append(err) } @@ -2573,10 +2573,6 @@ func resourceInstancePrevRunAddr(ctx EvalContext, currentAddr addrs.AbsResourceI } func (n *NodeAbstractResourceInstance) getProvider(ctx EvalContext, addr addrs.AbsProviderConfig) (providers.Interface, providers.ProviderSchema, error) { - return n.getProviderWithPlannedChange(ctx, addr, nil) -} - -func (n *NodeAbstractResourceInstance) getProviderWithPlannedChange(ctx EvalContext, addr addrs.AbsProviderConfig, plannedChange *plans.ResourceInstanceChange) (providers.Interface, providers.ProviderSchema, error) { underlyingProvider, schema, err := getProvider(ctx, addr) if err != nil { return nil, providers.ProviderSchema{}, err @@ -2586,15 +2582,9 @@ func (n *NodeAbstractResourceInstance) getProviderWithPlannedChange(ctx EvalCont return underlyingProvider, schema, nil } - providerForTest := providerForTest{ - internal: underlyingProvider, - schema: schema, - overrideValues: n.Config.OverrideValues, - } + providerForTest := newProviderForTestWithSchema(underlyingProvider, schema) - if plannedChange != nil { - providerForTest.plannedChange = &plannedChange.After - } + providerForTest.setSingleResource(n.Addr.Resource.Resource, n.Config.OverrideValues) return providerForTest, schema, nil } diff --git a/internal/tofu/provider_for_test_framework.go b/internal/tofu/provider_for_test_framework.go index ea7b6cbdc4..2a6b3510d5 100644 --- a/internal/tofu/provider_for_test_framework.go +++ b/internal/tofu/provider_for_test_framework.go @@ -6,126 +6,202 @@ package tofu import ( + "fmt" + "hash/fnv" + "github.com/opentofu/opentofu/internal/addrs" + "github.com/opentofu/opentofu/internal/configs" "github.com/opentofu/opentofu/internal/configs/hcl2shim" "github.com/opentofu/opentofu/internal/providers" "github.com/zclconf/go-cty/cty" ) -var _ providers.Interface = providerForTest{} +var _ providers.Interface = &providerForTest{} -// providerForTest is a wrapper around real provider to allow certain resources to be overridden -// for testing framework. Currently, it's used in NodeAbstractResourceInstance only in a format -// of one time use. It handles overrideValues and plannedChange for a single resource instance -// (i.e. by certain address). -// TODO: providerForTest should be extended to handle mock providers implementation with per-type -// mocking. It will allow providerForTest to be used for both overrides and full mocking. -// In such scenario, overrideValues should be extended to handle per-type values and plannedChange -// should contain per PlanResourceChangeRequest cache to produce the same plan result -// for the same PlanResourceChangeRequest. +// providerForTest is a wrapper around a real provider to allow certain resources to be overridden +// (by address) or mocked (by provider and resource type) for testing framework. type providerForTest struct { - // It's not embedded to make it safer to extend providers.Interface - // without silently breaking providerForTest functionality. + // providers.Interface is not embedded to make it safer to extend + // the interface without silently breaking providerForTest functionality. internal providers.Interface schema providers.ProviderSchema - overrideValues map[string]cty.Value - plannedChange *cty.Value + managedResources resourceForTestByType + dataResources resourceForTestByType } -func (p providerForTest) ReadResource(r providers.ReadResourceRequest) providers.ReadResourceResponse { +func newProviderForTestWithSchema(internal providers.Interface, schema providers.ProviderSchema) *providerForTest { + return &providerForTest{ + internal: internal, + schema: schema, + managedResources: make(resourceForTestByType), + dataResources: make(resourceForTestByType), + } +} + +func newProviderForTest(internal providers.Interface, res []*configs.MockResource) (providers.Interface, error) { + schema := internal.GetProviderSchema() + if schema.Diagnostics.HasErrors() { + return nil, fmt.Errorf("getting provider schema for test wrapper: %w", schema.Diagnostics.Err()) + } + + p := newProviderForTestWithSchema(internal, schema) + + p.addMockResources(res) + + return p, nil +} + +func (p *providerForTest) ReadResource(r providers.ReadResourceRequest) providers.ReadResourceResponse { var resp providers.ReadResourceResponse resSchema, _ := p.schema.SchemaForResourceType(addrs.ManagedResourceMode, r.TypeName) - resp.NewState, resp.Diagnostics = hcl2shim.ComposeMockValueBySchema(resSchema, r.ProviderMeta, p.overrideValues) + overrideValues := p.managedResources.getOverrideValues(r.TypeName) + + resp.NewState, resp.Diagnostics = newMockValueComposer(r.TypeName). + ComposeBySchema(resSchema, r.ProviderMeta, overrideValues) + return resp } -func (p providerForTest) PlanResourceChange(r providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { +func (p *providerForTest) PlanResourceChange(r providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { if r.Config.IsNull() { return providers.PlanResourceChangeResponse{ PlannedState: r.ProposedNewState, // null } } - if p.plannedChange != nil { - return providers.PlanResourceChangeResponse{ - PlannedState: *p.plannedChange, - } - } - resSchema, _ := p.schema.SchemaForResourceType(addrs.ManagedResourceMode, r.TypeName) + overrideValues := p.managedResources.getOverrideValues(r.TypeName) + var resp providers.PlanResourceChangeResponse - resp.PlannedState, resp.Diagnostics = hcl2shim.ComposeMockValueBySchema(resSchema, r.Config, p.overrideValues) + resp.PlannedState, resp.Diagnostics = newMockValueComposer(r.TypeName). + ComposeBySchema(resSchema, r.Config, overrideValues) return resp } -func (p providerForTest) ApplyResourceChange(r providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { +func (p *providerForTest) ApplyResourceChange(r providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { return providers.ApplyResourceChangeResponse{ NewState: r.PlannedState, } } -func (p providerForTest) ReadDataSource(r providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { +func (p *providerForTest) ReadDataSource(r providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { resSchema, _ := p.schema.SchemaForResourceType(addrs.DataResourceMode, r.TypeName) var resp providers.ReadDataSourceResponse - resp.State, resp.Diagnostics = hcl2shim.ComposeMockValueBySchema(resSchema, r.Config, p.overrideValues) + overrideValues := p.dataResources.getOverrideValues(r.TypeName) + + resp.State, resp.Diagnostics = newMockValueComposer(r.TypeName). + ComposeBySchema(resSchema, r.Config, overrideValues) return resp } // Calling the internal provider ensures providerForTest has the same behaviour as if -// it wasn't overridden. Some of these functions should be changed in the future to -// support mock_provider (e.g. ConfigureProvider should do nothing), mock_resource and -// mock_data. The only exception is ImportResourceState, which panics if called via providerForTest -// because importing is not supported in testing framework. +// it wasn't overridden or mocked. The only exception is ImportResourceState, which panics +// if called via providerForTest because importing is not supported in testing framework. -func (p providerForTest) GetProviderSchema() providers.GetProviderSchemaResponse { +func (p *providerForTest) GetProviderSchema() providers.GetProviderSchemaResponse { return p.internal.GetProviderSchema() } -func (p providerForTest) ValidateProviderConfig(r providers.ValidateProviderConfigRequest) providers.ValidateProviderConfigResponse { +func (p *providerForTest) ValidateProviderConfig(r providers.ValidateProviderConfigRequest) providers.ValidateProviderConfigResponse { return p.internal.ValidateProviderConfig(r) } -func (p providerForTest) ValidateResourceConfig(r providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse { +func (p *providerForTest) ValidateResourceConfig(r providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse { return p.internal.ValidateResourceConfig(r) } -func (p providerForTest) ValidateDataResourceConfig(r providers.ValidateDataResourceConfigRequest) providers.ValidateDataResourceConfigResponse { +func (p *providerForTest) ValidateDataResourceConfig(r providers.ValidateDataResourceConfigRequest) providers.ValidateDataResourceConfigResponse { return p.internal.ValidateDataResourceConfig(r) } -func (p providerForTest) UpgradeResourceState(r providers.UpgradeResourceStateRequest) providers.UpgradeResourceStateResponse { +func (p *providerForTest) UpgradeResourceState(r providers.UpgradeResourceStateRequest) providers.UpgradeResourceStateResponse { return p.internal.UpgradeResourceState(r) } -func (p providerForTest) ConfigureProvider(r providers.ConfigureProviderRequest) providers.ConfigureProviderResponse { - return p.internal.ConfigureProvider(r) +// providerForTest doesn't configure its internal provider because it is mocked. +func (p *providerForTest) ConfigureProvider(_ providers.ConfigureProviderRequest) providers.ConfigureProviderResponse { + return providers.ConfigureProviderResponse{} } -func (p providerForTest) Stop() error { +func (p *providerForTest) Stop() error { return p.internal.Stop() } -func (p providerForTest) GetFunctions() providers.GetFunctionsResponse { +func (p *providerForTest) GetFunctions() providers.GetFunctionsResponse { return p.internal.GetFunctions() } -func (p providerForTest) CallFunction(r providers.CallFunctionRequest) providers.CallFunctionResponse { +func (p *providerForTest) CallFunction(r providers.CallFunctionRequest) providers.CallFunctionResponse { return p.internal.CallFunction(r) } -func (p providerForTest) Close() error { +func (p *providerForTest) Close() error { return p.internal.Close() } -func (p providerForTest) ImportResourceState(providers.ImportResourceStateRequest) providers.ImportResourceStateResponse { +func (p *providerForTest) ImportResourceState(providers.ImportResourceStateRequest) providers.ImportResourceStateResponse { panic("Importing is not supported in testing context. providerForTest must not be used to call ImportResourceState") } + +func (p *providerForTest) setSingleResource(addr addrs.Resource, overrides map[string]cty.Value) { + res := resourceForTest{ + overrideValues: overrides, + } + + switch addr.Mode { + case addrs.ManagedResourceMode: + p.managedResources[addr.Type] = res + case addrs.DataResourceMode: + p.dataResources[addr.Type] = res + case addrs.InvalidResourceMode: + panic("BUG: invalid mock resource mode") + default: + panic("BUG: unsupported resource mode: " + addr.Mode.String()) + } +} + +func (p *providerForTest) addMockResources(mockResources []*configs.MockResource) { + for _, mockRes := range mockResources { + var resources resourceForTestByType + + switch mockRes.Mode { + case addrs.ManagedResourceMode: + resources = p.managedResources + case addrs.DataResourceMode: + resources = p.dataResources + case addrs.InvalidResourceMode: + panic("BUG: invalid mock resource mode") + default: + panic("BUG: unsupported mock resource mode: " + mockRes.Mode.String()) + } + + resources[mockRes.Type] = resourceForTest{ + overrideValues: mockRes.Defaults, + } + } +} + +type resourceForTest struct { + overrideValues map[string]cty.Value +} + +type resourceForTestByType map[string]resourceForTest + +func (m resourceForTestByType) getOverrideValues(typeName string) map[string]cty.Value { + return m[typeName].overrideValues +} + +func newMockValueComposer(typeName string) hcl2shim.MockValueComposer { + hash := fnv.New64() + hash.Write([]byte(typeName)) + return hcl2shim.NewMockValueComposer(int64(hash.Sum64())) +} diff --git a/website/docs/cli/commands/test/examples/mock_provider/main.tf b/website/docs/cli/commands/test/examples/mock_provider/main.tf new file mode 100644 index 0000000000..95c35083a3 --- /dev/null +++ b/website/docs/cli/commands/test/examples/mock_provider/main.tf @@ -0,0 +1,11 @@ +data "local_file" "bucket_name" { + filename = "bucket_name.txt" +} + +provider "aws" { + region = "us-east-2" +} + +resource "aws_s3_bucket" "test" { + bucket = data.local_file.bucket_name.content +} diff --git a/website/docs/cli/commands/test/examples/mock_provider/main.tftest.hcl b/website/docs/cli/commands/test/examples/mock_provider/main.tftest.hcl new file mode 100644 index 0000000000..650d8445db --- /dev/null +++ b/website/docs/cli/commands/test/examples/mock_provider/main.tftest.hcl @@ -0,0 +1,29 @@ +// All resources and data sources provided by `aws.mock` provider +// will be mocked. Their values will be automatically generated. +mock_provider "aws" { + alias = "mock" +} + +// The same goes for `local` provider. Also, every `local_file` +// data source will have its `content` set to `test`. +mock_provider "local" { + mock_data "local_file" { + defaults = { + content = "test" + } + } +} + +// Test if the bucket name is correctly passed to the aws_s3_bucket +// resource from the local file. +run "test" { + // Use `aws.mock` provider for this test run only. + providers = { + aws = aws.mock + } + + assert { + condition = aws_s3_bucket.test.bucket == "test" + error_message = "Incorrect bucket name: ${aws_s3_bucket.test.bucket}" + } +} diff --git a/website/docs/cli/commands/test/index.mdx b/website/docs/cli/commands/test/index.mdx index da2fc341fe..5296378bcc 100644 --- a/website/docs/cli/commands/test/index.mdx +++ b/website/docs/cli/commands/test/index.mdx @@ -29,6 +29,8 @@ import ExpectFailureResourcesMain from '!!raw-loader!./examples/expect_failures_ import ExpectFailureResourcesTest from '!!raw-loader!./examples/expect_failures_resources/main.tftest.hcl' import OverrideResourceMain from '!!raw-loader!./examples/override_resource/main.tf' import OverrideResourceTest from '!!raw-loader!./examples/override_resource/main.tftest.hcl' +import MockProviderMain from '!!raw-loader!./examples/mock_provider/main.tf' +import MockProviderTest from '!!raw-loader!./examples/mock_provider/main.tftest.hcl' import OverrideModuleMain from '!!raw-loader!./examples/override_module/main.tf' import OverrideModuleTest from '!!raw-loader!./examples/override_module/main.tftest.hcl' import OverrideModuleBucketMeta from '!!raw-loader!./examples/override_module/bucket_meta/main.tf' @@ -132,9 +134,10 @@ A test file consists of: * A **[`variables` block](#the-variables-and-runvariables-blocks)** (optional): define variables for all tests in the current file. * The **[`provider` blocks](#the-providers-block)** (optional): define the providers to be used for the tests. -* The **[`override_resource` block](#the-override_resource-and-override_data-blocks)** (optional): defines a resource to be overridden. -* The **[`override_data` block](#the-override_resource-and-override_data-blocks)** (optional): defines a data source to be overridden. -* The **[`override_module` block](#the-override_module-block)** (optional): defines a module call to be overridden. +* The **[`mock_provider` blocks](#the-mock-providers-blocks)** (optional): define the providers to be mocked. +* The **[`override_resource` blocks](#the-override_resource-and-override_data-blocks)** (optional): define the resources to be overridden. +* The **[`override_data` blocks](#the-override_resource-and-override_data-blocks)** (optional): define the data sources to be overridden. +* The **[`override_module` blocks](#the-override_module-block)** (optional): define the module calls to be overridden. ### The `run` block @@ -386,12 +389,46 @@ of the file. +### The `mock_provider` blocks + +A `mock_provider` block allows you to replace provider configuration by a mocked one. In such scenario, +creation and retrieval of provider resources and data sources will be skipped. Instead, OpenTofu +will automatically generate all computed attributes and blocks to be used in tests. + +:::tip Note + +Learn more on how OpenTofu produces [automatically generated values](#automatically-generated-values). + +::: + +Mock providers also support `alias` field as well as `mock_resource` and `mock_data` blocks. +In some cases, you may want to use default values instead of automatically generated ones by passing them +inside `defaults` field of `mock_resource` or `mock_data` blocks. + +In the example below, we test if the bucket name is correctly passed to the resource +without actually creating it: + + + + {MockProviderTest} + + + {MockProviderMain} + + + ### The `override_resource` and `override_data` blocks In some cases you may want to test your infrastructure with certain resources or data sources being overridden. You can use the `override_resource` or `override_data` blocks to skip creation and retrieval of these resources or data sources using the real provider. Instead, OpenTofu will automatically generate all computed attributes and blocks to be used in tests. +:::tip Note + +Learn more on how OpenTofu produces [automatically generated values](#automatically-generated-values). + +::: + These blocks consist of the following elements: | Name | Type | Description | @@ -419,9 +456,9 @@ Each instance of a resource or data source must be overridden. ::: -#### Automatically generated values +### Automatically generated values -Overriding resources and data sources requires OpenTofu to automatically generate computed attributes without calling respective providers. +Mocking resources and data sources requires OpenTofu to automatically generate computed attributes without calling respective providers. When generating these values, OpenTofu cannot follow custom provider logic, so it uses simple rules based on value type: | Attribute type | Generated value | @@ -437,7 +474,7 @@ When generating these values, OpenTofu cannot follow custom provider logic, so i :::tip Note -You can set custom values to use instead of automatically generated ones via `values` field in both `override_resource` and `override_data` blocks. +You can set custom values to use instead of automatically generated ones via respective mock or override fields. Keep in mind, it's only possible for computed attributes and configuration values cannot be changed. :::