diff --git a/internal/providers/mock.go b/internal/providers/mock.go index e05c20d0a6..b812cde20f 100644 --- a/internal/providers/mock.go +++ b/internal/providers/mock.go @@ -41,6 +41,23 @@ func (m *Mock) GetProviderSchema() GetProviderSchemaResponse { if m.schema == nil { // Cache the schema, it's not changing. schema := m.Provider.GetProviderSchema() + + // Override the provider schema with the constant mock provider schema. + // This is empty at the moment, check configs/mock_provider.go for the + // actual schema. + // + // The GetProviderSchemaResponse is returned by value, so it should be + // safe for us to modify directly, without affecting any shared state + // that could be in use elsewhere. + schema.Provider = Schema{ + Version: schema.Provider.Version, + Block: nil, // Empty - we support no blocks or attributes in mock provider configurations. + } + + // Note, we leave the resource and data source schemas as they are since + // we want to be able to validate those configurations against the real + // provider schemas. + m.schema = &schema } return *m.schema diff --git a/internal/terraform/context_apply2_test.go b/internal/terraform/context_apply2_test.go index 2f522abef2..d40a79bfb2 100644 --- a/internal/terraform/context_apply2_test.go +++ b/internal/terraform/context_apply2_test.go @@ -2492,6 +2492,151 @@ resource "test_object" "foo" { } } +func TestContext2Apply_mockProviderRequiredSchema(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +provider "test" {} + +data "test_object" "foo" {} + +resource "test_object" "foo" { + value = data.test_object.foo.output +} +`, + }) + + // Manually mark the provider config as being mocked. + m.Module.ProviderConfigs["test"].Mock = true + m.Module.ProviderConfigs["test"].MockData = &configs.MockData{ + MockDataSources: map[string]*configs.MockResource{ + "test_object": { + Mode: addrs.DataResourceMode, + Type: "test_object", + Defaults: cty.ObjectVal(map[string]cty.Value{ + "output": cty.StringVal("expected data output"), + }), + }, + }, + MockResources: map[string]*configs.MockResource{ + "test_object": { + Mode: addrs.ManagedResourceMode, + Type: "test_object", + Defaults: cty.ObjectVal(map[string]cty.Value{ + "output": cty.StringVal("expected resource output"), + }), + }, + }, + } + + // This time our test provider has a required attribute that we don't + // provide in the configuration. The fact we've marked this provider as a + // mock means the missing required attribute doesn't matter. + + testProvider := &MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{ + Block: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "required": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + ResourceTypes: map[string]providers.Schema{ + "test_object": { + Block: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Required: true, + }, + "output": { + Type: cty.String, + Computed: true, + }, + }, + }, + }, + }, + DataSources: map[string]providers.Schema{ + "test_object": { + Block: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "output": { + Type: cty.String, + Computed: true, + }, + }, + }, + }, + }, + }, + } + + reachedReadDataSourceFn := false + reachedPlanResourceChangeFn := false + reachedApplyResourceChangeFn := false + testProvider.ReadDataSourceFn = func(request providers.ReadDataSourceRequest) (resp providers.ReadDataSourceResponse) { + reachedReadDataSourceFn = true + cfg := request.Config.AsValueMap() + cfg["output"] = cty.StringVal("unexpected data output") + resp.State = cty.ObjectVal(cfg) + return resp + } + testProvider.PlanResourceChangeFn = func(request providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { + reachedPlanResourceChangeFn = true + cfg := request.Config.AsValueMap() + cfg["output"] = cty.UnknownVal(cty.String) + resp.PlannedState = cty.ObjectVal(cfg) + return resp + } + testProvider.ApplyResourceChangeFn = func(request providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { + reachedApplyResourceChangeFn = true + cfg := request.Config.AsValueMap() + cfg["output"] = cty.StringVal("unexpected resource output") + resp.NewState = cty.ObjectVal(cfg) + return resp + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(testProvider), + }, + }) + + plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + }) + if diags.HasErrors() { + t.Fatalf("expected no errors, but got %s", diags) + } + + state, diags := ctx.Apply(plan, m, nil) + if diags.HasErrors() { + t.Fatalf("expected no errors, but got %s", diags) + } + + // Check we never made it to the actual provider. + if reachedReadDataSourceFn { + t.Errorf("read the data source in the provider when it should have been mocked") + } + if reachedPlanResourceChangeFn { + t.Errorf("planned the resource in the provider when it should have been mocked") + } + if reachedApplyResourceChangeFn { + t.Errorf("applied the resource in the provider when it should have been mocked") + } + + // Check we got the right data back from our mocked provider. + instance := state.ResourceInstance(mustResourceInstanceAddr("test_object.foo")) + expected := "{\"output\":\"expected resource output\",\"value\":\"expected data output\"}" + if diff := cmp.Diff(string(instance.Current.AttrsJSON), expected); len(diff) > 0 { + t.Errorf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expected, string(instance.Current.AttrsJSON), diff) + } +} + func TestContext2Apply_forget(t *testing.T) { addrA := mustResourceInstanceAddr("test_object.a") m := testModuleInline(t, map[string]string{