implement override resources for mock providers (#2168)

Signed-off-by: ollevche <ollevche@gmail.com>
This commit is contained in:
Oleksandr Levchenkov 2024-12-03 18:24:26 +02:00 committed by GitHub
parent cb866bf503
commit 6c8bfa2794
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 403 additions and 185 deletions

View File

@ -42,6 +42,7 @@ ENHANCEMENTS:
* Improved performance for large graphs with many submodules. ([#1809](https://github.com/opentofu/opentofu/pull/1809)) * 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)) * 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 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: BUG FIXES:
* `templatefile` no longer crashes if the given filename is derived from a sensitive value. ([#1801](https://github.com/opentofu/opentofu/issues/1801)) * `templatefile` no longer crashes if the given filename is derived from a sensitive value. ([#1801](https://github.com/opentofu/opentofu/issues/1801))

View File

@ -76,7 +76,7 @@ func TestMocksAndOverrides(t *testing.T) {
if stderr != "" { if stderr != "" {
t.Errorf("unexpected stderr output on 'test':\n%s", 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) t.Errorf("output doesn't have expected success string:\n%s", stdout)
} }
} }

View File

@ -58,12 +58,17 @@ module "rand_count" {
source = "./rand" source = "./rand"
} }
resource "aws_s3_bucket" "test" { data "http" "first" {
bucket = "must not be used anyway" url = "must not be used anyway"
} }
data "aws_s3_bucket" "test" { provider http {
bucket = "must not be used anyway" alias = "aliased"
}
data "http" "second" {
provider = http.aliased
url = "must not be used anyway"
} }
provider "local" { provider "local" {

View File

@ -4,10 +4,16 @@ override_module {
override_resource { override_resource {
target = local_file.dont_create_me target = local_file.dont_create_me
values = {
file_permission = "000"
}
} }
override_resource { override_resource {
target = module.first.local_file.dont_create_me target = module.first.local_file.dont_create_me
values = {
file_permission = "000"
}
} }
run "check_root_overridden_res" { run "check_root_overridden_res" {
@ -223,16 +229,10 @@ run "check_for_each_n_count_overridden" {
} }
# ensures non-aliased provider is mocked by default # ensures non-aliased provider is mocked by default
mock_provider "aws" { mock_provider "http" {
mock_resource "aws_s3_bucket" { mock_data "http" {
defaults = { defaults = {
arn = "arn:aws:s3:::mocked" response_body = "I am mocked!"
}
}
mock_data "aws_s3_bucket" {
defaults = {
bucket_domain_name = "mocked.com"
} }
} }
} }
@ -241,6 +241,12 @@ mock_provider "aws" {
# and aliased one is mocked # and aliased one is mocked
mock_provider "local" { mock_provider "local" {
alias = "aliased" alias = "aliased"
mock_resource "local_file" {
defaults = {
file_permission = "000"
}
}
} }
# ensures we can use this provider in run's providers block # ensures we can use this provider in run's providers block
@ -267,13 +273,8 @@ mock_provider "random" {
run "check_mock_providers" { run "check_mock_providers" {
assert { assert {
condition = resource.aws_s3_bucket.test.arn == "arn:aws:s3:::mocked" condition = data.http.first.response_body == "I am mocked!"
error_message = "aws s3 bucket resource doesn't have mocked values" error_message = "http data 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 { assert {
@ -294,7 +295,8 @@ run "check_mock_providers" {
run "check_providers_block" { run "check_providers_block" {
providers = { providers = {
aws = aws http = http
http.aliased = http.aliased
local.aliased = local.aliased local.aliased = local.aliased
random = random.for_pets random = random.for_pets
} }
@ -309,3 +311,76 @@ run "check_providers_block" {
error_message = "random integer should not be mocked if providers block present" 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"
}
}

View File

@ -975,6 +975,7 @@ func (c *Config) transformProviderConfigsForTest(run *TestRun, file *TestFile) (
DeclRange: testProvider.DeclRange, DeclRange: testProvider.DeclRange,
IsMocked: testProvider.IsMocked, IsMocked: testProvider.IsMocked,
MockResources: testProvider.MockResources, MockResources: testProvider.MockResources,
OverrideResources: testProvider.OverrideResources,
} }
} }
@ -993,6 +994,7 @@ func (c *Config) transformProviderConfigsForTest(run *TestRun, file *TestFile) (
DeclRange: mp.DeclRange, DeclRange: mp.DeclRange,
IsMocked: true, IsMocked: true,
MockResources: mp.MockResources, MockResources: mp.MockResources,
OverrideResources: mp.OverrideResources,
} }
} }
} }

View File

@ -45,6 +45,7 @@ type Provider struct {
// testing framework to instantiate test provider wrapper. // testing framework to instantiate test provider wrapper.
IsMocked bool IsMocked bool
MockResources []*MockResource MockResources []*MockResource
OverrideResources []*OverrideResource
ForEach hcl.Expression ForEach hcl.Expression
Instances map[addrs.InstanceKey]instances.RepetitionData Instances map[addrs.InstanceKey]instances.RepetitionData

View File

@ -107,6 +107,7 @@ func (file *TestFile) getTestProviderOrMock(addr string) (*Provider, bool) {
DeclRange: mockProvider.DeclRange, DeclRange: mockProvider.DeclRange,
IsMocked: true, IsMocked: true,
MockResources: mockProvider.MockResources, MockResources: mockProvider.MockResources,
OverrideResources: mockProvider.OverrideResources,
} }
return p, true return p, true
@ -319,6 +320,7 @@ type MockProvider struct {
// Fields below are specific to configs.MockProvider: // Fields below are specific to configs.MockProvider:
MockResources []*MockResource MockResources []*MockResource
OverrideResources []*OverrideResource
} }
// moduleUniqueKey is copied from Provider.moduleUniqueKey // moduleUniqueKey is copied from Provider.moduleUniqueKey
@ -329,6 +331,58 @@ func (p *MockProvider) moduleUniqueKey() string {
return p.Name 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 ( const (
blockNameMockResource = "mock_resource" blockNameMockResource = "mock_resource"
blockNameMockData = "mock_data" blockNameMockData = "mock_data"
@ -402,20 +456,13 @@ func loadTestFile(body hcl.Body) (*TestFile, hcl.Diagnostics) {
tf.Providers[provider.moduleUniqueKey()] = provider tf.Providers[provider.moduleUniqueKey()] = provider
} }
case blockNameOverrideResource: case blockNameOverrideResource, blockNameOverrideData:
overrideRes, overrideResDiags := decodeOverrideResourceBlock(block, addrs.ManagedResourceMode) overrideRes, overrideResDiags := decodeOverrideResourceBlock(block)
diags = append(diags, overrideResDiags...) diags = append(diags, overrideResDiags...)
if !overrideResDiags.HasErrors() { if !overrideResDiags.HasErrors() {
tf.OverrideResources = append(tf.OverrideResources, overrideRes) 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: case blockNameOverrideModule:
overrideMod, overrideModDiags := decodeOverrideModuleBlock(block) overrideMod, overrideModDiags := decodeOverrideModuleBlock(block)
diags = append(diags, overrideModDiags...) diags = append(diags, overrideModDiags...)
@ -527,20 +574,13 @@ func decodeTestRunBlock(block *hcl.Block) (*TestRun, hcl.Diagnostics) {
r.Module = module r.Module = module
} }
case blockNameOverrideResource: case blockNameOverrideResource, blockNameOverrideData:
overrideRes, overrideResDiags := decodeOverrideResourceBlock(block, addrs.ManagedResourceMode) overrideRes, overrideResDiags := decodeOverrideResourceBlock(block)
diags = append(diags, overrideResDiags...) diags = append(diags, overrideResDiags...)
if !overrideResDiags.HasErrors() { if !overrideResDiags.HasErrors() {
r.OverrideResources = append(r.OverrideResources, overrideRes) 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: case blockNameOverrideModule:
overrideMod, overrideModDiags := decodeOverrideModuleBlock(block) overrideMod, overrideModDiags := decodeOverrideModuleBlock(block)
diags = append(diags, overrideModDiags...) diags = append(diags, overrideModDiags...)
@ -763,7 +803,7 @@ func decodeTestRunOptionsBlock(block *hcl.Block) (*TestRunOptions, hcl.Diagnosti
return &opts, diags 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) { parseTarget := func(attr *hcl.Attribute) (hcl.Traversal, *addrs.ConfigResource, hcl.Diagnostics) {
traversal, traversalDiags := hcl.AbsTraversalForExpr(attr.Expr) traversal, traversalDiags := hcl.AbsTraversalForExpr(attr.Expr)
diags := traversalDiags diags := traversalDiags
@ -780,8 +820,15 @@ func decodeOverrideResourceBlock(block *hcl.Block, mode addrs.ResourceMode) (*Ov
return traversal, &configRes, diags return traversal, &configRes, diags
} }
res := &OverrideResource{ res := &OverrideResource{}
Mode: mode,
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) content, diags := block.Body.Content(overrideResourceBlockSchema)
@ -875,37 +922,25 @@ func decodeMockProviderBlock(block *hcl.Block) (*MockProvider, hcl.Diagnostics)
} }
} }
var (
managedResources = make(map[string]struct{})
dataResources = make(map[string]struct{})
)
for _, block := range content.Blocks { for _, block := range content.Blocks {
switch block.Type {
case blockNameMockData, blockNameMockResource:
res, resDiags := decodeMockResourceBlock(block) res, resDiags := decodeMockResourceBlock(block)
diags = append(diags, resDiags...) diags = append(diags, resDiags...)
if resDiags.HasErrors() { 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) 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)
}
}
}
diags = append(diags, provider.validateMockResources()...)
diags = append(diags, provider.validateOverrideResources()...)
return provider, diags return provider, diags
} }
@ -1146,6 +1181,12 @@ var mockProviderBlockSchema = &hcl.BodySchema{
Type: blockNameMockData, Type: blockNameMockData,
LabelNames: []string{"type"}, LabelNames: []string{"type"},
}, },
{
Type: blockNameOverrideResource,
},
{
Type: blockNameOverrideData,
},
}, },
} }

