From 6c8bfa2794c683df1043ae6222d8e31218b92126 Mon Sep 17 00:00:00 2001 From: Oleksandr Levchenkov Date: Tue, 3 Dec 2024 18:24:26 +0200 Subject: [PATCH] implement override resources for mock providers (#2168) Signed-off-by: ollevche --- CHANGELOG.md | 1 + internal/command/e2etest/test_test.go | 2 +- .../testdata/overrides-in-tests/main.tf | 13 +- .../overrides-in-tests/main.tftest.hcl | 109 ++++++-- internal/configs/config.go | 34 +-- internal/configs/provider.go | 5 +- internal/configs/test_file.go | 155 +++++++---- internal/tofu/eval_context_builtin.go | 6 +- .../tofu/node_resource_abstract_instance.go | 15 +- internal/tofu/provider_for_test_framework.go | 243 ++++++++++++------ website/docs/cli/commands/test/index.mdx | 5 +- 11 files changed, 403 insertions(+), 185 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c419ae4d2e..350a802a70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ ENHANCEMENTS: * Improved performance for large graphs with many submodules. ([#1809](https://github.com/opentofu/opentofu/pull/1809)) * Extended trace logging for HTTP backend, including request and response bodies. ([#2120](https://github.com/opentofu/opentofu/pull/2120)) * `tofu test` now throws errors instead of warnings for invalid override and mock fields. ([#2220](https://github.com/opentofu/opentofu/pull/2220)) +* `tofu test` now supports `override_resource` and `override_data` blocks in the scope of a single `mock_provider`. ([#2168](https://github.com/opentofu/opentofu/pull/2168)) BUG FIXES: * `templatefile` no longer crashes if the given filename is derived from a sensitive value. ([#1801](https://github.com/opentofu/opentofu/issues/1801)) diff --git a/internal/command/e2etest/test_test.go b/internal/command/e2etest/test_test.go index a3e2182f00..6604a77025 100644 --- a/internal/command/e2etest/test_test.go +++ b/internal/command/e2etest/test_test.go @@ -76,7 +76,7 @@ func TestMocksAndOverrides(t *testing.T) { if stderr != "" { t.Errorf("unexpected stderr output on 'test':\n%s", stderr) } - if !strings.Contains(stdout, "13 passed, 0 failed") { + if !strings.Contains(stdout, "15 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 84b6d9ff40..c9bef43a95 100644 --- a/internal/command/e2etest/testdata/overrides-in-tests/main.tf +++ b/internal/command/e2etest/testdata/overrides-in-tests/main.tf @@ -58,12 +58,17 @@ module "rand_count" { source = "./rand" } -resource "aws_s3_bucket" "test" { - bucket = "must not be used anyway" +data "http" "first" { + url = "must not be used anyway" } -data "aws_s3_bucket" "test" { - bucket = "must not be used anyway" +provider http { + alias = "aliased" +} + +data "http" "second" { + provider = http.aliased + url = "must not be used anyway" } provider "local" { 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 13ff33d530..19a53d0f76 100644 --- a/internal/command/e2etest/testdata/overrides-in-tests/main.tftest.hcl +++ b/internal/command/e2etest/testdata/overrides-in-tests/main.tftest.hcl @@ -4,10 +4,16 @@ override_module { override_resource { target = local_file.dont_create_me + values = { + file_permission = "000" + } } override_resource { target = module.first.local_file.dont_create_me + values = { + file_permission = "000" + } } run "check_root_overridden_res" { @@ -223,16 +229,10 @@ run "check_for_each_n_count_overridden" { } # ensures non-aliased provider is mocked by default -mock_provider "aws" { - mock_resource "aws_s3_bucket" { +mock_provider "http" { + mock_data "http" { defaults = { - arn = "arn:aws:s3:::mocked" - } - } - - mock_data "aws_s3_bucket" { - defaults = { - bucket_domain_name = "mocked.com" + response_body = "I am mocked!" } } } @@ -241,6 +241,12 @@ mock_provider "aws" { # and aliased one is mocked mock_provider "local" { alias = "aliased" + + mock_resource "local_file" { + defaults = { + file_permission = "000" + } + } } # ensures we can use this provider in run's providers block @@ -267,13 +273,8 @@ mock_provider "random" { 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" + condition = data.http.first.response_body == "I am mocked!" + error_message = "http data doesn't have mocked values" } assert { @@ -294,7 +295,8 @@ run "check_mock_providers" { run "check_providers_block" { providers = { - aws = aws + http = http + http.aliased = http.aliased local.aliased = local.aliased random = random.for_pets } @@ -309,3 +311,76 @@ run "check_providers_block" { error_message = "random integer should not be mocked if providers block present" } } + +# http.aliased provider is not used directly, +# but we don't want http provider to make any +# requests. +mock_provider "http" { + alias = "aliased" +} + +mock_provider "http" { + alias = "with_mock_and_override" + + mock_data "http" { + defaults = { + response_body = "mocked" + } + } + + override_data { + target = data.http.first + values = { + response_body = "overridden [first]" + } + } + + override_data { + target = data.http.second + values = { + response_body = "overridden [second]" + } + } +} + +# This test ensures override_data works +# inside a provider block and priority +# between mocks and overrides is correct. +run "check_mock_provider_override" { + providers = { + http = http.with_mock_and_override + http.aliased = http.with_mock_and_override + local.aliased = local.aliased + } + + assert { + condition = data.http.first.response_body == "overridden [first]" + error_message = "HTTP response body is not mocked" + } + + assert { + condition = data.http.second.response_body == "overridden [second]" + error_message = "HTTP response body is not overridden" + } +} + +# This test ensures override inside a mock_provider +# doesn't apply for resources created via a different +# provider. +run "check_multiple_mock_provider_override" { + providers = { + http = http.with_mock_and_override + http.aliased = http + local.aliased = local.aliased + } + + assert { + condition = data.http.first.response_body == "overridden [first]" + error_message = "HTTP response body is not mocked" + } + + assert { + condition = data.http.second.response_body == "I am mocked!" + error_message = "HTTP response body is not overridden" + } +} diff --git a/internal/configs/config.go b/internal/configs/config.go index 6a73de50c3..e190af3296 100644 --- a/internal/configs/config.go +++ b/internal/configs/config.go @@ -966,15 +966,16 @@ 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, - IsMocked: testProvider.IsMocked, - MockResources: testProvider.MockResources, + 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, + OverrideResources: testProvider.OverrideResources, } } @@ -986,13 +987,14 @@ 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, + Name: mp.Name, + NameRange: mp.NameRange, + Alias: mp.Alias, + AliasRange: mp.AliasRange, + DeclRange: mp.DeclRange, + IsMocked: true, + MockResources: mp.MockResources, + OverrideResources: mp.OverrideResources, } } } diff --git a/internal/configs/provider.go b/internal/configs/provider.go index 333a55d017..1c36119e54 100644 --- a/internal/configs/provider.go +++ b/internal/configs/provider.go @@ -43,8 +43,9 @@ type Provider struct { // IsMocked indicates if this provider has been mocked. It is used in // testing framework to instantiate test provider wrapper. - IsMocked bool - MockResources []*MockResource + IsMocked bool + MockResources []*MockResource + OverrideResources []*OverrideResource ForEach hcl.Expression Instances map[addrs.InstanceKey]instances.RepetitionData diff --git a/internal/configs/test_file.go b/internal/configs/test_file.go index cf0ec5e0b1..d1f31089ee 100644 --- a/internal/configs/test_file.go +++ b/internal/configs/test_file.go @@ -100,13 +100,14 @@ func (file *TestFile) getTestProviderOrMock(addr string) (*Provider, bool) { 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, + Name: mockProvider.Name, + NameRange: mockProvider.NameRange, + Alias: mockProvider.Alias, + AliasRange: mockProvider.AliasRange, + DeclRange: mockProvider.DeclRange, + IsMocked: true, + MockResources: mockProvider.MockResources, + OverrideResources: mockProvider.OverrideResources, } return p, true @@ -318,7 +319,8 @@ type MockProvider struct { // Fields below are specific to configs.MockProvider: - MockResources []*MockResource + MockResources []*MockResource + OverrideResources []*OverrideResource } // moduleUniqueKey is copied from Provider.moduleUniqueKey @@ -329,6 +331,58 @@ func (p *MockProvider) moduleUniqueKey() string { return p.Name } +func (p *MockProvider) validateMockResources() hcl.Diagnostics { + var diags hcl.Diagnostics + + managedResources := make(map[string]struct{}) + dataResources := make(map[string]struct{}) + + for _, res := range p.MockResources { + 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: p.DeclRange.Ptr(), + }) + continue + } + + resources[res.Type] = struct{}{} + } + + return diags +} + +func (p *MockProvider) validateOverrideResources() hcl.Diagnostics { + var diags hcl.Diagnostics + + resources := make(map[string]struct{}) + + for _, res := range p.OverrideResources { + k := res.TargetParsed.String() + + if _, ok := resources[k]; ok { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("Duplicated `%v` block", res.getBlockName()), + Detail: fmt.Sprintf("`%v` with target `%v` is already defined in `mock_provider` block.", res.getBlockName(), k), + Subject: p.DeclRange.Ptr(), + }) + continue + } + + resources[k] = struct{}{} + } + + return diags +} + const ( blockNameMockResource = "mock_resource" blockNameMockData = "mock_data" @@ -402,20 +456,13 @@ func loadTestFile(body hcl.Body) (*TestFile, hcl.Diagnostics) { tf.Providers[provider.moduleUniqueKey()] = provider } - case blockNameOverrideResource: - overrideRes, overrideResDiags := decodeOverrideResourceBlock(block, addrs.ManagedResourceMode) + case blockNameOverrideResource, blockNameOverrideData: + overrideRes, overrideResDiags := decodeOverrideResourceBlock(block) diags = append(diags, overrideResDiags...) if !overrideResDiags.HasErrors() { tf.OverrideResources = append(tf.OverrideResources, overrideRes) } - case blockNameOverrideData: - overrideData, overrideDataDiags := decodeOverrideResourceBlock(block, addrs.DataResourceMode) - diags = append(diags, overrideDataDiags...) - if !overrideDataDiags.HasErrors() { - tf.OverrideResources = append(tf.OverrideResources, overrideData) - } - case blockNameOverrideModule: overrideMod, overrideModDiags := decodeOverrideModuleBlock(block) diags = append(diags, overrideModDiags...) @@ -527,20 +574,13 @@ func decodeTestRunBlock(block *hcl.Block) (*TestRun, hcl.Diagnostics) { r.Module = module } - case blockNameOverrideResource: - overrideRes, overrideResDiags := decodeOverrideResourceBlock(block, addrs.ManagedResourceMode) + case blockNameOverrideResource, blockNameOverrideData: + overrideRes, overrideResDiags := decodeOverrideResourceBlock(block) diags = append(diags, overrideResDiags...) if !overrideResDiags.HasErrors() { r.OverrideResources = append(r.OverrideResources, overrideRes) } - case blockNameOverrideData: - overrideData, overrideDataDiags := decodeOverrideResourceBlock(block, addrs.DataResourceMode) - diags = append(diags, overrideDataDiags...) - if !overrideDataDiags.HasErrors() { - r.OverrideResources = append(r.OverrideResources, overrideData) - } - case blockNameOverrideModule: overrideMod, overrideModDiags := decodeOverrideModuleBlock(block) diags = append(diags, overrideModDiags...) @@ -763,7 +803,7 @@ func decodeTestRunOptionsBlock(block *hcl.Block) (*TestRunOptions, hcl.Diagnosti return &opts, diags } -func decodeOverrideResourceBlock(block *hcl.Block, mode addrs.ResourceMode) (*OverrideResource, hcl.Diagnostics) { +func decodeOverrideResourceBlock(block *hcl.Block) (*OverrideResource, hcl.Diagnostics) { parseTarget := func(attr *hcl.Attribute) (hcl.Traversal, *addrs.ConfigResource, hcl.Diagnostics) { traversal, traversalDiags := hcl.AbsTraversalForExpr(attr.Expr) diags := traversalDiags @@ -780,8 +820,15 @@ func decodeOverrideResourceBlock(block *hcl.Block, mode addrs.ResourceMode) (*Ov return traversal, &configRes, diags } - res := &OverrideResource{ - Mode: mode, + res := &OverrideResource{} + + switch block.Type { + case blockNameOverrideResource: + res.Mode = addrs.ManagedResourceMode + case blockNameOverrideData: + res.Mode = addrs.DataResourceMode + default: + panic("BUG: unsupported block type for override resource: " + block.Type) } content, diags := block.Body.Content(overrideResourceBlockSchema) @@ -875,38 +922,26 @@ func decodeMockProviderBlock(block *hcl.Block) (*MockProvider, hcl.Diagnostics) } } - 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 + switch block.Type { + case blockNameMockData, blockNameMockResource: + res, resDiags := decodeMockResourceBlock(block) + diags = append(diags, resDiags...) + if !resDiags.HasErrors() { + provider.MockResources = append(provider.MockResources, res) + } + case blockNameOverrideData, blockNameOverrideResource: + res, resDiags := decodeOverrideResourceBlock(block) + diags = append(diags, resDiags...) + if !resDiags.HasErrors() { + provider.OverrideResources = append(provider.OverrideResources, res) + } } - - 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) } + diags = append(diags, provider.validateMockResources()...) + diags = append(diags, provider.validateOverrideResources()...) + return provider, diags } @@ -1146,6 +1181,12 @@ var mockProviderBlockSchema = &hcl.BodySchema{ Type: blockNameMockData, LabelNames: []string{"type"}, }, + { + Type: blockNameOverrideResource, + }, + { + Type: blockNameOverrideData, + }, }, } diff --git a/internal/tofu/eval_context_builtin.go b/internal/tofu/eval_context_builtin.go index 7910b5d47d..3b747f9d9c 100644 --- a/internal/tofu/eval_context_builtin.go +++ b/internal/tofu/eval_context_builtin.go @@ -153,10 +153,14 @@ func (ctx *BuiltinEvalContext) InitProvider(addr addrs.AbsProviderConfig, provid // 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) + testP, err := newProviderForTestWithSchema(p, p.GetProviderSchema()) if err != nil { return nil, err } + + p = testP. + withMockResources(pc.MockResources). + withOverrideResources(pc.OverrideResources) } } diff --git a/internal/tofu/node_resource_abstract_instance.go b/internal/tofu/node_resource_abstract_instance.go index 37f7d44e68..afe1bf6afb 100644 --- a/internal/tofu/node_resource_abstract_instance.go +++ b/internal/tofu/node_resource_abstract_instance.go @@ -2704,12 +2704,21 @@ func (n *NodeAbstractResourceInstance) getProvider(ctx EvalContext) (providers.I } if n.Config == nil || !n.Config.IsOverridden { + if p, ok := underlyingProvider.(providerForTest); ok { + underlyingProvider = p.linkWithCurrentResource(n.Addr.ConfigResource()) + } + return underlyingProvider, schema, nil } - providerForTest := newProviderForTestWithSchema(underlyingProvider, schema) + provider, err := newProviderForTestWithSchema(underlyingProvider, schema) + if err != nil { + return nil, providers.ProviderSchema{}, err + } - providerForTest.setSingleResource(n.Addr.Resource.Resource, n.Config.OverrideValues) + provider = provider. + withOverrideResource(n.Addr.ConfigResource(), n.Config.OverrideValues). + linkWithCurrentResource(n.Addr.ConfigResource()) - return providerForTest, schema, nil + return provider, schema, nil } diff --git a/internal/tofu/provider_for_test_framework.go b/internal/tofu/provider_for_test_framework.go index a0e8f9c56c..c517461b9a 100644 --- a/internal/tofu/provider_for_test_framework.go +++ b/internal/tofu/provider_for_test_framework.go @@ -26,46 +26,52 @@ type providerForTest struct { internal providers.Interface schema providers.ProviderSchema - managedResources resourceForTestByType - dataResources resourceForTestByType + mockResources mockResourcesForTest + overrideResources overrideResourcesForTest + + currentResourceAddress string } -func newProviderForTestWithSchema(internal providers.Interface, schema providers.ProviderSchema) *providerForTest { - return &providerForTest{ - internal: internal, - schema: schema, - managedResources: make(resourceForTestByType), - dataResources: make(resourceForTestByType), +func newProviderForTestWithSchema(internal providers.Interface, schema providers.ProviderSchema) (providerForTest, error) { + if p, ok := internal.(providerForTest); ok { + // We can create a proper deep copy here, however currently + // it is only relevant for override resources, since we extend + // the override resource map in NodeAbstractResourceInstance. + return p.withCopiedOverrideResources(), nil } -} -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()) + return providerForTest{}, fmt.Errorf("invalid provider schema for test wrapper: %w", schema.Diagnostics.Err()) } - p := newProviderForTestWithSchema(internal, schema) - - p.addMockResources(res) - - return p, nil + return providerForTest{ + internal: internal, + schema: schema, + mockResources: mockResourcesForTest{ + managed: make(map[string]resourceForTest), + data: make(map[string]resourceForTest), + }, + overrideResources: overrideResourcesForTest{ + managed: make(map[string]resourceForTest), + data: make(map[string]resourceForTest), + }, + }, nil } -func (p *providerForTest) ReadResource(r providers.ReadResourceRequest) providers.ReadResourceResponse { - var resp providers.ReadResourceResponse - +func (p providerForTest) ReadResource(r providers.ReadResourceRequest) providers.ReadResourceResponse { resSchema, _ := p.schema.SchemaForResourceType(addrs.ManagedResourceMode, r.TypeName) - overrideValues := p.managedResources.getOverrideValues(r.TypeName) + mockValues := p.getMockValuesForManagedResource(r.TypeName) + + var resp providers.ReadResourceResponse resp.NewState, resp.Diagnostics = newMockValueComposer(r.TypeName). - ComposeBySchema(resSchema, r.ProviderMeta, overrideValues) + ComposeBySchema(resSchema, r.ProviderMeta, mockValues) 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 @@ -74,37 +80,37 @@ func (p *providerForTest) PlanResourceChange(r providers.PlanResourceChangeReque resSchema, _ := p.schema.SchemaForResourceType(addrs.ManagedResourceMode, r.TypeName) - overrideValues := p.managedResources.getOverrideValues(r.TypeName) + mockValues := p.getMockValuesForManagedResource(r.TypeName) var resp providers.PlanResourceChangeResponse resp.PlannedState, resp.Diagnostics = newMockValueComposer(r.TypeName). - ComposeBySchema(resSchema, r.Config, overrideValues) + ComposeBySchema(resSchema, r.Config, mockValues) 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 - overrideValues := p.dataResources.getOverrideValues(r.TypeName) + mockValues := p.getMockValuesForDataResource(r.TypeName) resp.State, resp.Diagnostics = newMockValueComposer(r.TypeName). - ComposeBySchema(resSchema, r.Config, overrideValues) + ComposeBySchema(resSchema, r.Config, mockValues) return resp } // ValidateProviderConfig is irrelevant when provider is mocked or overridden. -func (p *providerForTest) ValidateProviderConfig(_ providers.ValidateProviderConfigRequest) providers.ValidateProviderConfigResponse { +func (p providerForTest) ValidateProviderConfig(_ providers.ValidateProviderConfigRequest) providers.ValidateProviderConfigResponse { return providers.ValidateProviderConfigResponse{} } @@ -114,7 +120,7 @@ func (p *providerForTest) ValidateProviderConfig(_ providers.ValidateProviderCon // is being transformed for testing framework and original provider configuration is not // accessible so it is safe to wipe metadata as well. See Config.transformProviderConfigsForTest // for more details. -func (p *providerForTest) GetProviderSchema() providers.GetProviderSchemaResponse { +func (p providerForTest) GetProviderSchema() providers.GetProviderSchemaResponse { providerSchema := p.internal.GetProviderSchema() providerSchema.Provider = providers.Schema{} providerSchema.ProviderMeta = providers.Schema{} @@ -122,92 +128,163 @@ func (p *providerForTest) GetProviderSchema() providers.GetProviderSchemaRespons } // providerForTest doesn't configure its internal provider because it is mocked. -func (p *providerForTest) ConfigureProvider(_ providers.ConfigureProviderRequest) providers.ConfigureProviderResponse { +func (p providerForTest) ConfigureProvider(_ providers.ConfigureProviderRequest) providers.ConfigureProviderResponse { return providers.ConfigureProviderResponse{} } -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, - } - } -} - // Calling the internal provider ensures providerForTest has the same behaviour as if // 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) 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) 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() } -type resourceForTest struct { - overrideValues map[string]cty.Value +func (p providerForTest) withMockResources(mockResources []*configs.MockResource) providerForTest { + for _, res := range mockResources { + var resources map[mockResourceType]resourceForTest + + switch res.Mode { + case addrs.ManagedResourceMode: + resources = p.mockResources.managed + case addrs.DataResourceMode: + resources = p.mockResources.data + case addrs.InvalidResourceMode: + panic("BUG: invalid mock resource mode") + default: + panic("BUG: unsupported mock resource mode: " + res.Mode.String()) + } + + resources[res.Type] = resourceForTest{ + values: res.Defaults, + } + } + + return p } -type resourceForTestByType map[string]resourceForTest +func (p providerForTest) withCopiedOverrideResources() providerForTest { + p.overrideResources = p.overrideResources.copy() + return p +} -func (m resourceForTestByType) getOverrideValues(typeName string) map[string]cty.Value { - return m[typeName].overrideValues +func (p providerForTest) withOverrideResources(overrideResources []*configs.OverrideResource) providerForTest { + for _, res := range overrideResources { + p = p.withOverrideResource(*res.TargetParsed, res.Values) + } + + return p +} + +func (p providerForTest) withOverrideResource(addr addrs.ConfigResource, overrides map[string]cty.Value) providerForTest { + var resources map[string]resourceForTest + + switch addr.Resource.Mode { + case addrs.ManagedResourceMode: + resources = p.overrideResources.managed + case addrs.DataResourceMode: + resources = p.overrideResources.data + case addrs.InvalidResourceMode: + panic("BUG: invalid override resource mode") + default: + panic("BUG: unsupported override resource mode: " + addr.Resource.Mode.String()) + } + + resources[addr.String()] = resourceForTest{ + values: overrides, + } + + return p +} + +func (p providerForTest) linkWithCurrentResource(addr addrs.ConfigResource) providerForTest { + p.currentResourceAddress = addr.String() + return p +} + +type resourceForTest struct { + values map[string]cty.Value +} + +type mockResourceType = string + +type mockResourcesForTest struct { + managed map[mockResourceType]resourceForTest + data map[mockResourceType]resourceForTest +} + +type overrideResourceAddress = string + +type overrideResourcesForTest struct { + managed map[overrideResourceAddress]resourceForTest + data map[overrideResourceAddress]resourceForTest +} + +func (res overrideResourcesForTest) copy() overrideResourcesForTest { + resCopy := overrideResourcesForTest{ + managed: make(map[overrideResourceAddress]resourceForTest, len(res.managed)), + data: make(map[overrideResourceAddress]resourceForTest, len(res.data)), + } + + for k, v := range res.managed { + resCopy.managed[k] = v + } + + for k, v := range res.data { + resCopy.data[k] = v + } + + return resCopy +} + +func (p providerForTest) getMockValuesForManagedResource(typeName string) map[string]cty.Value { + if p.currentResourceAddress != "" { + res, ok := p.overrideResources.managed[p.currentResourceAddress] + if ok { + return res.values + } + } + + return p.mockResources.managed[typeName].values +} + +func (p providerForTest) getMockValuesForDataResource(typeName string) map[string]cty.Value { + if p.currentResourceAddress != "" { + res, ok := p.overrideResources.data[p.currentResourceAddress] + if ok { + return res.values + } + } + + return p.mockResources.data[typeName].values } func newMockValueComposer(typeName string) hcl2shim.MockValueComposer { diff --git a/website/docs/cli/commands/test/index.mdx b/website/docs/cli/commands/test/index.mdx index 667cd5af84..65800e5188 100644 --- a/website/docs/cli/commands/test/index.mdx +++ b/website/docs/cli/commands/test/index.mdx @@ -158,7 +158,7 @@ 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 **[`mock_provider` blocks](#the-mock_providers-blocks)** (optional): define the providers to be mocked. +* The **[`mock_provider` blocks](#the-mock_provider-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. @@ -429,6 +429,9 @@ Mock providers also support `alias` field as well as `mock_resource` and `mock_d 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. +Additionally, you can use `override_resource` and `override_data` blocks to override resources or data +sources in the scope of a single provider. Read more about overriding in [the next section](#the-override_resource-and-override_data-blocks). + In the example below, we test if the bucket name is correctly passed to the resource without actually creating it: