mirror of https://github.com/hashicorp/terraform
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
3504 lines
91 KiB
3504 lines
91 KiB
// Copyright IBM Corp. 2014, 2026
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package plugin6
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"github.com/zclconf/go-cty/cty"
|
|
"go.uber.org/mock/gomock"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
|
|
"github.com/hashicorp/terraform/internal/addrs"
|
|
"github.com/hashicorp/terraform/internal/backend"
|
|
"github.com/hashicorp/terraform/internal/configs/configschema"
|
|
"github.com/hashicorp/terraform/internal/configs/hcl2shim"
|
|
"github.com/hashicorp/terraform/internal/plans"
|
|
"github.com/hashicorp/terraform/internal/plugin6/convert"
|
|
mockproto "github.com/hashicorp/terraform/internal/plugin6/mock_proto"
|
|
"github.com/hashicorp/terraform/internal/providers"
|
|
"github.com/hashicorp/terraform/internal/schemarepo"
|
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
|
"github.com/hashicorp/terraform/internal/tfplugin6"
|
|
proto "github.com/hashicorp/terraform/internal/tfplugin6"
|
|
)
|
|
|
|
var _ providers.Interface = (*GRPCProvider)(nil)
|
|
var _ providers.StateStoreChunkSizeSetter = (*GRPCProvider)(nil) // Specific to the v6 version of GRPCProvider
|
|
|
|
var (
|
|
equateEmpty = cmpopts.EquateEmpty()
|
|
typeComparer = cmp.Comparer(cty.Type.Equals)
|
|
valueComparer = cmp.Comparer(cty.Value.RawEquals)
|
|
)
|
|
|
|
func mockProviderClient(t *testing.T) *mockproto.MockProviderClient {
|
|
ctrl := gomock.NewController(t)
|
|
client := mockproto.NewMockProviderClient(ctrl)
|
|
|
|
// we always need a GetSchema method
|
|
client.EXPECT().GetProviderSchema(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(providerProtoSchema(), nil)
|
|
|
|
// GetResourceIdentitySchemas is called as part of GetSchema
|
|
client.EXPECT().GetResourceIdentitySchemas(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(providerResourceIdentitySchemas(), nil)
|
|
|
|
return client
|
|
}
|
|
|
|
func mockReadStateBytesClient(t *testing.T) *mockproto.MockProvider_ReadStateBytesClient[tfplugin6.ReadStateBytes_Response] {
|
|
ctrl := gomock.NewController(t)
|
|
return mockproto.NewMockProvider_ReadStateBytesClient[tfplugin6.ReadStateBytes_Response](ctrl)
|
|
}
|
|
|
|
func mockWriteStateBytesClient(t *testing.T) *mockproto.MockProvider_WriteStateBytesClient[tfplugin6.WriteStateBytes_RequestChunk, tfplugin6.WriteStateBytes_Response] {
|
|
ctrl := gomock.NewController(t)
|
|
return mockproto.NewMockProvider_WriteStateBytesClient[tfplugin6.WriteStateBytes_RequestChunk, tfplugin6.WriteStateBytes_Response](ctrl)
|
|
}
|
|
|
|
func checkDiags(t *testing.T, d tfdiags.Diagnostics) {
|
|
t.Helper()
|
|
if d.HasErrors() {
|
|
t.Fatal(d.Err())
|
|
}
|
|
}
|
|
|
|
// checkDiagsHasError ensures error diagnostics are present or fails the test.
|
|
func checkDiagsHasError(t *testing.T, d tfdiags.Diagnostics) {
|
|
t.Helper()
|
|
|
|
if !d.HasErrors() {
|
|
t.Fatal("expected error diagnostics")
|
|
}
|
|
}
|
|
|
|
func providerProtoSchema() *proto.GetProviderSchema_Response {
|
|
return &proto.GetProviderSchema_Response{
|
|
Provider: &proto.Schema{
|
|
Block: &proto.Schema_Block{
|
|
Attributes: []*proto.Schema_Attribute{
|
|
{
|
|
Name: "attr",
|
|
Type: []byte(`"string"`),
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
ResourceSchemas: map[string]*proto.Schema{
|
|
"resource": {
|
|
Version: 1,
|
|
Block: &proto.Schema_Block{
|
|
Attributes: []*proto.Schema_Attribute{
|
|
{
|
|
Name: "attr",
|
|
Type: []byte(`"string"`),
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"list": {
|
|
Version: 1,
|
|
Block: &proto.Schema_Block{
|
|
Attributes: []*proto.Schema_Attribute{
|
|
{
|
|
Name: "resource_attr",
|
|
Type: []byte(`"string"`),
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
DataSourceSchemas: map[string]*proto.Schema{
|
|
"data": {
|
|
Version: 1,
|
|
Block: &proto.Schema_Block{
|
|
Attributes: []*proto.Schema_Attribute{
|
|
{
|
|
Name: "attr",
|
|
Type: []byte(`"string"`),
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
EphemeralResourceSchemas: map[string]*proto.Schema{
|
|
"ephemeral": &proto.Schema{
|
|
Block: &proto.Schema_Block{
|
|
Attributes: []*proto.Schema_Attribute{
|
|
{
|
|
Name: "attr",
|
|
Type: []byte(`"string"`),
|
|
Computed: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
ListResourceSchemas: map[string]*proto.Schema{
|
|
"list": &proto.Schema{
|
|
Version: 1,
|
|
Block: &proto.Schema_Block{
|
|
Attributes: []*proto.Schema_Attribute{
|
|
{
|
|
Name: "filter_attr",
|
|
Type: []byte(`"string"`),
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
ActionSchemas: map[string]*proto.ActionSchema{
|
|
"action": {
|
|
Schema: &proto.Schema{
|
|
Block: &proto.Schema_Block{
|
|
Version: 1,
|
|
Attributes: []*proto.Schema_Attribute{
|
|
{
|
|
Name: "attr",
|
|
Type: []byte(`"string"`),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
StateStoreSchemas: map[string]*proto.Schema{
|
|
"mock_store": {
|
|
Block: &proto.Schema_Block{
|
|
Version: 1,
|
|
Attributes: []*proto.Schema_Attribute{
|
|
{
|
|
Name: "region",
|
|
Type: []byte(`"string"`),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
ServerCapabilities: &proto.ServerCapabilities{
|
|
GetProviderSchemaOptional: true,
|
|
},
|
|
}
|
|
}
|
|
|
|
func providerResourceIdentitySchemas() *proto.GetResourceIdentitySchemas_Response {
|
|
return &proto.GetResourceIdentitySchemas_Response{
|
|
IdentitySchemas: map[string]*proto.ResourceIdentitySchema{
|
|
"resource": {
|
|
Version: 1,
|
|
IdentityAttributes: []*proto.ResourceIdentitySchema_IdentityAttribute{
|
|
{
|
|
Name: "id_attr",
|
|
Type: []byte(`"string"`),
|
|
RequiredForImport: true,
|
|
},
|
|
},
|
|
},
|
|
"list": {
|
|
Version: 1,
|
|
IdentityAttributes: []*proto.ResourceIdentitySchema_IdentityAttribute{
|
|
{
|
|
Name: "id_attr",
|
|
Type: []byte(`"string"`),
|
|
RequiredForImport: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_GetProviderSchema(t *testing.T) {
|
|
p := &GRPCProvider{
|
|
client: mockProviderClient(t),
|
|
}
|
|
|
|
resp := p.GetProviderSchema()
|
|
checkDiags(t, resp.Diagnostics)
|
|
}
|
|
|
|
// ensure that the global schema cache is used when the provider supports
|
|
// GetProviderSchemaOptional
|
|
func TestGRPCProvider_GetSchema_globalCache(t *testing.T) {
|
|
p := &GRPCProvider{
|
|
Addr: addrs.ImpliedProviderForUnqualifiedType("test"),
|
|
client: mockProviderClient(t),
|
|
}
|
|
|
|
// first call primes the cache
|
|
resp := p.GetProviderSchema()
|
|
|
|
// create a new provider instance which does not expect a GetProviderSchemaCall
|
|
p = &GRPCProvider{
|
|
Addr: addrs.ImpliedProviderForUnqualifiedType("test"),
|
|
client: mockproto.NewMockProviderClient(gomock.NewController(t)),
|
|
}
|
|
|
|
resp = p.GetProviderSchema()
|
|
checkDiags(t, resp.Diagnostics)
|
|
}
|
|
|
|
// Ensure that gRPC errors are returned early.
|
|
// Reference: https://github.com/hashicorp/terraform/issues/31047
|
|
func TestGRPCProvider_GetSchema_GRPCError(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
client := mockproto.NewMockProviderClient(ctrl)
|
|
|
|
client.EXPECT().GetProviderSchema(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.GetProviderSchema_Response{}, fmt.Errorf("test error"))
|
|
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
resp := p.GetProviderSchema()
|
|
|
|
checkDiagsHasError(t, resp.Diagnostics)
|
|
}
|
|
|
|
// Ensure that provider error diagnostics are returned early.
|
|
// Reference: https://github.com/hashicorp/terraform/issues/31047
|
|
func TestGRPCProvider_GetSchema_ResponseErrorDiagnostic(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
client := mockproto.NewMockProviderClient(ctrl)
|
|
|
|
client.EXPECT().GetProviderSchema(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.GetProviderSchema_Response{
|
|
Diagnostics: []*proto.Diagnostic{
|
|
{
|
|
Severity: proto.Diagnostic_ERROR,
|
|
Summary: "error summary",
|
|
Detail: "error detail",
|
|
},
|
|
},
|
|
// Trigger potential panics
|
|
Provider: &proto.Schema{},
|
|
}, nil)
|
|
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
resp := p.GetProviderSchema()
|
|
|
|
checkDiagsHasError(t, resp.Diagnostics)
|
|
}
|
|
|
|
func TestGRPCProvider_GetSchema_IdentityError(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
client := mockproto.NewMockProviderClient(ctrl)
|
|
|
|
client.EXPECT().GetProviderSchema(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(providerProtoSchema(), nil)
|
|
|
|
client.EXPECT().GetResourceIdentitySchemas(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.GetResourceIdentitySchemas_Response{}, fmt.Errorf("test error"))
|
|
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
resp := p.GetProviderSchema()
|
|
|
|
checkDiagsHasError(t, resp.Diagnostics)
|
|
}
|
|
|
|
func TestGRPCProvider_GetSchema_IdentityUnimplemented(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
client := mockproto.NewMockProviderClient(ctrl)
|
|
|
|
client.EXPECT().GetProviderSchema(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(providerProtoSchema(), nil)
|
|
|
|
client.EXPECT().GetResourceIdentitySchemas(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.GetResourceIdentitySchemas_Response{}, status.Error(codes.Unimplemented, "test error"))
|
|
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
resp := p.GetProviderSchema()
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
}
|
|
|
|
func TestGRPCProvider_GetSchema_IdentityErrorDiagnostic(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
client := mockproto.NewMockProviderClient(ctrl)
|
|
|
|
client.EXPECT().GetProviderSchema(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(providerProtoSchema(), nil)
|
|
|
|
client.EXPECT().GetResourceIdentitySchemas(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.GetResourceIdentitySchemas_Response{
|
|
Diagnostics: []*proto.Diagnostic{
|
|
{
|
|
Severity: proto.Diagnostic_ERROR,
|
|
Summary: "error summary",
|
|
Detail: "error detail",
|
|
},
|
|
},
|
|
IdentitySchemas: map[string]*proto.ResourceIdentitySchema{},
|
|
}, nil)
|
|
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
resp := p.GetProviderSchema()
|
|
|
|
checkDiagsHasError(t, resp.Diagnostics)
|
|
}
|
|
|
|
func TestGRPCProvider_GetResourceIdentitySchemas(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
client := mockproto.NewMockProviderClient(ctrl)
|
|
|
|
client.EXPECT().GetResourceIdentitySchemas(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(providerResourceIdentitySchemas(), nil)
|
|
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
resp := p.GetResourceIdentitySchemas()
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
}
|
|
|
|
func TestGRPCProvider_GetResourceIdentitySchemas_Unimplemented(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
client := mockproto.NewMockProviderClient(ctrl)
|
|
|
|
client.EXPECT().GetResourceIdentitySchemas(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.GetResourceIdentitySchemas_Response{}, status.Error(codes.Unimplemented, "test error"))
|
|
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
resp := p.GetResourceIdentitySchemas()
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
}
|
|
|
|
func TestGRPCProvider_PrepareProviderConfig(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
client.EXPECT().ValidateProviderConfig(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.ValidateProviderConfig_Response{}, nil)
|
|
|
|
cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{"attr": "value"})
|
|
resp := p.ValidateProviderConfig(providers.ValidateProviderConfigRequest{Config: cfg})
|
|
checkDiags(t, resp.Diagnostics)
|
|
}
|
|
|
|
func TestGRPCProvider_ValidateResourceConfig(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
client.EXPECT().ValidateResourceConfig(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.ValidateResourceConfig_Response{}, nil)
|
|
|
|
cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{"attr": "value"})
|
|
resp := p.ValidateResourceConfig(providers.ValidateResourceConfigRequest{
|
|
TypeName: "resource",
|
|
Config: cfg,
|
|
})
|
|
checkDiags(t, resp.Diagnostics)
|
|
}
|
|
|
|
func TestGRPCProvider_ValidateDataResourceConfig(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
client.EXPECT().ValidateDataResourceConfig(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.ValidateDataResourceConfig_Response{}, nil)
|
|
|
|
cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{"attr": "value"})
|
|
resp := p.ValidateDataResourceConfig(providers.ValidateDataResourceConfigRequest{
|
|
TypeName: "data",
|
|
Config: cfg,
|
|
})
|
|
checkDiags(t, resp.Diagnostics)
|
|
}
|
|
|
|
func TestGRPCProvider_ValidateListResourceConfig(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
client.EXPECT().ValidateListResourceConfig(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.ValidateListResourceConfig_Response{}, nil)
|
|
|
|
cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{"config": map[string]interface{}{"filter_attr": "value"}})
|
|
resp := p.ValidateListResourceConfig(providers.ValidateListResourceConfigRequest{
|
|
TypeName: "list",
|
|
Config: cfg,
|
|
})
|
|
checkDiags(t, resp.Diagnostics)
|
|
}
|
|
|
|
func TestGRPCProvider_ValidateListResourceConfig_OptionalCfg(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
client := mockproto.NewMockProviderClient(ctrl)
|
|
sch := providerProtoSchema()
|
|
sch.ListResourceSchemas["list"].Block.Attributes[0].Optional = true
|
|
sch.ListResourceSchemas["list"].Block.Attributes[0].Required = false
|
|
// we always need a GetSchema method
|
|
client.EXPECT().GetProviderSchema(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(sch, nil)
|
|
|
|
// GetResourceIdentitySchemas is called as part of GetSchema
|
|
client.EXPECT().GetResourceIdentitySchemas(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(providerResourceIdentitySchemas(), nil)
|
|
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
client.EXPECT().ValidateListResourceConfig(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.ValidateListResourceConfig_Response{}, nil)
|
|
|
|
converted := convert.ProtoToListSchema(sch.ListResourceSchemas["list"])
|
|
cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{})
|
|
coercedCfg, err := converted.Body.CoerceValue(cfg)
|
|
if err != nil {
|
|
t.Fatalf("failed to coerce config: %v", err)
|
|
}
|
|
resp := p.ValidateListResourceConfig(providers.ValidateListResourceConfigRequest{
|
|
TypeName: "list",
|
|
Config: coercedCfg,
|
|
})
|
|
checkDiags(t, resp.Diagnostics)
|
|
}
|
|
|
|
func TestGRPCProvider_UpgradeResourceState(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
client.EXPECT().UpgradeResourceState(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.UpgradeResourceState_Response{
|
|
UpgradedState: &proto.DynamicValue{
|
|
Msgpack: []byte("\x81\xa4attr\xa3bar"),
|
|
},
|
|
}, nil)
|
|
|
|
resp := p.UpgradeResourceState(providers.UpgradeResourceStateRequest{
|
|
TypeName: "resource",
|
|
Version: 0,
|
|
RawStateJSON: []byte(`{"old_attr":"bar"}`),
|
|
})
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
expected := cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
})
|
|
|
|
if !cmp.Equal(expected, resp.UpgradedState, typeComparer, valueComparer, equateEmpty) {
|
|
t.Fatal(cmp.Diff(expected, resp.UpgradedState, typeComparer, valueComparer, equateEmpty))
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_UpgradeResourceStateJSON(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
client.EXPECT().UpgradeResourceState(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.UpgradeResourceState_Response{
|
|
UpgradedState: &proto.DynamicValue{
|
|
Json: []byte(`{"attr":"bar"}`),
|
|
},
|
|
}, nil)
|
|
|
|
resp := p.UpgradeResourceState(providers.UpgradeResourceStateRequest{
|
|
TypeName: "resource",
|
|
Version: 0,
|
|
RawStateJSON: []byte(`{"old_attr":"bar"}`),
|
|
})
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
expected := cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
})
|
|
|
|
if !cmp.Equal(expected, resp.UpgradedState, typeComparer, valueComparer, equateEmpty) {
|
|
t.Fatal(cmp.Diff(expected, resp.UpgradedState, typeComparer, valueComparer, equateEmpty))
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_UpgradeResourceIdentity(t *testing.T) {
|
|
testCases := []struct {
|
|
desc string
|
|
response *proto.UpgradeResourceIdentity_Response
|
|
expectError bool
|
|
expectedValue cty.Value
|
|
}{
|
|
{
|
|
"successful upgrade",
|
|
&proto.UpgradeResourceIdentity_Response{
|
|
UpgradedIdentity: &proto.ResourceIdentityData{
|
|
IdentityData: &proto.DynamicValue{
|
|
Json: []byte(`{"id_attr":"bar"}`),
|
|
},
|
|
},
|
|
},
|
|
false,
|
|
cty.ObjectVal(map[string]cty.Value{"id_attr": cty.StringVal("bar")}),
|
|
},
|
|
{
|
|
"response with error diagnostic",
|
|
&proto.UpgradeResourceIdentity_Response{
|
|
Diagnostics: []*proto.Diagnostic{
|
|
{
|
|
Severity: proto.Diagnostic_ERROR,
|
|
Summary: "test error",
|
|
Detail: "test error detail",
|
|
},
|
|
},
|
|
},
|
|
true,
|
|
cty.NilVal,
|
|
},
|
|
{
|
|
"schema mismatch",
|
|
&proto.UpgradeResourceIdentity_Response{
|
|
UpgradedIdentity: &proto.ResourceIdentityData{
|
|
IdentityData: &proto.DynamicValue{
|
|
Json: []byte(`{"attr_new":"bar"}`),
|
|
},
|
|
},
|
|
},
|
|
true,
|
|
cty.NilVal,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.desc, func(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
client.EXPECT().UpgradeResourceIdentity(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(tc.response, nil)
|
|
|
|
resp := p.UpgradeResourceIdentity(providers.UpgradeResourceIdentityRequest{
|
|
TypeName: "resource",
|
|
Version: 0,
|
|
RawIdentityJSON: []byte(`{"old_attr":"bar"}`),
|
|
})
|
|
|
|
if tc.expectError {
|
|
checkDiagsHasError(t, resp.Diagnostics)
|
|
} else {
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
if !cmp.Equal(tc.expectedValue, resp.UpgradedIdentity, typeComparer, valueComparer, equateEmpty) {
|
|
t.Fatal(cmp.Diff(tc.expectedValue, resp.UpgradedIdentity, typeComparer, valueComparer, equateEmpty))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_Configure(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
client.EXPECT().ConfigureProvider(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.ConfigureProvider_Response{}, nil)
|
|
|
|
resp := p.ConfigureProvider(providers.ConfigureProviderRequest{
|
|
Config: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("foo"),
|
|
}),
|
|
})
|
|
checkDiags(t, resp.Diagnostics)
|
|
}
|
|
|
|
func TestGRPCProvider_Stop(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
client := mockproto.NewMockProviderClient(ctrl)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
client.EXPECT().StopProvider(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.StopProvider_Response{}, nil)
|
|
|
|
err := p.Stop()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_ReadResource(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
client.EXPECT().ReadResource(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.ReadResource_Response{
|
|
NewState: &proto.DynamicValue{
|
|
Msgpack: []byte("\x81\xa4attr\xa3bar"),
|
|
},
|
|
}, nil)
|
|
|
|
resp := p.ReadResource(providers.ReadResourceRequest{
|
|
TypeName: "resource",
|
|
PriorState: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("foo"),
|
|
}),
|
|
})
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
expected := cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
})
|
|
|
|
if !cmp.Equal(expected, resp.NewState, typeComparer, valueComparer, equateEmpty) {
|
|
t.Fatal(cmp.Diff(expected, resp.NewState, typeComparer, valueComparer, equateEmpty))
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_ReadResource_deferred(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
client.EXPECT().ReadResource(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.ReadResource_Response{
|
|
NewState: &proto.DynamicValue{
|
|
Msgpack: []byte("\x81\xa4attr\xa3bar"),
|
|
},
|
|
Deferred: &proto.Deferred{
|
|
Reason: proto.Deferred_ABSENT_PREREQ,
|
|
},
|
|
}, nil)
|
|
|
|
resp := p.ReadResource(providers.ReadResourceRequest{
|
|
TypeName: "resource",
|
|
PriorState: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("foo"),
|
|
}),
|
|
})
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
expectedDeferred := &providers.Deferred{
|
|
Reason: providers.DeferredReasonAbsentPrereq,
|
|
}
|
|
if !cmp.Equal(expectedDeferred, resp.Deferred, typeComparer, valueComparer, equateEmpty) {
|
|
t.Fatal(cmp.Diff(expectedDeferred, resp.Deferred, typeComparer, valueComparer, equateEmpty))
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_ReadResourceJSON(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
client.EXPECT().ReadResource(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.ReadResource_Response{
|
|
NewState: &proto.DynamicValue{
|
|
Json: []byte(`{"attr":"bar"}`),
|
|
},
|
|
}, nil)
|
|
|
|
resp := p.ReadResource(providers.ReadResourceRequest{
|
|
TypeName: "resource",
|
|
PriorState: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("foo"),
|
|
}),
|
|
})
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
expected := cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
})
|
|
|
|
if !cmp.Equal(expected, resp.NewState, typeComparer, valueComparer, equateEmpty) {
|
|
t.Fatal(cmp.Diff(expected, resp.NewState, typeComparer, valueComparer, equateEmpty))
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_ReadEmptyJSON(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
client.EXPECT().ReadResource(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.ReadResource_Response{
|
|
NewState: &proto.DynamicValue{
|
|
Json: []byte(``),
|
|
},
|
|
}, nil)
|
|
|
|
obj := cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("foo"),
|
|
})
|
|
resp := p.ReadResource(providers.ReadResourceRequest{
|
|
TypeName: "resource",
|
|
PriorState: obj,
|
|
})
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
expected := cty.NullVal(obj.Type())
|
|
|
|
if !cmp.Equal(expected, resp.NewState, typeComparer, valueComparer, equateEmpty) {
|
|
t.Fatal(cmp.Diff(expected, resp.NewState, typeComparer, valueComparer, equateEmpty))
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_PlanResourceChange(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
expectedPrivate := []byte(`{"meta": "data"}`)
|
|
|
|
client.EXPECT().PlanResourceChange(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.PlanResourceChange_Response{
|
|
PlannedState: &proto.DynamicValue{
|
|
Msgpack: []byte("\x81\xa4attr\xa3bar"),
|
|
},
|
|
RequiresReplace: []*proto.AttributePath{
|
|
{
|
|
Steps: []*proto.AttributePath_Step{
|
|
{
|
|
Selector: &proto.AttributePath_Step_AttributeName{
|
|
AttributeName: "attr",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
PlannedPrivate: expectedPrivate,
|
|
}, nil)
|
|
|
|
resp := p.PlanResourceChange(providers.PlanResourceChangeRequest{
|
|
TypeName: "resource",
|
|
PriorState: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("foo"),
|
|
}),
|
|
ProposedNewState: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
}),
|
|
Config: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
}),
|
|
})
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
expectedState := cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
})
|
|
|
|
if !cmp.Equal(expectedState, resp.PlannedState, typeComparer, valueComparer, equateEmpty) {
|
|
t.Fatal(cmp.Diff(expectedState, resp.PlannedState, typeComparer, valueComparer, equateEmpty))
|
|
}
|
|
|
|
expectedReplace := `[]cty.Path{cty.Path{cty.GetAttrStep{Name:"attr"}}}`
|
|
replace := fmt.Sprintf("%#v", resp.RequiresReplace)
|
|
if expectedReplace != replace {
|
|
t.Fatalf("expected %q, got %q", expectedReplace, replace)
|
|
}
|
|
|
|
if !bytes.Equal(expectedPrivate, resp.PlannedPrivate) {
|
|
t.Fatalf("expected %q, got %q", expectedPrivate, resp.PlannedPrivate)
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_PlanResourceChangeJSON(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
expectedPrivate := []byte(`{"meta": "data"}`)
|
|
|
|
client.EXPECT().PlanResourceChange(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.PlanResourceChange_Response{
|
|
PlannedState: &proto.DynamicValue{
|
|
Json: []byte(`{"attr":"bar"}`),
|
|
},
|
|
RequiresReplace: []*proto.AttributePath{
|
|
{
|
|
Steps: []*proto.AttributePath_Step{
|
|
{
|
|
Selector: &proto.AttributePath_Step_AttributeName{
|
|
AttributeName: "attr",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
PlannedPrivate: expectedPrivate,
|
|
}, nil)
|
|
|
|
resp := p.PlanResourceChange(providers.PlanResourceChangeRequest{
|
|
TypeName: "resource",
|
|
PriorState: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("foo"),
|
|
}),
|
|
ProposedNewState: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
}),
|
|
Config: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
}),
|
|
})
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
expectedState := cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
})
|
|
|
|
if !cmp.Equal(expectedState, resp.PlannedState, typeComparer, valueComparer, equateEmpty) {
|
|
t.Fatal(cmp.Diff(expectedState, resp.PlannedState, typeComparer, valueComparer, equateEmpty))
|
|
}
|
|
|
|
expectedReplace := `[]cty.Path{cty.Path{cty.GetAttrStep{Name:"attr"}}}`
|
|
replace := fmt.Sprintf("%#v", resp.RequiresReplace)
|
|
if expectedReplace != replace {
|
|
t.Fatalf("expected %q, got %q", expectedReplace, replace)
|
|
}
|
|
|
|
if !bytes.Equal(expectedPrivate, resp.PlannedPrivate) {
|
|
t.Fatalf("expected %q, got %q", expectedPrivate, resp.PlannedPrivate)
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_ApplyResourceChange(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
expectedPrivate := []byte(`{"meta": "data"}`)
|
|
|
|
client.EXPECT().ApplyResourceChange(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.ApplyResourceChange_Response{
|
|
NewState: &proto.DynamicValue{
|
|
Msgpack: []byte("\x81\xa4attr\xa3bar"),
|
|
},
|
|
Private: expectedPrivate,
|
|
}, nil)
|
|
|
|
resp := p.ApplyResourceChange(providers.ApplyResourceChangeRequest{
|
|
TypeName: "resource",
|
|
PriorState: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("foo"),
|
|
}),
|
|
PlannedState: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
}),
|
|
Config: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
}),
|
|
PlannedPrivate: expectedPrivate,
|
|
})
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
expectedState := cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
})
|
|
|
|
if !cmp.Equal(expectedState, resp.NewState, typeComparer, valueComparer, equateEmpty) {
|
|
t.Fatal(cmp.Diff(expectedState, resp.NewState, typeComparer, valueComparer, equateEmpty))
|
|
}
|
|
|
|
if !bytes.Equal(expectedPrivate, resp.Private) {
|
|
t.Fatalf("expected %q, got %q", expectedPrivate, resp.Private)
|
|
}
|
|
}
|
|
func TestGRPCProvider_ApplyResourceChangeJSON(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
expectedPrivate := []byte(`{"meta": "data"}`)
|
|
|
|
client.EXPECT().ApplyResourceChange(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.ApplyResourceChange_Response{
|
|
NewState: &proto.DynamicValue{
|
|
Json: []byte(`{"attr":"bar"}`),
|
|
},
|
|
Private: expectedPrivate,
|
|
}, nil)
|
|
|
|
resp := p.ApplyResourceChange(providers.ApplyResourceChangeRequest{
|
|
TypeName: "resource",
|
|
PriorState: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("foo"),
|
|
}),
|
|
PlannedState: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
}),
|
|
Config: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
}),
|
|
PlannedPrivate: expectedPrivate,
|
|
})
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
expectedState := cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
})
|
|
|
|
if !cmp.Equal(expectedState, resp.NewState, typeComparer, valueComparer, equateEmpty) {
|
|
t.Fatal(cmp.Diff(expectedState, resp.NewState, typeComparer, valueComparer, equateEmpty))
|
|
}
|
|
|
|
if !bytes.Equal(expectedPrivate, resp.Private) {
|
|
t.Fatalf("expected %q, got %q", expectedPrivate, resp.Private)
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_ImportResourceState(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
expectedPrivate := []byte(`{"meta": "data"}`)
|
|
|
|
client.EXPECT().ImportResourceState(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.ImportResourceState_Response{
|
|
ImportedResources: []*proto.ImportResourceState_ImportedResource{
|
|
{
|
|
TypeName: "resource",
|
|
State: &proto.DynamicValue{
|
|
Msgpack: []byte("\x81\xa4attr\xa3bar"),
|
|
},
|
|
Private: expectedPrivate,
|
|
},
|
|
},
|
|
}, nil)
|
|
|
|
resp := p.ImportResourceState(providers.ImportResourceStateRequest{
|
|
TypeName: "resource",
|
|
ID: "foo",
|
|
})
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
expectedResource := providers.ImportedResource{
|
|
TypeName: "resource",
|
|
State: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
}),
|
|
Private: expectedPrivate,
|
|
}
|
|
|
|
imported := resp.ImportedResources[0]
|
|
if !cmp.Equal(expectedResource, imported, typeComparer, valueComparer, equateEmpty) {
|
|
t.Fatal(cmp.Diff(expectedResource, imported, typeComparer, valueComparer, equateEmpty))
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_ImportResourceStateJSON(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
expectedPrivate := []byte(`{"meta": "data"}`)
|
|
|
|
client.EXPECT().ImportResourceState(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.ImportResourceState_Response{
|
|
ImportedResources: []*proto.ImportResourceState_ImportedResource{
|
|
{
|
|
TypeName: "resource",
|
|
State: &proto.DynamicValue{
|
|
Json: []byte(`{"attr":"bar"}`),
|
|
},
|
|
Private: expectedPrivate,
|
|
},
|
|
},
|
|
}, nil)
|
|
|
|
resp := p.ImportResourceState(providers.ImportResourceStateRequest{
|
|
TypeName: "resource",
|
|
ID: "foo",
|
|
})
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
expectedResource := providers.ImportedResource{
|
|
TypeName: "resource",
|
|
State: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
}),
|
|
Private: expectedPrivate,
|
|
}
|
|
|
|
imported := resp.ImportedResources[0]
|
|
if !cmp.Equal(expectedResource, imported, typeComparer, valueComparer, equateEmpty) {
|
|
t.Fatal(cmp.Diff(expectedResource, imported, typeComparer, valueComparer, equateEmpty))
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_ImportResourceState_Identity(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
client.EXPECT().ImportResourceState(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.ImportResourceState_Response{
|
|
ImportedResources: []*proto.ImportResourceState_ImportedResource{
|
|
{
|
|
TypeName: "resource",
|
|
State: &proto.DynamicValue{
|
|
Msgpack: []byte("\x81\xa4attr\xa3bar"),
|
|
},
|
|
Identity: &proto.ResourceIdentityData{
|
|
IdentityData: &proto.DynamicValue{
|
|
Msgpack: []byte("\x81\xa7id_attr\xa3foo"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
|
|
resp := p.ImportResourceState(providers.ImportResourceStateRequest{
|
|
TypeName: "resource",
|
|
Identity: cty.ObjectVal(map[string]cty.Value{
|
|
"id_attr": cty.StringVal("foo"),
|
|
}),
|
|
})
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
expectedResource := providers.ImportedResource{
|
|
TypeName: "resource",
|
|
State: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
}),
|
|
Identity: cty.ObjectVal(map[string]cty.Value{
|
|
"id_attr": cty.StringVal("foo"),
|
|
}),
|
|
}
|
|
|
|
imported := resp.ImportedResources[0]
|
|
if !cmp.Equal(expectedResource, imported, typeComparer, valueComparer, equateEmpty) {
|
|
t.Fatal(cmp.Diff(expectedResource, imported, typeComparer, valueComparer, equateEmpty))
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_MoveResourceState(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
expectedTargetPrivate := []byte(`{"target": "private"}`)
|
|
expectedTargetState := cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
})
|
|
|
|
client.EXPECT().MoveResourceState(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.MoveResourceState_Response{
|
|
TargetState: &proto.DynamicValue{
|
|
Msgpack: []byte("\x81\xa4attr\xa3bar"),
|
|
},
|
|
TargetPrivate: expectedTargetPrivate,
|
|
}, nil)
|
|
|
|
resp := p.MoveResourceState(providers.MoveResourceStateRequest{
|
|
SourcePrivate: []byte(`{"source": "private"}`),
|
|
SourceStateJSON: []byte(`{"source_attr":"bar"}`),
|
|
TargetTypeName: "resource",
|
|
})
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
if !cmp.Equal(expectedTargetPrivate, resp.TargetPrivate, typeComparer, valueComparer, equateEmpty) {
|
|
t.Fatal(cmp.Diff(expectedTargetPrivate, resp.TargetPrivate, typeComparer, valueComparer, equateEmpty))
|
|
}
|
|
|
|
if !cmp.Equal(expectedTargetState, resp.TargetState, typeComparer, valueComparer, equateEmpty) {
|
|
t.Fatal(cmp.Diff(expectedTargetState, resp.TargetState, typeComparer, valueComparer, equateEmpty))
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_MoveResourceStateJSON(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
expectedTargetPrivate := []byte(`{"target": "private"}`)
|
|
expectedTargetState := cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
})
|
|
|
|
client.EXPECT().MoveResourceState(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.MoveResourceState_Response{
|
|
TargetState: &proto.DynamicValue{
|
|
Json: []byte(`{"attr":"bar"}`),
|
|
},
|
|
TargetPrivate: expectedTargetPrivate,
|
|
}, nil)
|
|
|
|
resp := p.MoveResourceState(providers.MoveResourceStateRequest{
|
|
SourcePrivate: []byte(`{"source": "private"}`),
|
|
SourceStateJSON: []byte(`{"source_attr":"bar"}`),
|
|
TargetTypeName: "resource",
|
|
})
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
if !cmp.Equal(expectedTargetPrivate, resp.TargetPrivate, typeComparer, valueComparer, equateEmpty) {
|
|
t.Fatal(cmp.Diff(expectedTargetPrivate, resp.TargetPrivate, typeComparer, valueComparer, equateEmpty))
|
|
}
|
|
|
|
if !cmp.Equal(expectedTargetState, resp.TargetState, typeComparer, valueComparer, equateEmpty) {
|
|
t.Fatal(cmp.Diff(expectedTargetState, resp.TargetState, typeComparer, valueComparer, equateEmpty))
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_ReadDataSource(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
client.EXPECT().ReadDataSource(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.ReadDataSource_Response{
|
|
State: &proto.DynamicValue{
|
|
Msgpack: []byte("\x81\xa4attr\xa3bar"),
|
|
},
|
|
}, nil)
|
|
|
|
resp := p.ReadDataSource(providers.ReadDataSourceRequest{
|
|
TypeName: "data",
|
|
Config: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("foo"),
|
|
}),
|
|
})
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
expected := cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
})
|
|
|
|
if !cmp.Equal(expected, resp.State, typeComparer, valueComparer, equateEmpty) {
|
|
t.Fatal(cmp.Diff(expected, resp.State, typeComparer, valueComparer, equateEmpty))
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_ReadDataSourceJSON(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
client.EXPECT().ReadDataSource(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.ReadDataSource_Response{
|
|
State: &proto.DynamicValue{
|
|
Json: []byte(`{"attr":"bar"}`),
|
|
},
|
|
}, nil)
|
|
|
|
resp := p.ReadDataSource(providers.ReadDataSourceRequest{
|
|
TypeName: "data",
|
|
Config: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("foo"),
|
|
}),
|
|
})
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
expected := cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
})
|
|
|
|
if !cmp.Equal(expected, resp.State, typeComparer, valueComparer, equateEmpty) {
|
|
t.Fatal(cmp.Diff(expected, resp.State, typeComparer, valueComparer, equateEmpty))
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_openEphemeralResource(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
client.EXPECT().OpenEphemeralResource(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.OpenEphemeralResource_Response{
|
|
Result: &proto.DynamicValue{
|
|
Msgpack: []byte("\x81\xa4attr\xa3bar"),
|
|
},
|
|
RenewAt: timestamppb.New(time.Now().Add(time.Second)),
|
|
Private: []byte("private data"),
|
|
}, nil)
|
|
|
|
resp := p.OpenEphemeralResource(providers.OpenEphemeralResourceRequest{
|
|
TypeName: "ephemeral",
|
|
Config: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.NullVal(cty.String),
|
|
}),
|
|
})
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
expected := cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
})
|
|
|
|
if !cmp.Equal(expected, resp.Result, typeComparer, valueComparer, equateEmpty) {
|
|
t.Fatal(cmp.Diff(expected, resp.Result, typeComparer, valueComparer, equateEmpty))
|
|
}
|
|
|
|
if !resp.RenewAt.After(time.Now()) {
|
|
t.Fatal("invalid RenewAt:", resp.RenewAt)
|
|
}
|
|
|
|
if !bytes.Equal(resp.Private, []byte("private data")) {
|
|
t.Fatalf("invalid private data: %q", resp.Private)
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_renewEphemeralResource(t *testing.T) {
|
|
client := mockproto.NewMockProviderClient(gomock.NewController(t))
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
client.EXPECT().RenewEphemeralResource(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.RenewEphemeralResource_Response{
|
|
RenewAt: timestamppb.New(time.Now().Add(time.Second)),
|
|
Private: []byte("private data"),
|
|
}, nil)
|
|
|
|
resp := p.RenewEphemeralResource(providers.RenewEphemeralResourceRequest{
|
|
TypeName: "ephemeral",
|
|
Private: []byte("private data"),
|
|
})
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
if !resp.RenewAt.After(time.Now()) {
|
|
t.Fatal("invalid RenewAt:", resp.RenewAt)
|
|
}
|
|
|
|
if !bytes.Equal(resp.Private, []byte("private data")) {
|
|
t.Fatalf("invalid private data: %q", resp.Private)
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_closeEphemeralResource(t *testing.T) {
|
|
client := mockproto.NewMockProviderClient(gomock.NewController(t))
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
client.EXPECT().CloseEphemeralResource(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.CloseEphemeralResource_Response{}, nil)
|
|
|
|
resp := p.CloseEphemeralResource(providers.CloseEphemeralResourceRequest{
|
|
TypeName: "ephemeral",
|
|
Private: []byte("private data"),
|
|
})
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
}
|
|
func TestGRPCProvider_GetSchema_ListResourceTypes(t *testing.T) {
|
|
p := &GRPCProvider{
|
|
client: mockProviderClient(t),
|
|
ctx: context.Background(),
|
|
}
|
|
|
|
resp := p.GetProviderSchema()
|
|
listResourceSchema := resp.ListResourceTypes
|
|
expected := map[string]providers.Schema{
|
|
"list": {
|
|
Version: 1,
|
|
Body: &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"data": {
|
|
Type: cty.DynamicPseudoType,
|
|
Computed: true,
|
|
},
|
|
},
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
"config": {
|
|
Block: configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"filter_attr": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
Nesting: configschema.NestingSingle,
|
|
MinItems: 1,
|
|
MaxItems: 1,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
actualBody := convert.ConfigSchemaToProto(listResourceSchema["list"].Body).String()
|
|
expectedBody := convert.ConfigSchemaToProto(expected["list"].Body).String()
|
|
if diff := cmp.Diff(expectedBody, actualBody); diff != "" {
|
|
t.Fatalf("unexpected body (-want +got):\n%s", diff)
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_Encode(t *testing.T) {
|
|
// TODO: This is the only test in this package that imports plans. If that
|
|
// ever leads to a circular import, we should consider moving this test to
|
|
// a different package or refactoring the test to not use plans.
|
|
p := &GRPCProvider{
|
|
client: mockProviderClient(t),
|
|
ctx: context.Background(),
|
|
Addr: addrs.ImpliedProviderForUnqualifiedType("testencode"),
|
|
}
|
|
resp := p.GetProviderSchema()
|
|
|
|
src := plans.NewChanges()
|
|
src.SyncWrapper().AppendResourceInstanceChange(&plans.ResourceInstanceChange{
|
|
Addr: addrs.AbsResourceInstance{
|
|
Module: addrs.RootModuleInstance,
|
|
Resource: addrs.ResourceInstance{
|
|
Resource: addrs.Resource{
|
|
Mode: addrs.ListResourceMode,
|
|
Type: "list",
|
|
Name: "test",
|
|
},
|
|
Key: addrs.NoKey,
|
|
},
|
|
},
|
|
ProviderAddr: addrs.AbsProviderConfig{
|
|
Provider: p.Addr,
|
|
},
|
|
Change: plans.Change{
|
|
Before: cty.NullVal(cty.Object(map[string]cty.Type{
|
|
"config": cty.Object(map[string]cty.Type{
|
|
"filter_attr": cty.String,
|
|
}),
|
|
"data": cty.List(cty.Object(map[string]cty.Type{
|
|
"state": cty.Object(map[string]cty.Type{
|
|
"resource_attr": cty.String,
|
|
}),
|
|
"identity": cty.Object(map[string]cty.Type{
|
|
"id_attr": cty.String,
|
|
}),
|
|
})),
|
|
})),
|
|
After: cty.ObjectVal(map[string]cty.Value{
|
|
"config": cty.ObjectVal(map[string]cty.Value{
|
|
"filter_attr": cty.StringVal("value"),
|
|
}),
|
|
"data": cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"state": cty.ObjectVal(map[string]cty.Value{
|
|
"resource_attr": cty.StringVal("value"),
|
|
}),
|
|
"identity": cty.ObjectVal(map[string]cty.Value{
|
|
"id_attr": cty.StringVal("value"),
|
|
}),
|
|
}),
|
|
}),
|
|
}),
|
|
},
|
|
})
|
|
_, err := src.Encode(&schemarepo.Schemas{
|
|
Providers: map[addrs.Provider]providers.ProviderSchema{
|
|
p.Addr: {
|
|
ResourceTypes: resp.ResourceTypes,
|
|
ListResourceTypes: resp.ListResourceTypes,
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error encoding changes: %s", err)
|
|
}
|
|
}
|
|
|
|
// Mock implementation of the ListResource stream client
|
|
type mockListResourceStreamClient struct {
|
|
events []*proto.ListResource_Event
|
|
current int
|
|
proto.Provider_ListResourceClient
|
|
}
|
|
|
|
func (m *mockListResourceStreamClient) Recv() (*proto.ListResource_Event, error) {
|
|
if m.current >= len(m.events) {
|
|
return nil, io.EOF
|
|
}
|
|
|
|
event := m.events[m.current]
|
|
m.current++
|
|
return event, nil
|
|
}
|
|
|
|
func TestGRPCProvider_ListResource(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
ctx: context.Background(),
|
|
}
|
|
|
|
// Create a mock stream client that will return resource events
|
|
mockStream := &mockListResourceStreamClient{
|
|
events: []*proto.ListResource_Event{
|
|
{
|
|
DisplayName: "Test Resource 1",
|
|
Identity: &proto.ResourceIdentityData{
|
|
IdentityData: &proto.DynamicValue{
|
|
Msgpack: []byte("\x81\xa7id_attr\xa4id-1"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
DisplayName: "Test Resource 2",
|
|
Identity: &proto.ResourceIdentityData{
|
|
IdentityData: &proto.DynamicValue{
|
|
Msgpack: []byte("\x81\xa7id_attr\xa4id-2"),
|
|
},
|
|
},
|
|
ResourceObject: &proto.DynamicValue{
|
|
Msgpack: []byte("\x81\xadresource_attr\xa5value"),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
client.EXPECT().ListResource(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(mockStream, nil)
|
|
|
|
// Create the request
|
|
configVal := cty.ObjectVal(map[string]cty.Value{
|
|
"config": cty.ObjectVal(map[string]cty.Value{
|
|
"filter_attr": cty.StringVal("filter-value"),
|
|
}),
|
|
})
|
|
request := providers.ListResourceRequest{
|
|
TypeName: "list",
|
|
Config: configVal,
|
|
IncludeResourceObject: true,
|
|
Limit: 100,
|
|
}
|
|
|
|
resp := p.ListResource(request)
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
data := resp.Result.AsValueMap()
|
|
if _, ok := data["data"]; !ok {
|
|
t.Fatal("Expected 'data' key in result")
|
|
}
|
|
// Verify that we received both events
|
|
if len(data["data"].AsValueSlice()) != 2 {
|
|
t.Fatalf("Expected 2 resources, got %d", len(data["data"].AsValueSlice()))
|
|
}
|
|
results := data["data"].AsValueSlice()
|
|
|
|
// Verify first event
|
|
displayName := results[0].GetAttr("display_name")
|
|
if displayName.AsString() != "Test Resource 1" {
|
|
t.Errorf("Expected DisplayName 'Test Resource 1', got '%s'", displayName.AsString())
|
|
}
|
|
|
|
expectedId1 := cty.ObjectVal(map[string]cty.Value{
|
|
"id_attr": cty.StringVal("id-1"),
|
|
})
|
|
|
|
identity := results[0].GetAttr("identity")
|
|
if !identity.RawEquals(expectedId1) {
|
|
t.Errorf("Expected Identity %#v, got %#v", expectedId1, identity)
|
|
}
|
|
|
|
// ResourceObject should be null for the first event as it wasn't provided
|
|
resourceObject := results[0].GetAttr("state")
|
|
if !resourceObject.IsNull() {
|
|
t.Errorf("Expected ResourceObject to be null, got %#v", resourceObject)
|
|
}
|
|
|
|
// Verify second event
|
|
displayName = results[1].GetAttr("display_name")
|
|
if displayName.AsString() != "Test Resource 2" {
|
|
t.Errorf("Expected DisplayName 'Test Resource 2', got '%s'", displayName.AsString())
|
|
}
|
|
|
|
expectedId2 := cty.ObjectVal(map[string]cty.Value{
|
|
"id_attr": cty.StringVal("id-2"),
|
|
})
|
|
identity = results[1].GetAttr("identity")
|
|
if !identity.RawEquals(expectedId2) {
|
|
t.Errorf("Expected Identity %#v, got %#v", expectedId2, identity)
|
|
}
|
|
|
|
expectedResource := cty.ObjectVal(map[string]cty.Value{
|
|
"resource_attr": cty.StringVal("value"),
|
|
})
|
|
resourceObject = results[1].GetAttr("state")
|
|
if !resourceObject.RawEquals(expectedResource) {
|
|
t.Errorf("Expected ResourceObject %#v, got %#v", expectedResource, resourceObject)
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_ListResource_Error(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
ctx: context.Background(),
|
|
}
|
|
|
|
// Test case where the provider returns an error
|
|
client.EXPECT().ListResource(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(nil, fmt.Errorf("provider error"))
|
|
|
|
configVal := cty.ObjectVal(map[string]cty.Value{
|
|
"config": cty.ObjectVal(map[string]cty.Value{
|
|
"filter_attr": cty.StringVal("filter-value"),
|
|
}),
|
|
})
|
|
request := providers.ListResourceRequest{
|
|
TypeName: "list",
|
|
Config: configVal,
|
|
}
|
|
|
|
resp := p.ListResource(request)
|
|
checkDiagsHasError(t, resp.Diagnostics)
|
|
}
|
|
|
|
func TestGRPCProvider_ListResource_Diagnostics(t *testing.T) {
|
|
configVal := cty.ObjectVal(map[string]cty.Value{
|
|
"config": cty.ObjectVal(map[string]cty.Value{
|
|
"filter_attr": cty.StringVal("filter-value"),
|
|
}),
|
|
})
|
|
request := providers.ListResourceRequest{
|
|
TypeName: "list",
|
|
Config: configVal,
|
|
Limit: 100,
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
events []*proto.ListResource_Event
|
|
expectedCount int
|
|
expectedDiags int
|
|
expectedWarns int // subset of expectedDiags
|
|
}{
|
|
{
|
|
"no events",
|
|
[]*proto.ListResource_Event{},
|
|
0,
|
|
0,
|
|
0,
|
|
},
|
|
{
|
|
"single event no diagnostics",
|
|
[]*proto.ListResource_Event{
|
|
{
|
|
DisplayName: "Test Resource",
|
|
Identity: &proto.ResourceIdentityData{
|
|
IdentityData: &proto.DynamicValue{
|
|
Msgpack: []byte("\x81\xa7id_attr\xa4id-1"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
1,
|
|
0,
|
|
0,
|
|
},
|
|
{
|
|
"event with warning",
|
|
[]*proto.ListResource_Event{
|
|
{
|
|
DisplayName: "Test Resource",
|
|
Identity: &proto.ResourceIdentityData{
|
|
IdentityData: &proto.DynamicValue{
|
|
Msgpack: []byte("\x81\xa7id_attr\xa4id-1"),
|
|
},
|
|
},
|
|
Diagnostic: []*proto.Diagnostic{
|
|
{
|
|
Severity: proto.Diagnostic_WARNING,
|
|
Summary: "Test warning",
|
|
Detail: "Warning detail",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
1,
|
|
1,
|
|
1,
|
|
},
|
|
{
|
|
"only a warning",
|
|
[]*proto.ListResource_Event{
|
|
{
|
|
Diagnostic: []*proto.Diagnostic{
|
|
{
|
|
Severity: proto.Diagnostic_WARNING,
|
|
Summary: "Test warning",
|
|
Detail: "Warning detail",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
0,
|
|
1,
|
|
1,
|
|
},
|
|
{
|
|
"only an error",
|
|
[]*proto.ListResource_Event{
|
|
{
|
|
Diagnostic: []*proto.Diagnostic{
|
|
{
|
|
Severity: proto.Diagnostic_ERROR,
|
|
Summary: "Test error",
|
|
Detail: "Error detail",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
0,
|
|
1,
|
|
0,
|
|
},
|
|
{
|
|
"event with error",
|
|
[]*proto.ListResource_Event{
|
|
{
|
|
DisplayName: "Test Resource",
|
|
Identity: &proto.ResourceIdentityData{
|
|
IdentityData: &proto.DynamicValue{
|
|
Msgpack: []byte("\x81\xa7id_attr\xa4id-1"),
|
|
},
|
|
},
|
|
Diagnostic: []*proto.Diagnostic{
|
|
{
|
|
Severity: proto.Diagnostic_ERROR,
|
|
Summary: "Test error",
|
|
Detail: "Error detail",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
0,
|
|
1,
|
|
0,
|
|
},
|
|
{
|
|
"multiple events mixed diagnostics",
|
|
[]*proto.ListResource_Event{
|
|
{
|
|
DisplayName: "Resource 1",
|
|
Identity: &proto.ResourceIdentityData{
|
|
IdentityData: &proto.DynamicValue{
|
|
Msgpack: []byte("\x81\xa7id_attr\xa4id-1"),
|
|
},
|
|
},
|
|
Diagnostic: []*proto.Diagnostic{
|
|
{
|
|
Severity: proto.Diagnostic_WARNING,
|
|
Summary: "Warning 1",
|
|
Detail: "Warning detail 1",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
DisplayName: "Resource 2",
|
|
Identity: &proto.ResourceIdentityData{
|
|
IdentityData: &proto.DynamicValue{
|
|
Msgpack: []byte("\x81\xa7id_attr\xa4id-2"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
DisplayName: "Resource 3",
|
|
Identity: &proto.ResourceIdentityData{
|
|
IdentityData: &proto.DynamicValue{
|
|
Msgpack: []byte("\x81\xa7id_attr\xa4id-3"),
|
|
},
|
|
},
|
|
Diagnostic: []*proto.Diagnostic{
|
|
{
|
|
Severity: proto.Diagnostic_ERROR,
|
|
Summary: "Error 1",
|
|
Detail: "Error detail 1",
|
|
},
|
|
{
|
|
Severity: proto.Diagnostic_WARNING,
|
|
Summary: "Warning 2",
|
|
Detail: "Warning detail 2",
|
|
},
|
|
},
|
|
},
|
|
{ // This event will never be reached
|
|
DisplayName: "Resource 4",
|
|
Identity: &proto.ResourceIdentityData{
|
|
IdentityData: &proto.DynamicValue{
|
|
Msgpack: []byte("\x81\xa7id_attr\xa4id-4"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
2,
|
|
3,
|
|
2,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
ctx: context.Background(),
|
|
}
|
|
|
|
mockStream := &mockListResourceStreamClient{
|
|
events: tc.events,
|
|
}
|
|
|
|
client.EXPECT().ListResource(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(mockStream, nil)
|
|
|
|
resp := p.ListResource(request)
|
|
|
|
result := resp.Result.AsValueMap()
|
|
nResults := result["data"].LengthInt()
|
|
if nResults != tc.expectedCount {
|
|
t.Fatalf("Expected %d results, got %d", tc.expectedCount, nResults)
|
|
}
|
|
|
|
nDiagnostics := len(resp.Diagnostics)
|
|
if nDiagnostics != tc.expectedDiags {
|
|
t.Fatalf("Expected %d diagnostics, got %d", tc.expectedDiags, nDiagnostics)
|
|
}
|
|
|
|
nWarnings := len(resp.Diagnostics.Warnings())
|
|
if nWarnings != tc.expectedWarns {
|
|
t.Fatalf("Expected %d warnings, got %d", tc.expectedWarns, nWarnings)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_ListResource_Limit(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
ctx: context.Background(),
|
|
}
|
|
|
|
// Create a mock stream client that will return resource events
|
|
mockStream := &mockListResourceStreamClient{
|
|
events: []*proto.ListResource_Event{
|
|
{
|
|
DisplayName: "Test Resource 1",
|
|
Identity: &proto.ResourceIdentityData{
|
|
IdentityData: &proto.DynamicValue{
|
|
Msgpack: []byte("\x81\xa7id_attr\xa4id-1"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
DisplayName: "Test Resource 2",
|
|
Identity: &proto.ResourceIdentityData{
|
|
IdentityData: &proto.DynamicValue{
|
|
Msgpack: []byte("\x81\xa7id_attr\xa4id-2"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
DisplayName: "Test Resource 3",
|
|
Identity: &proto.ResourceIdentityData{
|
|
IdentityData: &proto.DynamicValue{
|
|
Msgpack: []byte("\x81\xa7id_attr\xa4id-3"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
client.EXPECT().ListResource(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(mockStream, nil)
|
|
|
|
// Create the request
|
|
configVal := cty.ObjectVal(map[string]cty.Value{
|
|
"config": cty.ObjectVal(map[string]cty.Value{
|
|
"filter_attr": cty.StringVal("filter-value"),
|
|
}),
|
|
})
|
|
request := providers.ListResourceRequest{
|
|
TypeName: "list",
|
|
Config: configVal,
|
|
Limit: 2,
|
|
}
|
|
|
|
resp := p.ListResource(request)
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
data := resp.Result.AsValueMap()
|
|
if _, ok := data["data"]; !ok {
|
|
t.Fatal("Expected 'data' key in result")
|
|
}
|
|
// Verify that we received both events
|
|
if len(data["data"].AsValueSlice()) != 2 {
|
|
t.Fatalf("Expected 2 resources, got %d", len(data["data"].AsValueSlice()))
|
|
}
|
|
results := data["data"].AsValueSlice()
|
|
|
|
// Verify that we received both events
|
|
if len(results) != 2 {
|
|
t.Fatalf("Expected 2 events, got %d", len(results))
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_planAction_valid(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
ctx: context.Background(),
|
|
}
|
|
|
|
client.EXPECT().PlanAction(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.PlanAction_Response{}, nil)
|
|
|
|
resp := p.PlanAction(providers.PlanActionRequest{
|
|
ActionType: "action",
|
|
ProposedActionData: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("foo"),
|
|
}),
|
|
})
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
}
|
|
|
|
func TestGRPCProvider_planAction_valid_but_fails(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
client.EXPECT().PlanAction(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.PlanAction_Response{
|
|
Diagnostics: []*proto.Diagnostic{
|
|
{
|
|
Severity: proto.Diagnostic_ERROR,
|
|
Summary: "Boom",
|
|
Detail: "Explosion",
|
|
},
|
|
},
|
|
}, nil)
|
|
|
|
resp := p.PlanAction(providers.PlanActionRequest{
|
|
ActionType: "action",
|
|
ProposedActionData: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("foo"),
|
|
}),
|
|
})
|
|
|
|
checkDiagsHasError(t, resp.Diagnostics)
|
|
}
|
|
|
|
func TestGRPCProvider_planAction_invalid_config(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
resp := p.PlanAction(providers.PlanActionRequest{
|
|
ActionType: "action",
|
|
ProposedActionData: cty.ObjectVal(map[string]cty.Value{
|
|
"not_the_right_attr": cty.StringVal("foo"),
|
|
}),
|
|
})
|
|
|
|
checkDiagsHasError(t, resp.Diagnostics)
|
|
}
|
|
|
|
func TestGRPCProvider_invokeAction_valid(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
mockInvokeClient := mockproto.NewMockProvider_InvokeActionClient[tfplugin6.InvokeAction_Event](ctrl)
|
|
mockInvokeClient.EXPECT().Recv().Return(&proto.InvokeAction_Event{
|
|
Type: &proto.InvokeAction_Event_Progress_{
|
|
Progress: &proto.InvokeAction_Event_Progress{
|
|
Message: "Hello from the action",
|
|
},
|
|
},
|
|
}, nil)
|
|
mockInvokeClient.EXPECT().Recv().Return(&proto.InvokeAction_Event{
|
|
Type: &proto.InvokeAction_Event_Completed_{
|
|
Completed: &proto.InvokeAction_Event_Completed{},
|
|
},
|
|
}, nil)
|
|
mockInvokeClient.EXPECT().Recv().Return(nil, io.EOF)
|
|
|
|
client.EXPECT().InvokeAction(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(mockInvokeClient, nil)
|
|
|
|
resp := p.InvokeAction(providers.InvokeActionRequest{
|
|
ActionType: "action",
|
|
PlannedActionData: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("foo"),
|
|
}),
|
|
})
|
|
|
|
evts := []providers.InvokeActionEvent{}
|
|
for e := range resp.Events {
|
|
evts = append(evts, e)
|
|
}
|
|
|
|
if len(evts) != 2 {
|
|
t.Fatalf("expected 2 events, got %d", len(evts))
|
|
}
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
}
|
|
|
|
func TestGRPCProvider_invokeAction_invalid(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
resp := p.InvokeAction(providers.InvokeActionRequest{
|
|
ActionType: "action",
|
|
PlannedActionData: cty.ObjectVal(map[string]cty.Value{
|
|
"not-defined": cty.StringVal("foo"),
|
|
}),
|
|
})
|
|
|
|
checkDiagsHasError(t, resp.Diagnostics)
|
|
}
|
|
|
|
func TestGRPCProvider_ValidateStateStoreConfig_returns_validation_errors(t *testing.T) {
|
|
storeName := "mock_store" // mockProviderClient returns a mock that has this state store in its schemas
|
|
|
|
t.Run("no validation error raised", func(t *testing.T) {
|
|
typeName := storeName
|
|
var diagnostic []*proto.Diagnostic = nil
|
|
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
ctx: context.Background(),
|
|
}
|
|
|
|
client.EXPECT().ValidateStateStoreConfig(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.ValidateStateStore_Response{
|
|
Diagnostics: diagnostic,
|
|
}, nil)
|
|
|
|
request := providers.ValidateStateStoreConfigRequest{
|
|
TypeName: typeName,
|
|
Config: cty.ObjectVal(map[string]cty.Value{
|
|
"region": cty.StringVal("neptune"),
|
|
}),
|
|
}
|
|
|
|
// Act
|
|
resp := p.ValidateStateStoreConfig(request)
|
|
|
|
// Assert no error returned
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
})
|
|
|
|
t.Run("validation error raised", func(t *testing.T) {
|
|
typeName := storeName
|
|
diagnostic := []*proto.Diagnostic{
|
|
{
|
|
Severity: proto.Diagnostic_ERROR,
|
|
Summary: "Error from ValidateStateStoreConfig",
|
|
Detail: "Something went wrong",
|
|
},
|
|
}
|
|
errorText := "Error from ValidateStateStoreConfig"
|
|
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
ctx: context.Background(),
|
|
}
|
|
|
|
client.EXPECT().ValidateStateStoreConfig(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.ValidateStateStore_Response{
|
|
Diagnostics: diagnostic,
|
|
}, nil)
|
|
|
|
request := providers.ValidateStateStoreConfigRequest{
|
|
TypeName: typeName,
|
|
Config: cty.ObjectVal(map[string]cty.Value{
|
|
"region": cty.StringVal("neptune"),
|
|
}),
|
|
}
|
|
|
|
// Act
|
|
resp := p.ValidateStateStoreConfig(request)
|
|
|
|
// Assert error returned
|
|
checkDiagsHasError(t, resp.Diagnostics)
|
|
if resp.Diagnostics[0].Description().Summary != errorText {
|
|
t.Fatalf("expected error summary to be %q, but got %q",
|
|
errorText,
|
|
resp.Diagnostics[0].Description().Summary,
|
|
)
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
func TestGRPCProvider_ValidateStateStoreConfig_schema_errors(t *testing.T) {
|
|
t.Run("no matching store type in provider", func(t *testing.T) {
|
|
typeName := "does_not_exist" // not present in mockProviderClient state store schemas
|
|
config := cty.EmptyObjectVal
|
|
expectedErrorSummary := "unknown state store type \"does_not_exist\""
|
|
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
ctx: context.Background(),
|
|
}
|
|
|
|
request := providers.ValidateStateStoreConfigRequest{
|
|
TypeName: typeName,
|
|
Config: config,
|
|
}
|
|
|
|
// Act
|
|
resp := p.ValidateStateStoreConfig(request)
|
|
|
|
// Note - we haven't asserted that we expect ValidateStateStoreConfig
|
|
// to be called via the client; this package returns these errors before then.
|
|
|
|
// Assert that the expected error is returned
|
|
checkDiagsHasError(t, resp.Diagnostics)
|
|
if resp.Diagnostics[0].Description().Summary != expectedErrorSummary {
|
|
t.Fatalf("expected error summary to be %q, but got %q",
|
|
expectedErrorSummary,
|
|
resp.Diagnostics[0].Description().Summary,
|
|
)
|
|
}
|
|
})
|
|
|
|
t.Run("missing required attributes", func(t *testing.T) {
|
|
typeName := "mock_store" // Is present in mockProviderClient
|
|
config := cty.ObjectVal(map[string]cty.Value{
|
|
// Missing required `region` attr
|
|
})
|
|
expectedErrorSummary := "attribute \"region\" is required"
|
|
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
ctx: context.Background(),
|
|
}
|
|
|
|
request := providers.ValidateStateStoreConfigRequest{
|
|
TypeName: typeName,
|
|
Config: config,
|
|
}
|
|
|
|
// Act
|
|
resp := p.ValidateStateStoreConfig(request)
|
|
|
|
// Note - we haven't asserted that we expect ValidateStateStoreConfig
|
|
// to be called via the client; this package returns these errors before then.
|
|
|
|
// Assert that the expected error is returned
|
|
checkDiagsHasError(t, resp.Diagnostics)
|
|
if resp.Diagnostics[0].Description().Summary != expectedErrorSummary {
|
|
t.Fatalf("expected error summary to be %q, but got %q",
|
|
expectedErrorSummary,
|
|
resp.Diagnostics[0].Description().Summary,
|
|
)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGRPCProvider_ConfigureStateStore_returns_validation_errors(t *testing.T) {
|
|
storeName := "mock_store" // mockProviderClient returns a mock that has this state store in its schemas
|
|
chunkSize := 4 << 20 // 4MB
|
|
|
|
t.Run("no validation error raised", func(t *testing.T) {
|
|
typeName := storeName
|
|
var diagnostic []*proto.Diagnostic = nil
|
|
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
ctx: context.Background(),
|
|
}
|
|
|
|
client.EXPECT().ConfigureStateStore(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.ConfigureStateStore_Response{
|
|
Capabilities: &proto.StateStoreServerCapabilities{
|
|
ChunkSize: int64(chunkSize),
|
|
},
|
|
Diagnostics: diagnostic,
|
|
}, nil)
|
|
|
|
request := providers.ConfigureStateStoreRequest{
|
|
TypeName: typeName,
|
|
Config: cty.ObjectVal(map[string]cty.Value{
|
|
"region": cty.StringVal("neptune"),
|
|
}),
|
|
Capabilities: providers.StateStoreClientCapabilities{
|
|
ChunkSize: int64(chunkSize),
|
|
},
|
|
}
|
|
|
|
// Act
|
|
resp := p.ConfigureStateStore(request)
|
|
|
|
// Assert no error returned
|
|
checkDiags(t, resp.Diagnostics)
|
|
})
|
|
|
|
t.Run("validation error raised", func(t *testing.T) {
|
|
typeName := storeName
|
|
diagnostic := []*proto.Diagnostic{
|
|
{
|
|
Severity: proto.Diagnostic_ERROR,
|
|
Summary: "Error from ConfigureStateStore",
|
|
Detail: "Something went wrong",
|
|
},
|
|
}
|
|
errorText := "Error from ConfigureStateStore"
|
|
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
ctx: context.Background(),
|
|
}
|
|
|
|
client.EXPECT().ConfigureStateStore(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.ConfigureStateStore_Response{
|
|
Capabilities: &proto.StateStoreServerCapabilities{
|
|
ChunkSize: int64(chunkSize),
|
|
},
|
|
Diagnostics: diagnostic,
|
|
}, nil)
|
|
|
|
request := providers.ConfigureStateStoreRequest{
|
|
TypeName: typeName,
|
|
Config: cty.ObjectVal(map[string]cty.Value{
|
|
"region": cty.StringVal("neptune"),
|
|
}),
|
|
Capabilities: providers.StateStoreClientCapabilities{
|
|
ChunkSize: int64(chunkSize),
|
|
},
|
|
}
|
|
|
|
// Act
|
|
resp := p.ConfigureStateStore(request)
|
|
|
|
// Assert whether error returned or not
|
|
checkDiagsHasError(t, resp.Diagnostics)
|
|
if resp.Diagnostics[0].Description().Summary != errorText {
|
|
t.Fatalf("expected error summary to be %q, but got %q",
|
|
errorText,
|
|
resp.Diagnostics[0].Description().Summary,
|
|
)
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
func TestGRPCProvider_ConfigureStateStore_schema_errors(t *testing.T) {
|
|
t.Run("no matching store type in provider", func(t *testing.T) {
|
|
typeName := "does_not_exist" // not present in mockProviderClient state store schemas
|
|
config := cty.EmptyObjectVal
|
|
expectedErrorSummary := "unknown state store type \"does_not_exist\""
|
|
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
ctx: context.Background(),
|
|
}
|
|
|
|
request := providers.ConfigureStateStoreRequest{
|
|
TypeName: typeName,
|
|
Config: config,
|
|
}
|
|
|
|
// Act
|
|
resp := p.ConfigureStateStore(request)
|
|
|
|
// Note - we haven't asserted that we expect ConfigureStateStore
|
|
// to be called via the client; this package returns these errors before then.
|
|
|
|
// Assert that the expected error is returned
|
|
checkDiagsHasError(t, resp.Diagnostics)
|
|
if resp.Diagnostics[0].Description().Summary != expectedErrorSummary {
|
|
t.Fatalf("expected error summary to be %q, but got %q",
|
|
expectedErrorSummary,
|
|
resp.Diagnostics[0].Description().Summary,
|
|
)
|
|
}
|
|
})
|
|
|
|
t.Run("missing required attributes", func(t *testing.T) {
|
|
typeName := "mock_store" // Is present in mockProviderClient
|
|
config := cty.ObjectVal(map[string]cty.Value{
|
|
// Missing required `region` attr
|
|
})
|
|
expectedErrorSummary := "attribute \"region\" is required"
|
|
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
ctx: context.Background(),
|
|
}
|
|
|
|
request := providers.ConfigureStateStoreRequest{
|
|
TypeName: typeName,
|
|
Config: config,
|
|
}
|
|
|
|
// Act
|
|
resp := p.ConfigureStateStore(request)
|
|
|
|
// Note - we haven't asserted that we expect ConfigureStateStore
|
|
// to be called via the client; this package returns these errors before then.
|
|
|
|
// Assert that the expected error is returned
|
|
checkDiagsHasError(t, resp.Diagnostics)
|
|
if resp.Diagnostics[0].Description().Summary != expectedErrorSummary {
|
|
t.Fatalf("expected error summary to be %q, but got %q",
|
|
expectedErrorSummary,
|
|
resp.Diagnostics[0].Description().Summary,
|
|
)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGRPCProvider_GetStates(t *testing.T) {
|
|
t.Run("returns expected values", func(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
ctx: context.Background(),
|
|
}
|
|
|
|
client.EXPECT().GetStates(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.GetStates_Response{
|
|
StateId: []string{"default"},
|
|
Diagnostics: []*proto.Diagnostic{
|
|
{
|
|
Severity: proto.Diagnostic_ERROR,
|
|
Summary: "Error from GetStates",
|
|
Detail: "Something went wrong",
|
|
},
|
|
},
|
|
}, nil)
|
|
|
|
request := providers.GetStatesRequest{
|
|
TypeName: "mock_store",
|
|
}
|
|
|
|
// Act
|
|
resp := p.GetStates(request)
|
|
|
|
// Assert returned values
|
|
if len(resp.States) != 1 || resp.States[0] != "default" {
|
|
t.Fatalf("expected the returned states to be [\"default\"], instead got: %s", resp.States)
|
|
}
|
|
|
|
checkDiagsHasError(t, resp.Diagnostics)
|
|
expectedErrorSummary := "Error from GetStates"
|
|
if resp.Diagnostics[0].Description().Summary != expectedErrorSummary {
|
|
t.Fatalf("expected error summary to be %q, but got %q",
|
|
expectedErrorSummary,
|
|
resp.Diagnostics[0].Description().Summary,
|
|
)
|
|
}
|
|
})
|
|
|
|
t.Run("no matching store type in provider", func(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
ctx: context.Background(),
|
|
}
|
|
|
|
request := providers.GetStatesRequest{
|
|
TypeName: "does_not_exist", // not present in mockProviderClient state store schemas
|
|
}
|
|
|
|
// Act
|
|
resp := p.GetStates(request)
|
|
|
|
checkDiagsHasError(t, resp.Diagnostics)
|
|
expectedErrorSummary := "unknown state store type \"does_not_exist\""
|
|
if resp.Diagnostics[0].Description().Summary != expectedErrorSummary {
|
|
t.Fatalf("expected error summary to be %q, but got %q",
|
|
expectedErrorSummary,
|
|
resp.Diagnostics[0].Description().Summary,
|
|
)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGRPCProvider_DeleteState(t *testing.T) {
|
|
t.Run("returns expected values", func(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
ctx: context.Background(),
|
|
}
|
|
|
|
client.EXPECT().DeleteState(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.DeleteState_Response{
|
|
Diagnostics: []*proto.Diagnostic{
|
|
{
|
|
Severity: proto.Diagnostic_ERROR,
|
|
Summary: "Error from DeleteState",
|
|
Detail: "Something went wrong",
|
|
},
|
|
},
|
|
}, nil)
|
|
|
|
request := providers.DeleteStateRequest{
|
|
TypeName: "mock_store",
|
|
}
|
|
|
|
// Act
|
|
resp := p.DeleteState(request)
|
|
|
|
// Assert returned values
|
|
checkDiagsHasError(t, resp.Diagnostics)
|
|
expectedErrorSummary := "Error from DeleteState"
|
|
if resp.Diagnostics[0].Description().Summary != expectedErrorSummary {
|
|
t.Fatalf("expected error summary to be %q, but got %q",
|
|
expectedErrorSummary,
|
|
resp.Diagnostics[0].Description().Summary,
|
|
)
|
|
}
|
|
})
|
|
|
|
t.Run("no matching store type in provider", func(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
ctx: context.Background(),
|
|
}
|
|
|
|
request := providers.DeleteStateRequest{
|
|
TypeName: "does_not_exist", // not present in mockProviderClient state store schemas
|
|
}
|
|
|
|
// Act
|
|
resp := p.DeleteState(request)
|
|
|
|
checkDiagsHasError(t, resp.Diagnostics)
|
|
expectedErrorSummary := "unknown state store type \"does_not_exist\""
|
|
if resp.Diagnostics[0].Description().Summary != expectedErrorSummary {
|
|
t.Fatalf("expected error summary to be %q, but got %q",
|
|
expectedErrorSummary,
|
|
resp.Diagnostics[0].Description().Summary,
|
|
)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGRPCProvider_ReadStateBytes(t *testing.T) {
|
|
chunkSize := 4 << 20 // 4MB
|
|
|
|
t.Run("can process multiple chunks", func(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
ctx: context.Background(),
|
|
}
|
|
p.SetStateStoreChunkSize("mock_store", 5)
|
|
|
|
// Call to ReadStateBytes
|
|
// > Assert the arguments received
|
|
// > Define the returned mock client
|
|
expectedReq := &proto.ReadStateBytes_Request{
|
|
TypeName: "mock_store",
|
|
StateId: backend.DefaultStateName,
|
|
}
|
|
mockReadBytesClient := mockReadStateBytesClient(t)
|
|
client.EXPECT().ReadStateBytes(
|
|
gomock.Any(),
|
|
gomock.Eq(expectedReq),
|
|
gomock.Any(),
|
|
).Return(mockReadBytesClient, nil)
|
|
|
|
// Define what will be returned by each call to Recv
|
|
chunks := []string{"hello", "world"}
|
|
totalLength := len(chunks[0]) + len(chunks[1])
|
|
mockResp := map[int]struct {
|
|
resp *proto.ReadStateBytes_Response
|
|
err error
|
|
}{
|
|
0: {
|
|
resp: &proto.ReadStateBytes_Response{
|
|
Bytes: []byte(chunks[0]),
|
|
TotalLength: int64(totalLength),
|
|
Range: &proto.StateRange{
|
|
Start: 0,
|
|
End: int64(len(chunks[0])) - 1,
|
|
},
|
|
},
|
|
err: nil,
|
|
},
|
|
1: {
|
|
resp: &proto.ReadStateBytes_Response{
|
|
Bytes: []byte(chunks[1]),
|
|
TotalLength: int64(totalLength),
|
|
Range: &proto.StateRange{
|
|
Start: int64(len(chunks[0])),
|
|
End: int64(len(chunks[1])) - 1,
|
|
},
|
|
},
|
|
err: nil,
|
|
},
|
|
2: {
|
|
resp: &proto.ReadStateBytes_Response{},
|
|
err: io.EOF,
|
|
},
|
|
}
|
|
var count int
|
|
mockReadBytesClient.EXPECT().Recv().DoAndReturn(func() (*proto.ReadStateBytes_Response, error) {
|
|
ret := mockResp[count]
|
|
count++
|
|
return ret.resp, ret.err
|
|
}).Times(3)
|
|
|
|
// There will be a call to CloseSend to close the stream
|
|
mockReadBytesClient.EXPECT().CloseSend().Return(nil).Times(1)
|
|
|
|
// Act
|
|
request := providers.ReadStateBytesRequest{
|
|
TypeName: expectedReq.TypeName,
|
|
StateId: expectedReq.StateId,
|
|
}
|
|
resp := p.ReadStateBytes(request)
|
|
|
|
// Assert returned values
|
|
checkDiags(t, resp.Diagnostics)
|
|
if string(resp.Bytes) != "helloworld" {
|
|
t.Fatalf("expected data to be %q, got: %q", "helloworld", string(resp.Bytes))
|
|
}
|
|
})
|
|
|
|
t.Run("can process multiple chunks when last chunk size is one byte", func(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
ctx: context.Background(),
|
|
}
|
|
p.SetStateStoreChunkSize("mock_store", 5)
|
|
|
|
// Call to ReadStateBytes
|
|
// > Assert the arguments received
|
|
// > Define the returned mock client
|
|
mockReadBytesClient := mockReadStateBytesClient(t)
|
|
|
|
expectedReq := &proto.ReadStateBytes_Request{
|
|
TypeName: "mock_store",
|
|
StateId: backend.DefaultStateName,
|
|
}
|
|
client.EXPECT().ReadStateBytes(
|
|
gomock.Any(),
|
|
gomock.Eq(expectedReq),
|
|
gomock.Any(),
|
|
).Return(mockReadBytesClient, nil)
|
|
|
|
// Define what will be returned by each call to Recv
|
|
chunk := "helloworld!"
|
|
totalLength := len(chunk)
|
|
mockResp := map[int]struct {
|
|
resp *proto.ReadStateBytes_Response
|
|
err error
|
|
}{
|
|
0: {
|
|
resp: &proto.ReadStateBytes_Response{
|
|
Bytes: []byte(chunk[:5]),
|
|
TotalLength: int64(totalLength),
|
|
Range: &proto.StateRange{
|
|
Start: 0,
|
|
End: 4,
|
|
},
|
|
Diagnostics: []*proto.Diagnostic{},
|
|
},
|
|
err: nil,
|
|
},
|
|
1: {
|
|
resp: &proto.ReadStateBytes_Response{
|
|
Bytes: []byte(chunk[5:10]),
|
|
TotalLength: int64(totalLength),
|
|
Range: &proto.StateRange{
|
|
Start: 5,
|
|
End: 9,
|
|
},
|
|
Diagnostics: []*proto.Diagnostic{},
|
|
},
|
|
err: nil,
|
|
},
|
|
2: {
|
|
resp: &proto.ReadStateBytes_Response{
|
|
Bytes: []byte(chunk[10:]),
|
|
TotalLength: int64(totalLength),
|
|
Range: &proto.StateRange{
|
|
Start: 10,
|
|
End: 10,
|
|
},
|
|
Diagnostics: []*proto.Diagnostic{},
|
|
},
|
|
err: nil,
|
|
},
|
|
3: {
|
|
resp: &proto.ReadStateBytes_Response{},
|
|
err: io.EOF,
|
|
},
|
|
}
|
|
var count int
|
|
mockReadBytesClient.EXPECT().Recv().DoAndReturn(func() (*proto.ReadStateBytes_Response, error) {
|
|
ret := mockResp[count]
|
|
count++
|
|
return ret.resp, ret.err
|
|
}).Times(4)
|
|
|
|
// There will be a call to CloseSend to close the stream
|
|
mockReadBytesClient.EXPECT().CloseSend().Return(nil).Times(1)
|
|
|
|
// Act
|
|
request := providers.ReadStateBytesRequest{
|
|
TypeName: expectedReq.TypeName,
|
|
StateId: expectedReq.StateId,
|
|
}
|
|
resp := p.ReadStateBytes(request)
|
|
|
|
// Assert returned values
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
// Chunk size mismatches are warnings so ensure there aren't any
|
|
if resp.Diagnostics.HasWarnings() {
|
|
t.Fatal(resp.Diagnostics.ErrWithWarnings())
|
|
}
|
|
|
|
if string(resp.Bytes) != "helloworld!" {
|
|
t.Fatalf("expected data to be %q, got: %q", "helloworld!", string(resp.Bytes))
|
|
}
|
|
})
|
|
|
|
t.Run("an error diagnostic is returned when final length does not match expectations", func(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
ctx: context.Background(),
|
|
}
|
|
p.SetStateStoreChunkSize("mock_store", 5)
|
|
|
|
// Call to ReadStateBytes
|
|
// > Assert the arguments received
|
|
// > Define the returned mock client
|
|
mockReadBytesClient := mockReadStateBytesClient(t)
|
|
expectedReq := &proto.ReadStateBytes_Request{
|
|
TypeName: "mock_store",
|
|
StateId: backend.DefaultStateName,
|
|
}
|
|
client.EXPECT().ReadStateBytes(
|
|
gomock.Any(),
|
|
gomock.Eq(expectedReq),
|
|
gomock.Any(),
|
|
).Return(mockReadBytesClient, nil)
|
|
|
|
// Define what will be returned by each call to Recv
|
|
chunks := []string{"hello", "world"}
|
|
var incorrectLength int64 = 999
|
|
correctLength := len(chunks[0]) + len(chunks[1])
|
|
mockResp := map[int]struct {
|
|
resp *proto.ReadStateBytes_Response
|
|
err error
|
|
}{
|
|
0: {
|
|
resp: &proto.ReadStateBytes_Response{
|
|
Bytes: []byte(chunks[0]),
|
|
TotalLength: incorrectLength,
|
|
Range: &proto.StateRange{
|
|
Start: 0,
|
|
End: int64(len(chunks[0])) - 1,
|
|
},
|
|
},
|
|
err: nil,
|
|
},
|
|
1: {
|
|
resp: &proto.ReadStateBytes_Response{
|
|
Bytes: []byte(chunks[1]),
|
|
TotalLength: incorrectLength,
|
|
Range: &proto.StateRange{
|
|
Start: int64(len(chunks[0])),
|
|
End: int64(len(chunks[1])) - 1,
|
|
},
|
|
},
|
|
err: nil,
|
|
},
|
|
2: {
|
|
resp: &proto.ReadStateBytes_Response{},
|
|
err: io.EOF,
|
|
},
|
|
}
|
|
var count int
|
|
mockReadBytesClient.EXPECT().Recv().DoAndReturn(func() (*proto.ReadStateBytes_Response, error) {
|
|
ret := mockResp[count]
|
|
count++
|
|
return ret.resp, ret.err
|
|
}).Times(3)
|
|
|
|
// Act
|
|
request := providers.ReadStateBytesRequest{
|
|
TypeName: expectedReq.TypeName,
|
|
StateId: expectedReq.StateId,
|
|
}
|
|
resp := p.ReadStateBytes(request)
|
|
|
|
// Assert returned values
|
|
checkDiagsHasError(t, resp.Diagnostics)
|
|
expectedErr := fmt.Sprintf("expected state file of total %d bytes, received %d bytes", incorrectLength, correctLength)
|
|
if resp.Diagnostics.Err().Error() != expectedErr {
|
|
t.Fatalf("expected error diagnostic %q, but got: %q", expectedErr, resp.Diagnostics.Err())
|
|
}
|
|
if len(resp.Bytes) != 0 {
|
|
t.Fatalf("expected data to be omitted in error condition, but got: %q", string(resp.Bytes))
|
|
}
|
|
})
|
|
|
|
t.Run("an error diagnostic is returned when store type does not exist", func(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
ctx: context.Background(),
|
|
}
|
|
p.SetStateStoreChunkSize("mock_store", chunkSize)
|
|
|
|
// In this scenario the method returns before the call to the
|
|
// ReadStateBytes RPC, so no mocking needed
|
|
|
|
badStoreType := "doesnt_exist"
|
|
request := providers.ReadStateBytesRequest{
|
|
TypeName: badStoreType,
|
|
StateId: backend.DefaultStateName,
|
|
}
|
|
|
|
// Act
|
|
resp := p.ReadStateBytes(request)
|
|
|
|
// Assert returned values
|
|
checkDiagsHasError(t, resp.Diagnostics)
|
|
expectedErr := fmt.Sprintf("unknown state store type %q", badStoreType)
|
|
if resp.Diagnostics.Err().Error() != expectedErr {
|
|
t.Fatalf("expected error diagnostic %q, but got: %q", expectedErr, resp.Diagnostics.Err())
|
|
}
|
|
if len(resp.Bytes) != 0 {
|
|
t.Fatalf("expected data to be omitted in error condition, but got: %q", string(resp.Bytes))
|
|
}
|
|
})
|
|
|
|
t.Run("error diagnostics from the provider are returned", func(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
ctx: context.Background(),
|
|
}
|
|
p.SetStateStoreChunkSize("mock_store", chunkSize)
|
|
|
|
// Call to ReadStateBytes
|
|
// > Assert the arguments received
|
|
// > Define the returned mock client
|
|
mockReadBytesClient := mockReadStateBytesClient(t)
|
|
|
|
expectedReq := &proto.ReadStateBytes_Request{
|
|
TypeName: "mock_store",
|
|
StateId: backend.DefaultStateName,
|
|
}
|
|
client.EXPECT().ReadStateBytes(
|
|
gomock.Any(),
|
|
gomock.Eq(expectedReq),
|
|
gomock.Any(),
|
|
).Return(mockReadBytesClient, nil)
|
|
|
|
// Define what will be returned by each call to Recv
|
|
mockReadBytesClient.EXPECT().Recv().Return(&proto.ReadStateBytes_Response{
|
|
Diagnostics: []*proto.Diagnostic{
|
|
{
|
|
Severity: proto.Diagnostic_ERROR,
|
|
Summary: "Error from test",
|
|
Detail: "This error is forced by the test case",
|
|
},
|
|
},
|
|
}, nil)
|
|
|
|
// Act
|
|
request := providers.ReadStateBytesRequest{
|
|
TypeName: expectedReq.TypeName,
|
|
StateId: expectedReq.StateId,
|
|
}
|
|
resp := p.ReadStateBytes(request)
|
|
|
|
// Assert returned values
|
|
checkDiagsHasError(t, resp.Diagnostics)
|
|
expectedErr := "Error from test: This error is forced by the test case"
|
|
if resp.Diagnostics.Err().Error() != expectedErr {
|
|
t.Fatalf("expected error diagnostic %q, but got: %q", expectedErr, resp.Diagnostics.Err())
|
|
}
|
|
if len(resp.Bytes) != 0 {
|
|
t.Fatalf("expected data to be omitted in error condition, but got: %q", string(resp.Bytes))
|
|
}
|
|
})
|
|
|
|
t.Run("warning diagnostics from the provider are returned", func(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
ctx: context.Background(),
|
|
}
|
|
p.SetStateStoreChunkSize("mock_store", chunkSize)
|
|
|
|
// Call to ReadStateBytes
|
|
// > Assert the arguments received
|
|
// > Define the returned mock client
|
|
mockReadBytesClient := mockReadStateBytesClient(t)
|
|
|
|
expectedReq := &proto.ReadStateBytes_Request{
|
|
TypeName: "mock_store",
|
|
StateId: backend.DefaultStateName,
|
|
}
|
|
client.EXPECT().ReadStateBytes(
|
|
gomock.Any(),
|
|
gomock.Eq(expectedReq),
|
|
gomock.Any(),
|
|
).Return(mockReadBytesClient, nil)
|
|
|
|
// Define what will be returned by each call to Recv
|
|
chunk := "hello world"
|
|
totalLength := len(chunk)
|
|
mockResp := map[int]struct {
|
|
resp *proto.ReadStateBytes_Response
|
|
err error
|
|
}{
|
|
0: {
|
|
resp: &proto.ReadStateBytes_Response{
|
|
Bytes: []byte(chunk),
|
|
TotalLength: int64(totalLength),
|
|
Range: &proto.StateRange{
|
|
Start: 0,
|
|
End: int64(len(chunk)) - 1,
|
|
},
|
|
Diagnostics: []*proto.Diagnostic{
|
|
{
|
|
Severity: proto.Diagnostic_WARNING,
|
|
Summary: "Warning from test",
|
|
Detail: "This warning is forced by the test case",
|
|
},
|
|
},
|
|
},
|
|
err: nil,
|
|
},
|
|
1: {
|
|
resp: &proto.ReadStateBytes_Response{},
|
|
err: io.EOF,
|
|
},
|
|
}
|
|
var count int
|
|
mockReadBytesClient.EXPECT().Recv().DoAndReturn(func() (*proto.ReadStateBytes_Response, error) {
|
|
ret := mockResp[count]
|
|
count++
|
|
return ret.resp, ret.err
|
|
}).Times(2)
|
|
|
|
// There will be a call to CloseSend to close the stream
|
|
mockReadBytesClient.EXPECT().CloseSend().Return(nil).Times(1)
|
|
|
|
// Act
|
|
request := providers.ReadStateBytesRequest{
|
|
TypeName: expectedReq.TypeName,
|
|
StateId: expectedReq.StateId,
|
|
}
|
|
resp := p.ReadStateBytes(request)
|
|
|
|
// Assert returned values
|
|
checkDiags(t, resp.Diagnostics)
|
|
expectedWarn := "Warning from test: This warning is forced by the test case"
|
|
if resp.Diagnostics.ErrWithWarnings().Error() != expectedWarn {
|
|
t.Fatalf("expected warning diagnostic %q, but got: %q", expectedWarn, resp.Diagnostics.ErrWithWarnings().Error())
|
|
}
|
|
if len(resp.Bytes) == 0 {
|
|
t.Fatal("expected data to included despite warnings, but got no bytes")
|
|
}
|
|
})
|
|
|
|
t.Run("when reading data, grpc errors are surfaced via diagnostics", func(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
ctx: context.Background(),
|
|
}
|
|
p.SetStateStoreChunkSize("mock_store", chunkSize)
|
|
|
|
// Call to ReadStateBytes
|
|
// > Assert the arguments received
|
|
// > Define the returned mock client
|
|
mockClient := mockReadStateBytesClient(t)
|
|
expectedReq := &proto.ReadStateBytes_Request{
|
|
TypeName: "mock_store",
|
|
StateId: backend.DefaultStateName,
|
|
}
|
|
client.EXPECT().ReadStateBytes(
|
|
gomock.Any(),
|
|
gomock.Eq(expectedReq),
|
|
gomock.Any(),
|
|
).Return(mockClient, nil)
|
|
|
|
mockError := errors.New("grpc error forced in test")
|
|
mockClient.EXPECT().Recv().Return(&proto.ReadStateBytes_Response{}, mockError)
|
|
|
|
// Act
|
|
request := providers.ReadStateBytesRequest{
|
|
TypeName: expectedReq.TypeName,
|
|
StateId: expectedReq.StateId,
|
|
}
|
|
resp := p.ReadStateBytes(request)
|
|
|
|
// Assert returned values
|
|
checkDiagsHasError(t, resp.Diagnostics)
|
|
wantErr := fmt.Sprintf("Plugin error: The plugin returned an unexpected error from plugin6.(*GRPCProvider).ReadStateBytes: %s", mockError)
|
|
if resp.Diagnostics.Err().Error() != wantErr {
|
|
t.Fatalf("expected error diagnostic %q, but got: %q", wantErr, resp.Diagnostics.Err())
|
|
}
|
|
if len(resp.Bytes) != 0 {
|
|
t.Fatalf("expected data to be omitted in error condition, but got: %q", string(resp.Bytes))
|
|
}
|
|
})
|
|
|
|
t.Run("when closing the stream, grpc errors are surfaced via diagnostics", func(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
ctx: context.Background(),
|
|
}
|
|
p.SetStateStoreChunkSize("mock_store", chunkSize)
|
|
|
|
// Call to ReadStateBytes
|
|
// > Assert the arguments received
|
|
// > Define the returned mock client
|
|
mockClient := mockReadStateBytesClient(t)
|
|
expectedReq := &proto.ReadStateBytes_Request{
|
|
TypeName: "mock_store",
|
|
StateId: backend.DefaultStateName,
|
|
}
|
|
client.EXPECT().ReadStateBytes(
|
|
gomock.Any(),
|
|
gomock.Eq(expectedReq),
|
|
gomock.Any(),
|
|
).Return(mockClient, nil)
|
|
|
|
// Sufficient mocking of Recv to get to the call to CloseSend
|
|
mockClient.EXPECT().Recv().Return(&proto.ReadStateBytes_Response{}, io.EOF)
|
|
|
|
// Force a gRPC error from CloseSend
|
|
mockError := errors.New("grpc error forced in test")
|
|
mockClient.EXPECT().CloseSend().Return(mockError).Times(1)
|
|
|
|
// Act
|
|
request := providers.ReadStateBytesRequest{
|
|
TypeName: expectedReq.TypeName,
|
|
StateId: expectedReq.StateId,
|
|
}
|
|
resp := p.ReadStateBytes(request)
|
|
|
|
// Assert returned values
|
|
checkDiagsHasError(t, resp.Diagnostics)
|
|
wantErr := fmt.Sprintf("Plugin error: The plugin returned an unexpected error from plugin6.(*GRPCProvider).ReadStateBytes: %s", mockError)
|
|
if resp.Diagnostics.Err().Error() != wantErr {
|
|
t.Fatalf("expected error diagnostic %q, but got: %q", wantErr, resp.Diagnostics.Err())
|
|
}
|
|
if len(resp.Bytes) != 0 {
|
|
t.Fatalf("expected data to be omitted in error condition, but got: %q", string(resp.Bytes))
|
|
}
|
|
})
|
|
|
|
t.Run("when reading the data, warnings are raised when chunk sizes mismatch", func(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
ctx: context.Background(),
|
|
}
|
|
p.SetStateStoreChunkSize("mock_store", 5)
|
|
|
|
// Call to ReadStateBytes
|
|
// > Assert the arguments received
|
|
// > Define the returned mock client
|
|
mockReadBytesClient := mockReadStateBytesClient(t)
|
|
|
|
expectedReq := &proto.ReadStateBytes_Request{
|
|
TypeName: "mock_store",
|
|
StateId: backend.DefaultStateName,
|
|
}
|
|
client.EXPECT().ReadStateBytes(
|
|
gomock.Any(),
|
|
gomock.Eq(expectedReq),
|
|
gomock.Any(),
|
|
).Return(mockReadBytesClient, nil)
|
|
|
|
// Define what will be returned by each call to Recv
|
|
chunk := "helloworld"
|
|
totalLength := len(chunk)
|
|
mockResp := map[int]struct {
|
|
resp *proto.ReadStateBytes_Response
|
|
err error
|
|
}{
|
|
0: {
|
|
resp: &proto.ReadStateBytes_Response{
|
|
Bytes: []byte(chunk[:4]),
|
|
TotalLength: int64(totalLength),
|
|
Range: &proto.StateRange{
|
|
Start: 0,
|
|
End: 3,
|
|
},
|
|
Diagnostics: []*proto.Diagnostic{},
|
|
},
|
|
err: nil,
|
|
},
|
|
1: {
|
|
resp: &proto.ReadStateBytes_Response{
|
|
Bytes: []byte(chunk[4:]),
|
|
TotalLength: int64(totalLength),
|
|
Range: &proto.StateRange{
|
|
Start: 4,
|
|
End: 9,
|
|
},
|
|
Diagnostics: []*proto.Diagnostic{},
|
|
},
|
|
err: nil,
|
|
},
|
|
2: {
|
|
resp: &proto.ReadStateBytes_Response{},
|
|
err: io.EOF,
|
|
},
|
|
}
|
|
var count int
|
|
mockReadBytesClient.EXPECT().Recv().DoAndReturn(func() (*proto.ReadStateBytes_Response, error) {
|
|
ret := mockResp[count]
|
|
count++
|
|
return ret.resp, ret.err
|
|
}).Times(3)
|
|
|
|
// There will be a call to CloseSend to close the stream
|
|
mockReadBytesClient.EXPECT().CloseSend().Return(nil).Times(1)
|
|
|
|
// Act
|
|
request := providers.ReadStateBytesRequest{
|
|
TypeName: expectedReq.TypeName,
|
|
StateId: expectedReq.StateId,
|
|
}
|
|
resp := p.ReadStateBytes(request)
|
|
|
|
// Assert returned values
|
|
checkDiags(t, resp.Diagnostics)
|
|
expectedWarn := `2 warnings:
|
|
|
|
- Unexpected size of chunk received: Unexpected chunk of size 4 was received, expected 5; this is a bug in the provider mock_store - please report it there
|
|
- Unexpected size of last chunk received: Last chunk exceeded agreed size, expected 5, given 6; this is a bug in the provider mock_store - please report it there`
|
|
if resp.Diagnostics.ErrWithWarnings().Error() != expectedWarn {
|
|
t.Fatalf("expected warning diagnostic %q, but got: %q", expectedWarn, resp.Diagnostics.ErrWithWarnings().Error())
|
|
}
|
|
if len(resp.Bytes) != 10 {
|
|
t.Fatalf("expected 10 bytes to be read despite warnings, but got %d", len(resp.Bytes))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGRPCProvider_WriteStateBytes(t *testing.T) {
|
|
chunkSize := 4 << 20 // 4MB
|
|
|
|
t.Run("data smaller than the chunk size is sent in one write action", func(t *testing.T) {
|
|
// Less than 4MB
|
|
data := []byte("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod" +
|
|
"tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud" +
|
|
" exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor" +
|
|
" in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint" +
|
|
" occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")
|
|
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
ctx: context.Background(),
|
|
}
|
|
p.SetStateStoreChunkSize("mock_store", chunkSize)
|
|
|
|
// Assert there will be a call to WriteStateBytes
|
|
// & make it return the mock client
|
|
mockWriteClient := mockWriteStateBytesClient(t)
|
|
client.EXPECT().WriteStateBytes(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(mockWriteClient, nil)
|
|
|
|
// Spy on arguments passed to the Send method of the client
|
|
//
|
|
// We expect 1 call to Send as the total data
|
|
// is less than the chunk size
|
|
expectedReq := &proto.WriteStateBytes_RequestChunk{
|
|
Meta: &proto.RequestChunkMeta{
|
|
TypeName: "mock_store",
|
|
StateId: backend.DefaultStateName,
|
|
},
|
|
Bytes: data,
|
|
TotalLength: int64(len(data)),
|
|
Range: &proto.StateRange{
|
|
Start: 0,
|
|
End: int64(len(data)) - 1,
|
|
},
|
|
}
|
|
mockWriteClient.EXPECT().Send(gomock.Eq(expectedReq)).Times(1).Return(nil)
|
|
mockWriteClient.EXPECT().CloseAndRecv().Times(1).Return(&proto.WriteStateBytes_Response{}, nil)
|
|
|
|
// Act
|
|
request := providers.WriteStateBytesRequest{
|
|
TypeName: "mock_store",
|
|
StateId: backend.DefaultStateName,
|
|
Bytes: data,
|
|
}
|
|
resp := p.WriteStateBytes(request)
|
|
|
|
// Assert returned values
|
|
checkDiags(t, resp.Diagnostics)
|
|
})
|
|
|
|
t.Run("data larger than the chunk size is sent in multiple write actions", func(t *testing.T) {
|
|
// Make a buffer that can contain 10 bytes more than the 4MB chunk size
|
|
chunkSize := 4 * 1_000_000
|
|
dataBuff := bytes.NewBuffer(make([]byte, 0, chunkSize+10))
|
|
dataBuffCopy := bytes.NewBuffer(make([]byte, 0, chunkSize+10))
|
|
for i := 0; i < (chunkSize + 10); i++ {
|
|
dataBuff.WriteByte(63) // We're making 4MB + 10 bytes of question marks because why not
|
|
dataBuffCopy.WriteByte(63) // Used to make assertions
|
|
}
|
|
data := dataBuff.Bytes()
|
|
dataFirstChunk := dataBuffCopy.Next(chunkSize) // First write will have a full chunk
|
|
dataSecondChunk := dataBuffCopy.Next(chunkSize) // This will be the extra 10 bytes
|
|
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
ctx: context.Background(),
|
|
}
|
|
p.SetStateStoreChunkSize("mock_store", chunkSize)
|
|
|
|
// Assert there will be a call to WriteStateBytes
|
|
// & make it return the mock client
|
|
mockWriteClient := mockWriteStateBytesClient(t)
|
|
client.EXPECT().WriteStateBytes(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(mockWriteClient, nil)
|
|
|
|
// Spy on arguments passed to the Send method because data
|
|
// is written via separate chunks and separate calls to Send.
|
|
//
|
|
// We expect 2 calls to Send as the total data
|
|
// is 10 bytes larger than the chunk size
|
|
req1 := &proto.WriteStateBytes_RequestChunk{
|
|
Meta: &proto.RequestChunkMeta{
|
|
TypeName: "mock_store",
|
|
StateId: backend.DefaultStateName,
|
|
},
|
|
Bytes: dataFirstChunk,
|
|
TotalLength: int64(len(data)),
|
|
Range: &proto.StateRange{
|
|
Start: 0,
|
|
End: int64(chunkSize) - 1,
|
|
},
|
|
}
|
|
req2 := &proto.WriteStateBytes_RequestChunk{
|
|
Meta: nil,
|
|
Bytes: dataSecondChunk,
|
|
TotalLength: int64(len(data)),
|
|
Range: &proto.StateRange{
|
|
Start: int64(chunkSize),
|
|
End: int64(chunkSize+10) - 1,
|
|
},
|
|
}
|
|
mockWriteClient.EXPECT().Send(gomock.AnyOf(req1, req2)).Times(2).Return(nil)
|
|
mockWriteClient.EXPECT().CloseAndRecv().Times(1).Return(&proto.WriteStateBytes_Response{}, nil)
|
|
|
|
// Act
|
|
request := providers.WriteStateBytesRequest{
|
|
TypeName: "mock_store",
|
|
StateId: backend.DefaultStateName,
|
|
Bytes: data,
|
|
}
|
|
resp := p.WriteStateBytes(request)
|
|
|
|
// Assert returned values
|
|
checkDiags(t, resp.Diagnostics)
|
|
})
|
|
|
|
t.Run("when writing data, grpc errors are surfaced via diagnostics", func(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
ctx: context.Background(),
|
|
}
|
|
p.SetStateStoreChunkSize("mock_store", chunkSize)
|
|
|
|
// Assert there will be a call to WriteStateBytes
|
|
// & make it return the mock client
|
|
mockWriteClient := mockWriteStateBytesClient(t)
|
|
client.EXPECT().WriteStateBytes(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(mockWriteClient, nil)
|
|
|
|
mockError := errors.New("grpc error forced in test")
|
|
mockWriteClient.EXPECT().Send(gomock.Any()).Return(mockError)
|
|
|
|
// Act
|
|
request := providers.WriteStateBytesRequest{
|
|
TypeName: "mock_store",
|
|
StateId: backend.DefaultStateName,
|
|
Bytes: []byte("helloworld"),
|
|
}
|
|
resp := p.WriteStateBytes(request)
|
|
|
|
// Assert returned values
|
|
checkDiagsHasError(t, resp.Diagnostics)
|
|
wantErr := fmt.Sprintf("Plugin error: The plugin returned an unexpected error from plugin6.(*GRPCProvider).WriteStateBytes: %s", mockError)
|
|
if resp.Diagnostics.Err().Error() != wantErr {
|
|
t.Fatalf("unexpected error, wanted %q, got: %s", wantErr, resp.Diagnostics.Err())
|
|
}
|
|
})
|
|
|
|
t.Run("error diagnostics from the provider when closing the connection are returned", func(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
ctx: context.Background(),
|
|
}
|
|
p.SetStateStoreChunkSize("mock_store", chunkSize)
|
|
|
|
// Assert there will be a call to WriteStateBytes
|
|
// & make it return the mock client
|
|
mockWriteClient := mockWriteStateBytesClient(t)
|
|
client.EXPECT().WriteStateBytes(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(mockWriteClient, nil)
|
|
|
|
data := []byte("helloworld")
|
|
mockReq := &proto.WriteStateBytes_RequestChunk{
|
|
Meta: &proto.RequestChunkMeta{
|
|
TypeName: "mock_store",
|
|
StateId: backend.DefaultStateName,
|
|
},
|
|
Bytes: data,
|
|
TotalLength: int64(len(data)),
|
|
Range: &proto.StateRange{
|
|
Start: 0,
|
|
End: int64(len(data)) - 1,
|
|
},
|
|
}
|
|
mockResp := &proto.WriteStateBytes_Response{
|
|
Diagnostics: []*proto.Diagnostic{
|
|
{
|
|
Severity: proto.Diagnostic_ERROR,
|
|
Summary: "Error from test mock",
|
|
Detail: "This error is returned from the test mock",
|
|
},
|
|
},
|
|
}
|
|
mockWriteClient.EXPECT().Send(gomock.Eq(mockReq)).Times(1).Return(nil)
|
|
mockWriteClient.EXPECT().CloseAndRecv().Times(1).Return(mockResp, nil)
|
|
|
|
// Act
|
|
request := providers.WriteStateBytesRequest{
|
|
TypeName: "mock_store",
|
|
StateId: backend.DefaultStateName,
|
|
Bytes: data,
|
|
}
|
|
resp := p.WriteStateBytes(request)
|
|
|
|
// Assert returned values
|
|
checkDiagsHasError(t, resp.Diagnostics)
|
|
if resp.Diagnostics.Err().Error() != "Error from test mock: This error is returned from the test mock" {
|
|
t.Fatal()
|
|
}
|
|
})
|
|
|
|
t.Run("warning diagnostics from the provider when closing the connection are returned", func(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
ctx: context.Background(),
|
|
}
|
|
p.SetStateStoreChunkSize("mock_store", chunkSize)
|
|
|
|
// Assert there will be a call to WriteStateBytes
|
|
// & make it return the mock client
|
|
mockWriteClient := mockWriteStateBytesClient(t)
|
|
client.EXPECT().WriteStateBytes(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(mockWriteClient, nil)
|
|
|
|
data := []byte("helloworld")
|
|
mockReq := &proto.WriteStateBytes_RequestChunk{
|
|
Meta: &proto.RequestChunkMeta{
|
|
TypeName: "mock_store",
|
|
StateId: backend.DefaultStateName,
|
|
},
|
|
Bytes: data,
|
|
TotalLength: int64(len(data)),
|
|
Range: &proto.StateRange{
|
|
Start: 0,
|
|
End: int64(len(data)) - 1,
|
|
},
|
|
}
|
|
mockResp := &proto.WriteStateBytes_Response{
|
|
Diagnostics: []*proto.Diagnostic{
|
|
{
|
|
Severity: proto.Diagnostic_WARNING,
|
|
Summary: "Warning from test mock",
|
|
Detail: "This warning is returned from the test mock",
|
|
},
|
|
},
|
|
}
|
|
mockWriteClient.EXPECT().Send(gomock.Eq(mockReq)).Times(1).Return(nil)
|
|
mockWriteClient.EXPECT().CloseAndRecv().Times(1).Return(mockResp, nil)
|
|
|
|
// Act
|
|
request := providers.WriteStateBytesRequest{
|
|
TypeName: "mock_store",
|
|
StateId: backend.DefaultStateName,
|
|
Bytes: data,
|
|
}
|
|
resp := p.WriteStateBytes(request)
|
|
|
|
// Assert returned values
|
|
checkDiags(t, resp.Diagnostics)
|
|
if resp.Diagnostics.ErrWithWarnings().Error() != "Warning from test mock: This warning is returned from the test mock" {
|
|
t.Fatal()
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGRPCProvider_GenerateResourceConfig(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
client.EXPECT().GenerateResourceConfig(
|
|
gomock.Any(),
|
|
gomock.Cond[any](func(x any) bool {
|
|
req := x.(*proto.GenerateResourceConfig_Request)
|
|
if req.TypeName != "resource" {
|
|
return false
|
|
}
|
|
if req.State == nil {
|
|
t.Log("GenerateResourceConfig state is nil")
|
|
return false
|
|
}
|
|
return true
|
|
}),
|
|
).Return(&proto.GenerateResourceConfig_Response{
|
|
Config: &proto.DynamicValue{
|
|
Msgpack: []byte("\x81\xa4attr\xa3bar"),
|
|
},
|
|
}, nil)
|
|
resp := p.GenerateResourceConfig(providers.GenerateResourceConfigRequest{
|
|
TypeName: "resource",
|
|
State: cty.ObjectVal(map[string]cty.Value{
|
|
"computed": cty.StringVal("computed"),
|
|
"attr": cty.StringVal("foo"),
|
|
}),
|
|
})
|
|
checkDiags(t, resp.Diagnostics)
|
|
}
|