View File

@ -153,10 +153,14 @@ func (ctx *BuiltinEvalContext) InitProvider(addr addrs.AbsProviderConfig, provid
// We cannot wrap providers.Factory itself, because factories don't support aliases. // We cannot wrap providers.Factory itself, because factories don't support aliases.
pc, ok := ctx.Evaluator.Config.Module.GetProviderConfig(addr.Provider.Type, addr.Alias) pc, ok := ctx.Evaluator.Config.Module.GetProviderConfig(addr.Provider.Type, addr.Alias)
if ok && pc.IsMocked { if ok && pc.IsMocked {
p, err = newProviderForTest(p, pc.MockResources) testP, err := newProviderForTestWithSchema(p, p.GetProviderSchema())
if err != nil { if err != nil {
return nil, err return nil, err
} }
p = testP.
withMockResources(pc.MockResources).
withOverrideResources(pc.OverrideResources)
} }
} }

View File

@ -2704,12 +2704,21 @@ func (n *NodeAbstractResourceInstance) getProvider(ctx EvalContext) (providers.I
} }
if n.Config == nil || !n.Config.IsOverridden { if n.Config == nil || !n.Config.IsOverridden {
if p, ok := underlyingProvider.(providerForTest); ok {
underlyingProvider = p.linkWithCurrentResource(n.Addr.ConfigResource())
}
return underlyingProvider, schema, nil return underlyingProvider, schema, nil
} }
providerForTest := newProviderForTestWithSchema(underlyingProvider, schema) provider, err := newProviderForTestWithSchema(underlyingProvider, schema)
if err != nil {
providerForTest.setSingleResource(n.Addr.Resource.Resource, n.Config.OverrideValues) return nil, providers.ProviderSchema{}, err
}
return providerForTest, schema, nil
provider = provider.
withOverrideResource(n.Addr.ConfigResource(), n.Config.OverrideValues).
linkWithCurrentResource(n.Addr.ConfigResource())
return provider, schema, nil
} }

