// Copyright IBM Corp. 2014, 2026 // SPDX-License-Identifier: BUSL-1.1 package pluggable import ( "errors" "maps" "slices" "strings" "testing" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/providers" testing_provider "github.com/hashicorp/terraform/internal/providers/testing" "github.com/zclconf/go-cty/cty" ) func TestNewPluggable(t *testing.T) { cases := map[string]struct { provider providers.Interface typeName string wantError string }{ "no error when inputs are provided": { provider: &testing_provider.MockProvider{}, typeName: "foo_bar", }, "no error when store name has underscores": { provider: &testing_provider.MockProvider{}, // foo provider containing fizz_buzz store typeName: "foo_fizz_buzz", }, "error when store type not provided": { provider: &testing_provider.MockProvider{}, typeName: "", wantError: "Attempted to initialize pluggable state with an empty string identifier for the state store.", }, "error when provider interface is nil": { provider: nil, typeName: "foo_bar", wantError: "Attempted to initialize pluggable state with a nil provider interface.", }, } for tn, tc := range cases { t.Run(tn, func(t *testing.T) { _, err := NewPluggable(tc.provider, tc.typeName) if err != nil { if tc.wantError == "" { t.Fatalf("unexpected error: %s", err) } if !strings.Contains(err.Error(), tc.wantError) { t.Fatalf("expected error %q but got %q", tc.wantError, err) } return } if err == nil && tc.wantError != "" { t.Fatalf("expected error %q but got none", tc.wantError) } }) } } func TestPluggable_ConfigSchema(t *testing.T) { p := &testing_provider.MockProvider{ GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ Provider: providers.Schema{}, DataSources: map[string]providers.Schema{}, ResourceTypes: map[string]providers.Schema{}, ListResourceTypes: map[string]providers.Schema{}, StateStores: map[string]providers.Schema{ // This imagines a provider called foo that contains // two pluggable state store implementations, called // bar and baz. // It's accurate to include the prefixed provider name // in the keys of schema maps "foo_bar": { Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "bar": { Type: cty.String, Required: true, }, }, }, }, "foo_baz": { Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "baz": { Type: cty.String, Required: true, }, }, }, }, }, }, } cases := map[string]struct { provider providers.Interface typeName string expectedAttrName string expectNil bool }{ "returns expected schema - bar store": { provider: p, typeName: "foo_bar", expectedAttrName: "bar", }, "returns expected schema - baz store": { provider: p, typeName: "foo_baz", expectedAttrName: "baz", }, "returns nil if there isn't a store with a matching name": { provider: p, typeName: "foo_not_implemented", expectNil: true, }, "returns nil if no state stores are implemented in the provider": { provider: &testing_provider.MockProvider{}, typeName: "foo_bar", expectNil: true, }, } for tn, tc := range cases { t.Run(tn, func(t *testing.T) { p, err := NewPluggable(tc.provider, tc.typeName) if err != nil { t.Fatalf("unexpected error: %s", err) } s := p.ConfigSchema() if mock, ok := tc.provider.(*testing_provider.MockProvider); ok { if !mock.GetProviderSchemaCalled { t.Fatal("expected mock's GetProviderSchema method to have been called") } } if s == nil { if !tc.expectNil { t.Fatal("ConfigSchema returned an unexpected nil schema") } return } if val := s.Attributes[tc.expectedAttrName]; val == nil { t.Fatalf("expected the returned schema to include an attr called %q, but it was missing. Schema contains attrs: %v", tc.expectedAttrName, slices.Sorted(maps.Keys(s.Attributes))) } }) } } func TestPluggable_PrepareConfig(t *testing.T) { fooBar := "foo_bar" cases := map[string]struct { provider providers.Interface typeName string config cty.Value wantError string }{ "when config is deemed valid there are no diagnostics": { provider: &testing_provider.MockProvider{ ConfigureProviderCalled: true, ValidateStateStoreConfigFn: func(req providers.ValidateStateStoreConfigRequest) providers.ValidateStateStoreConfigResponse { // if validation is ok, response has no diags return providers.ValidateStateStoreConfigResponse{} }, }, typeName: fooBar, }, "errors are returned, and expected arguments are in the request": { provider: &testing_provider.MockProvider{ ConfigureProviderCalled: true, ValidateStateStoreConfigFn: func(req providers.ValidateStateStoreConfigRequest) providers.ValidateStateStoreConfigResponse { // Are the right values being put into the incoming request? if req.TypeName != fooBar || req.Config != cty.True { t.Fatalf("expected provider ValidateStateStoreConfig method to receive TypeName %q and Config %q, instead got TypeName %q and Config %q", fooBar, cty.True, req.TypeName, req.Config) } // Force an error, to see it makes it back to the invoked method ok resp := providers.ValidateStateStoreConfigResponse{} resp.Diagnostics = resp.Diagnostics.Append(errors.New("error diagnostic raised from mock")) return resp }, }, typeName: fooBar, config: cty.BoolVal(true), wantError: "error diagnostic raised from mock", }, } for tn, tc := range cases { t.Run(tn, func(t *testing.T) { p, err := NewPluggable(tc.provider, tc.typeName) if err != nil { t.Fatalf("unexpected error: %s", err) } _, diags := p.PrepareConfig(tc.config) if mock, ok := tc.provider.(*testing_provider.MockProvider); ok { if !mock.ValidateStateStoreConfigCalled { t.Fatal("expected mock's ValidateStateStoreConfig method to have been called") } } if diags.HasErrors() { if tc.wantError == "" { t.Fatalf("unexpected error: %s", diags.Err()) } if !strings.Contains(diags.Err().Error(), tc.wantError) { t.Fatalf("expected error %q but got: %q", tc.wantError, diags.Err()) } return } if !diags.HasErrors() && tc.wantError != "" { t.Fatal("expected an error but got none") } }) } } func TestPluggable_Configure(t *testing.T) { // Arrange mocks typeName := "foo_bar" wantError := "error diagnostic raised from mock" mock := &testing_provider.MockProvider{ ConfigureProviderCalled: true, ValidateStateStoreConfigCalled: true, ConfigureStateStoreFn: func(req providers.ConfigureStateStoreRequest) providers.ConfigureStateStoreResponse { if req.TypeName != typeName || req.Config != cty.True { t.Fatalf("expected provider ConfigureStateStore method to receive TypeName %q and Config %q, instead got TypeName %q and Config %q", typeName, cty.True, req.TypeName, req.Config) } resp := providers.ConfigureStateStoreResponse{} resp.Diagnostics = resp.Diagnostics.Append(errors.New(wantError)) return resp }, } // Make Pluggable and invoke Configure p, err := NewPluggable(mock, typeName) if err != nil { t.Fatalf("unexpected error: %s", err) } // This isn't representative of true values used with the method, but is sufficient // for testing that the mock receives the provided value as expected. config := cty.BoolVal(true) diags := p.Configure(config) // Assertions if !mock.ValidateStateStoreConfigCalled { t.Fatal("expected mock's ValidateStateStoreConfig method to have been called") } if !diags.HasErrors() { t.Fatal("expected an error but got none") } if !strings.Contains(diags.Err().Error(), wantError) { t.Fatalf("expected error %q but got: %q", wantError, diags.Err()) } } func TestPluggable_Workspaces(t *testing.T) { fooBar := "foo_bar" cases := map[string]struct { provider providers.Interface expectedWorkspaces []string wantError string }{ "returned workspaces match what's returned from the store": { // and "default" isn't included by default provider: &testing_provider.MockProvider{ ConfigureProviderCalled: true, ValidateStateStoreConfigCalled: true, ConfigureStateStoreCalled: true, GetStatesFn: func(req providers.GetStatesRequest) providers.GetStatesResponse { workspaces := []string{"abcd", "efg"} resp := providers.GetStatesResponse{ States: workspaces, } return resp }, }, expectedWorkspaces: []string{"abcd", "efg"}, }, "errors are returned, and expected arguments are in the request": { provider: &testing_provider.MockProvider{ ConfigureProviderCalled: true, ValidateStateStoreConfigCalled: true, ConfigureStateStoreCalled: true, GetStatesFn: func(req providers.GetStatesRequest) providers.GetStatesResponse { if req.TypeName != fooBar { t.Fatalf("expected provider GetStates method to receive TypeName %q, instead got TypeName %q", fooBar, req.TypeName) } resp := providers.GetStatesResponse{} resp.Diagnostics = resp.Diagnostics.Append(errors.New("error diagnostic raised from mock")) return resp }, }, wantError: "error diagnostic raised from mock", }, } for tn, tc := range cases { t.Run(tn, func(t *testing.T) { p, err := NewPluggable(tc.provider, fooBar) if err != nil { t.Fatalf("unexpected error: %s", err) } workspaces, wDiags := p.Workspaces() if mock, ok := tc.provider.(*testing_provider.MockProvider); ok { if !mock.GetStatesCalled { t.Fatal("expected mock's GetStates method to have been called") } } if wDiags.HasErrors() { if tc.wantError == "" { t.Fatalf("unexpected error: %s", err) } if !strings.Contains(wDiags.Err().Error(), tc.wantError) { t.Fatalf("expected error %q but got: %q", tc.wantError, err) } return } if tc.wantError != "" { t.Fatal("expected an error but got none") } if slices.Compare(workspaces, tc.expectedWorkspaces) != 0 { t.Fatalf("expected workspaces %v, got %v", tc.expectedWorkspaces, workspaces) } }) } } func TestPluggable_DeleteWorkspace(t *testing.T) { // Arrange mocks typeName := "foo_bar" stateId := "my-state" mock := &testing_provider.MockProvider{ ConfigureProviderCalled: true, ValidateStateStoreConfigCalled: true, ConfigureStateStoreCalled: true, DeleteStateFn: func(req providers.DeleteStateRequest) providers.DeleteStateResponse { if req.TypeName != typeName || req.StateId != stateId { t.Fatalf("expected provider DeleteState method to receive TypeName %q and StateId %q, instead got TypeName %q and StateId %q", typeName, stateId, req.TypeName, req.StateId, ) } resp := providers.DeleteStateResponse{} resp.Diagnostics = resp.Diagnostics.Append(errors.New("error diagnostic raised from mock")) return resp }, } // Make Pluggable and invoke DeleteWorkspace p, err := NewPluggable(mock, typeName) if err != nil { t.Fatalf("unexpected error: %s", err) } dwDiags := p.DeleteWorkspace(stateId, false) // Assertions if !mock.DeleteStateCalled { t.Fatal("expected mock's DeleteState method to have been called") } if !dwDiags.HasErrors() { t.Fatal("test is expected to return an error, but there isn't one") } wantError := "error diagnostic raised from mock" if !strings.Contains(dwDiags.Err().Error(), wantError) { t.Fatalf("expected error %q but got: %q", wantError, err) } } func TestPluggable_ProviderSchema(t *testing.T) { t.Run("Returns the expected provider schema", func(t *testing.T) { mock := &testing_provider.MockProvider{ GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ Provider: providers.Schema{ Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "custom_attr": {Type: cty.String, Optional: true}, }, }, }, }, } p, err := NewPluggable(mock, "foobar") if err != nil { t.Fatalf("unexpected error: %s", err) } providerSchema := p.ProviderSchema() if !mock.GetProviderSchemaCalled { t.Fatal("expected ProviderSchema to call the GetProviderSchema RPC") } if providerSchema == nil { t.Fatal("ProviderSchema returned an unexpected nil schema") } if val := providerSchema.Attributes["custom_attr"]; val == nil { t.Fatalf("expected the returned schema to include an attr called %q, but it was missing. Schema contains attrs: %v", "custom_attr", slices.Sorted(maps.Keys(providerSchema.Attributes))) } }) t.Run("Returns a nil schema when the provider has an empty schema", func(t *testing.T) { mock := &testing_provider.MockProvider{ GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ Provider: providers.Schema{ // empty schema }, }, } p, err := NewPluggable(mock, "foobar") if err != nil { t.Fatalf("unexpected error: %s", err) } providerSchema := p.ProviderSchema() if !mock.GetProviderSchemaCalled { t.Fatal("expected ProviderSchema to call the GetProviderSchema RPC") } if providerSchema != nil { t.Fatalf("expected ProviderSchema to return a nil schema but got: %#v", providerSchema) } }) }