terraform/internal/backend/pluggable/pluggable_test.go
2026-02-17 13:56:34 +00:00

457 lines
13 KiB
Go

// 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)
}
})
}