View File

@ -26,46 +26,52 @@ type providerForTest struct {
internal providers.Interface internal providers.Interface
schema providers.ProviderSchema schema providers.ProviderSchema
managedResources resourceForTestByType mockResources mockResourcesForTest
dataResources resourceForTestByType overrideResources overrideResourcesForTest
currentResourceAddress string
} }
func newProviderForTestWithSchema(internal providers.Interface, schema providers.ProviderSchema) *providerForTest { func newProviderForTestWithSchema(internal providers.Interface, schema providers.ProviderSchema) (providerForTest, error) {
return &providerForTest{ 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
}
if schema.Diagnostics.HasErrors() {
return providerForTest{}, fmt.Errorf("invalid provider schema for test wrapper: %w", schema.Diagnostics.Err())
}
return providerForTest{
internal: internal, internal: internal,
schema: schema, schema: schema,
managedResources: make(resourceForTestByType), mockResources: mockResourcesForTest{
dataResources: make(resourceForTestByType), managed: make(map[string]resourceForTest),
} data: make(map[string]resourceForTest),
},
overrideResources: overrideResourcesForTest{
managed: make(map[string]resourceForTest),
data: make(map[string]resourceForTest),
},
}, nil
} }
func newProviderForTest(internal providers.Interface, res []*configs.MockResource) (providers.Interface, error) { func (p providerForTest) ReadResource(r providers.ReadResourceRequest) providers.ReadResourceResponse {
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) 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). resp.NewState, resp.Diagnostics = newMockValueComposer(r.TypeName).
ComposeBySchema(resSchema, r.ProviderMeta, overrideValues) ComposeBySchema(resSchema, r.ProviderMeta, mockValues)
return resp return resp
} }
func (p *providerForTest) PlanResourceChange(r providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { func (p providerForTest) PlanResourceChange(r providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
if r.Config.IsNull() { if r.Config.IsNull() {
return providers.PlanResourceChangeResponse{ return providers.PlanResourceChangeResponse{
PlannedState: r.ProposedNewState, // null PlannedState: r.ProposedNewState, // null
@ -74,37 +80,37 @@ func (p *providerForTest) PlanResourceChange(r providers.PlanResourceChangeReque
resSchema, _ := p.schema.SchemaForResourceType(addrs.ManagedResourceMode, r.TypeName) resSchema, _ := p.schema.SchemaForResourceType(addrs.ManagedResourceMode, r.TypeName)
overrideValues := p.managedResources.getOverrideValues(r.TypeName) mockValues := p.getMockValuesForManagedResource(r.TypeName)
var resp providers.PlanResourceChangeResponse var resp providers.PlanResourceChangeResponse
resp.PlannedState, resp.Diagnostics = newMockValueComposer(r.TypeName). resp.PlannedState, resp.Diagnostics = newMockValueComposer(r.TypeName).
ComposeBySchema(resSchema, r.Config, overrideValues) ComposeBySchema(resSchema, r.Config, mockValues)
return resp return resp
} }
func (p *providerForTest) ApplyResourceChange(r providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { func (p providerForTest) ApplyResourceChange(r providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
return providers.ApplyResourceChangeResponse{ return providers.ApplyResourceChangeResponse{
NewState: r.PlannedState, 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) resSchema, _ := p.schema.SchemaForResourceType(addrs.DataResourceMode, r.TypeName)
var resp providers.ReadDataSourceResponse var resp providers.ReadDataSourceResponse
overrideValues := p.dataResources.getOverrideValues(r.TypeName) mockValues := p.getMockValuesForDataResource(r.TypeName)
resp.State, resp.Diagnostics = newMockValueComposer(r.TypeName). resp.State, resp.Diagnostics = newMockValueComposer(r.TypeName).
ComposeBySchema(resSchema, r.Config, overrideValues) ComposeBySchema(resSchema, r.Config, mockValues)
return resp return resp
} }
// ValidateProviderConfig is irrelevant when provider is mocked or overridden. // 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{} 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 // 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 // accessible so it is safe to wipe metadata as well. See Config.transformProviderConfigsForTest
// for more details. // for more details.
func (p *providerForTest) GetProviderSchema() providers.GetProviderSchemaResponse { func (p providerForTest) GetProviderSchema() providers.GetProviderSchemaResponse {
providerSchema := p.internal.GetProviderSchema() providerSchema := p.internal.GetProviderSchema()
providerSchema.Provider = providers.Schema{} providerSchema.Provider = providers.Schema{}
providerSchema.ProviderMeta = 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. // 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{} 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") 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 // 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 // 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. // 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) 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) 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) return p.internal.UpgradeResourceState(r)
} }
func (p *providerForTest) Stop() error { func (p providerForTest) Stop() error {
return p.internal.Stop() return p.internal.Stop()
} }
func (p *providerForTest) GetFunctions() providers.GetFunctionsResponse { func (p providerForTest) GetFunctions() providers.GetFunctionsResponse {
return p.internal.GetFunctions() 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) return p.internal.CallFunction(r)
} }
func (p *providerForTest) Close() error { func (p providerForTest) Close() error {
return p.internal.Close() return p.internal.Close()
} }
type resourceForTest struct { func (p providerForTest) withMockResources(mockResources []*configs.MockResource) providerForTest {
overrideValues map[string]cty.Value 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())
} }
type resourceForTestByType map[string]resourceForTest resources[res.Type] = resourceForTest{
values: res.Defaults,
}
}
func (m resourceForTestByType) getOverrideValues(typeName string) map[string]cty.Value { return p
return m[typeName].overrideValues }
func (p providerForTest) withCopiedOverrideResources() providerForTest {
p.overrideResources = p.overrideResources.copy()
return p
}
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 { func newMockValueComposer(typeName string) hcl2shim.MockValueComposer {

View File

@ -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 * A **[`variables` block](#the-variables-and-runvariables-blocks)** (optional): define variables for all tests in the
current file. current file.
* The **[`provider` blocks](#the-providers-block)** (optional): define the providers to be used for the tests. * 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_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_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 **[`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 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. 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 In the example below, we test if the bucket name is correctly passed to the resource
without actually creating it: without actually creating it: