mirror of
https://github.com/hashicorp/terraform.git
synced 2026-03-21 10:00:09 -04:00
457 lines
13 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|