Files
opentf/internal/plugin6/grpc_provider_test.go
Martin Atkins 4f09b06624 providers: Remove explicit handling of "deferred" signal from providers
This removes most of the code previously added in 491969d29d, because we
since learned that the hashicorp/helm provider signals deferral when any
unknown values are present in provider configuration even though in
practice it can sometimes successfully plan changes in spite of those
unknown values.

That therefore made the hashicorp/helm provider behavior worse under this
change than it was before, returning an error when no error was actually
warranted.

The ephemeral resources implementation landed later and was also
interacting with this change, and so this isn't a line-for-line revert of
the original change but still removes everything that was added in support
of handling provider deferral signals so that we'll be able to start fresh
with this later if we find a better way to handle it.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
2025-12-16 13:09:32 -08:00

1382 lines
38 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package plugin6
import (
"bytes"
"context"
"fmt"
"slices"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/msgpack"
"go.uber.org/mock/gomock"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/legacy/hcl2shim"
mockproto "github.com/opentofu/opentofu/internal/plugin6/mock_proto"
"github.com/opentofu/opentofu/internal/providers"
"github.com/opentofu/opentofu/internal/tfdiags"
proto "github.com/opentofu/opentofu/internal/tfplugin6"
)
var _ providers.Interface = (*GRPCProvider)(nil)
var (
equateEmpty = cmpopts.EquateEmpty()
typeComparer = cmp.Comparer(cty.Type.Equals)
valueComparer = cmp.Comparer(cty.Value.RawEquals)
)
func mutateSchemaResponse(response *proto.GetProviderSchema_Response, mut ...func(schemaResponse *proto.GetProviderSchema_Response)) *proto.GetProviderSchema_Response {
for _, f := range mut {
f(response)
}
return response
}
func addAttributeToResource(resourceName string, attr *proto.Schema_Attribute) func(response *proto.GetProviderSchema_Response) {
return func(schemaResponse *proto.GetProviderSchema_Response) {
schemaResponse.ResourceSchemas[resourceName].Block.Attributes = append(schemaResponse.ResourceSchemas[resourceName].Block.Attributes, attr)
}
}
func mockProviderClient(t *testing.T) *mockproto.MockProviderClient {
return mockProviderClientWithSchema(t, providerProtoSchema())
}
func mockProviderClientWithSchema(t *testing.T, schema *proto.GetProviderSchema_Response) *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(schema, nil)
return client
}
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,
},
},
},
},
},
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{
"eph": {
Version: 1,
Block: &proto.Schema_Block{
Attributes: []*proto.Schema_Attribute{
{
Name: "attr",
Type: []byte(`"string"`),
Required: true,
},
},
},
},
},
Functions: map[string]*proto.Function{
"fn": &proto.Function{
Parameters: []*proto.Function_Parameter{{
Name: "par_a",
Type: []byte(`"string"`),
AllowNullValue: false,
AllowUnknownValues: false,
}},
VariadicParameter: &proto.Function_Parameter{
Name: "par_var",
Type: []byte(`"string"`),
AllowNullValue: true,
AllowUnknownValues: false,
},
Return: &proto.Function_Return{
Type: []byte(`"string"`),
},
},
},
}
}
func TestGRPCProvider_GetSchema(t *testing.T) {
p := &GRPCProvider{
client: mockProviderClient(t),
}
resp := p.GetProviderSchema(t.Context())
checkDiags(t, resp.Diagnostics)
{ // check ephemeral attribute of the schema blocks
if !resp.Provider.Block.Ephemeral {
t.Errorf("provider.Block.Ephemeral meant to be true")
}
checkResources := func(t *testing.T, r map[string]providers.Schema, want bool) {
for typ, schema := range r {
if schema.Block.Ephemeral != want {
t.Errorf("expected resource %q to have ephemeral as %t", typ, want)
}
}
}
checkResources(t, resp.ResourceTypes, false)
checkResources(t, resp.DataSources, false)
checkResources(t, resp.EphemeralResources, true)
}
}
// 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(t.Context())
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(t.Context())
checkDiagsHasError(t, resp.Diagnostics)
}
func TestGRPCProvider_GetSchema_GlobalCacheEnabled(t *testing.T) {
ctrl := gomock.NewController(t)
client := mockproto.NewMockProviderClient(ctrl)
// The SchemaCache is global and is saved between test runs
providers.SchemaCache = providers.NewMockSchemaCache()
providerAddr := addrs.Provider{
Namespace: "namespace",
Type: "type",
}
mockedProviderResponse := &proto.Schema{Version: 2, Block: &proto.Schema_Block{}}
client.EXPECT().GetProviderSchema(
gomock.Any(),
gomock.Any(),
gomock.Any(),
).Times(1).Return(&proto.GetProviderSchema_Response{
Provider: mockedProviderResponse,
ServerCapabilities: &proto.ServerCapabilities{GetProviderSchemaOptional: true},
}, nil)
// Run GetProviderTwice, expect GetSchema to be called once
// Re-initialize the provider before each run to avoid usage of the local cache
p := &GRPCProvider{
client: client,
Addr: providerAddr,
}
resp := p.GetProviderSchema(t.Context())
checkDiags(t, resp.Diagnostics)
if !cmp.Equal(resp.Provider.Version, mockedProviderResponse.Version) {
t.Fatal(cmp.Diff(resp.Provider.Version, mockedProviderResponse.Version))
}
p = &GRPCProvider{
client: client,
Addr: providerAddr,
}
resp = p.GetProviderSchema(t.Context())
checkDiags(t, resp.Diagnostics)
if !cmp.Equal(resp.Provider.Version, mockedProviderResponse.Version) {
t.Fatal(cmp.Diff(resp.Provider.Version, mockedProviderResponse.Version))
}
}
func TestGRPCProvider_GetSchema_GlobalCacheDisabled(t *testing.T) {
ctrl := gomock.NewController(t)
client := mockproto.NewMockProviderClient(ctrl)
// The SchemaCache is global and is saved between test runs
providers.SchemaCache = providers.NewMockSchemaCache()
providerAddr := addrs.Provider{
Namespace: "namespace",
Type: "type",
}
mockedProviderResponse := &proto.Schema{Version: 2, Block: &proto.Schema_Block{}}
client.EXPECT().GetProviderSchema(
gomock.Any(),
gomock.Any(),
gomock.Any(),
).Times(2).Return(&proto.GetProviderSchema_Response{
Provider: mockedProviderResponse,
ServerCapabilities: &proto.ServerCapabilities{GetProviderSchemaOptional: false},
}, nil)
// Run GetProviderTwice, expect GetSchema to be called once
// Re-initialize the provider before each run to avoid usage of the local cache
p := &GRPCProvider{
client: client,
Addr: providerAddr,
}
resp := p.GetProviderSchema(t.Context())
checkDiags(t, resp.Diagnostics)
if !cmp.Equal(resp.Provider.Version, mockedProviderResponse.Version) {
t.Fatal(cmp.Diff(resp.Provider.Version, mockedProviderResponse.Version))
}
p = &GRPCProvider{
client: client,
Addr: providerAddr,
}
resp = p.GetProviderSchema(t.Context())
checkDiags(t, resp.Diagnostics)
if !cmp.Equal(resp.Provider.Version, mockedProviderResponse.Version) {
t.Fatal(cmp.Diff(resp.Provider.Version, mockedProviderResponse.Version))
}
}
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(t.Context(), 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(t.Context(), 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(t.Context(), providers.ValidateDataResourceConfigRequest{
TypeName: "data",
Config: cfg,
})
checkDiags(t, resp.Diagnostics)
}
func TestGRPCProvider_ValidateEphemeralResourceConfig(t *testing.T) {
client := mockProviderClient(t)
p := &GRPCProvider{
client: client,
}
client.EXPECT().ValidateEphemeralResourceConfig(
gomock.Any(),
gomock.Any(),
).Return(&proto.ValidateEphemeralResourceConfig_Response{}, nil)
cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{"attr": "value"})
resp := p.ValidateEphemeralConfig(t.Context(), providers.ValidateEphemeralConfigRequest{
TypeName: "eph",
Config: cfg,
})
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(t.Context(), 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(t.Context(), 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_UpgradeResourceStateWithWriteOnlyReturned(t *testing.T) {
client := mockProviderClientWithSchema(t, mutateSchemaResponse(providerProtoSchema(), addAttributeToResource("resource", &proto.Schema_Attribute{
Name: "write_only_attr",
Type: []byte(`"string"`),
Optional: true,
WriteOnly: true,
})))
p := &GRPCProvider{
client: client,
}
client.EXPECT().UpgradeResourceState(
gomock.Any(),
gomock.Any(),
).DoAndReturn(func(_ context.Context, _ *proto.UpgradeResourceState_Request, _ ...grpc.CallOption) (*proto.UpgradeResourceState_Response, error) {
b, err := msgpack.Marshal(
cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("bar"), "write_only_attr": cty.StringVal("val")}),
cty.Object(map[string]cty.Type{"attr": cty.String, "write_only_attr": cty.String}),
)
if err != nil {
return nil, err
}
return &proto.UpgradeResourceState_Response{
UpgradedState: &proto.DynamicValue{
Msgpack: b,
},
}, nil
})
resp := p.UpgradeResourceState(t.Context(), providers.UpgradeResourceStateRequest{
TypeName: "resource",
Version: 0,
RawStateJSON: []byte(`{"attr":"bar"}`),
})
checkDiagsHasError(t, resp.Diagnostics)
expectedErr := `Resource type "resource" returned an actual value for the write-only attribute ".write_only_attr" while it is meant to be nil. This is an issue in the provider SDK.`
if gotErr := resp.Diagnostics[0].Description().Detail; expectedErr != gotErr {
t.Errorf("the expected error is not the same with the one returned.\nexpected: %s\ngot: %s", expectedErr, gotErr)
}
}
func TestGRPCProvider_MoveResourceState(t *testing.T) {
client := mockProviderClient(t)
p := &GRPCProvider{
client: client,
}
client.EXPECT().MoveResourceState(
gomock.Any(),
gomock.Any(),
).Return(&proto.MoveResourceState_Response{
TargetState: &proto.DynamicValue{
Msgpack: []byte("\x81\xa4attr\xa3bar"),
},
TargetPrivate: []byte(`{"meta": "data"}`),
}, nil)
resp := p.MoveResourceState(t.Context(), providers.MoveResourceStateRequest{
SourceTypeName: "resource_old",
SourceSchemaVersion: 0,
TargetTypeName: "resource",
})
checkDiags(t, resp.Diagnostics)
expectedState := cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("bar"),
})
expectedPrivate := []byte(`{"meta": "data"}`)
if !cmp.Equal(expectedState, resp.TargetState, typeComparer, valueComparer, equateEmpty) {
t.Fatal(cmp.Diff(expectedState, resp.TargetState, typeComparer, valueComparer, equateEmpty))
}
if !bytes.Equal(expectedPrivate, resp.TargetPrivate) {
t.Fatalf("expected %q, got %q", expectedPrivate, resp.TargetPrivate)
}
}
func TestGRPCProvider_MoveResourceStateReturnsWriteOnlyValue(t *testing.T) {
client := mockProviderClientWithSchema(t, mutateSchemaResponse(providerProtoSchema(), addAttributeToResource("resource", &proto.Schema_Attribute{
Name: "write_only_attr",
Type: []byte(`"string"`),
Optional: true,
WriteOnly: true,
})))
p := &GRPCProvider{
client: client,
}
client.EXPECT().MoveResourceState(
gomock.Any(),
gomock.Any(),
).DoAndReturn(func(_ context.Context, _ *proto.MoveResourceState_Request, _ ...grpc.CallOption) (*proto.MoveResourceState_Response, error) {
b, err := msgpack.Marshal(
cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("bar"), "write_only_attr": cty.StringVal("val")}),
cty.Object(map[string]cty.Type{"attr": cty.String, "write_only_attr": cty.String}),
)
if err != nil {
return nil, err
}
return &proto.MoveResourceState_Response{
TargetState: &proto.DynamicValue{
Msgpack: b,
},
}, nil
})
resp := p.MoveResourceState(t.Context(), providers.MoveResourceStateRequest{
SourceTypeName: "resource_old",
SourceSchemaVersion: 0,
TargetTypeName: "resource",
})
checkDiagsHasError(t, resp.Diagnostics)
expectedErr := `Resource type "resource" returned an actual value for the write-only attribute ".write_only_attr" while it is meant to be nil. This is an issue in the provider SDK.`
if gotErr := resp.Diagnostics[0].Description().Detail; expectedErr != gotErr {
t.Errorf("the expected error is not the same with the one returned.\nexpected: %s\ngot: %s", expectedErr, gotErr)
}
}
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(t.Context(), 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(t.Context())
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(t.Context(), 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_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(t.Context(), 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_ReadResourceReturnsWriteOnlyValue(t *testing.T) {
client := mockProviderClientWithSchema(t, mutateSchemaResponse(providerProtoSchema(), addAttributeToResource("resource", &proto.Schema_Attribute{
Name: "write_only_attr",
Type: []byte(`"string"`),
Optional: true,
WriteOnly: true,
})))
p := &GRPCProvider{
client: client,
}
client.EXPECT().ReadResource(
gomock.Any(),
gomock.Any(),
).DoAndReturn(func(_ context.Context, _ *proto.ReadResource_Request, opts ...grpc.CallOption) (*proto.ReadResource_Response, error) {
b, err := msgpack.Marshal(
cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("bar"), "write_only_attr": cty.StringVal("val")}),
cty.Object(map[string]cty.Type{"attr": cty.String, "write_only_attr": cty.String}),
)
if err != nil {
return nil, err
}
return &proto.ReadResource_Response{
NewState: &proto.DynamicValue{
Msgpack: b,
},
}, nil
})
resp := p.ReadResource(t.Context(), providers.ReadResourceRequest{
TypeName: "resource",
PriorState: cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("foo"),
"write_only_attr": cty.NullVal(cty.String),
}),
})
checkDiagsHasError(t, resp.Diagnostics)
expectedErr := `Resource type "resource" returned an actual value for the write-only attribute ".write_only_attr" while it is meant to be nil. This is an issue in the provider SDK.`
if gotErr := resp.Diagnostics[0].Description().Detail; expectedErr != gotErr {
t.Errorf("the expected error is not the same with the one returned.\nexpected: %s\ngot: %s", expectedErr, gotErr)
}
}
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(t.Context(), 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(t.Context(), 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(t.Context(), 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_PlanResourceChangeReturnsWriteOnlyValue(t *testing.T) {
client := mockProviderClientWithSchema(t, mutateSchemaResponse(providerProtoSchema(), addAttributeToResource("resource", &proto.Schema_Attribute{
Name: "write_only_attr",
Type: []byte(`"string"`),
Optional: true,
WriteOnly: true,
})))
p := &GRPCProvider{
client: client,
}
client.EXPECT().PlanResourceChange(
gomock.Any(),
gomock.Any(),
).DoAndReturn(func(_ context.Context, _ *proto.PlanResourceChange_Request, opts ...grpc.CallOption) (*proto.PlanResourceChange_Response, error) {
b, err := msgpack.Marshal(
cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("bar"), "write_only_attr": cty.StringVal("val")}),
cty.Object(map[string]cty.Type{"attr": cty.String, "write_only_attr": cty.String}),
)
if err != nil {
return nil, err
}
return &proto.PlanResourceChange_Response{
PlannedState: &proto.DynamicValue{
Msgpack: b,
},
}, nil
})
resp := p.PlanResourceChange(t.Context(), providers.PlanResourceChangeRequest{
TypeName: "resource",
PriorState: cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("foo"),
"write_only_attr": cty.NullVal(cty.String),
}),
ProposedNewState: cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("bar"),
"write_only_attr": cty.NullVal(cty.String),
}),
Config: cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("bar"),
"write_only_attr": cty.NullVal(cty.String),
}),
})
checkDiagsHasError(t, resp.Diagnostics)
expectedErr := `Resource type "resource" returned an actual value for the write-only attribute ".write_only_attr" while it is meant to be nil. This is an issue in the provider SDK.`
if gotErr := resp.Diagnostics[0].Description().Detail; expectedErr != gotErr {
t.Errorf("the expected error is not the same with the one returned.\nexpected: %s\ngot: %s", expectedErr, gotErr)
}
}
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(t.Context(), 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(t.Context(), 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_ApplyResourceChangeReturnsWriteOnlyValue(t *testing.T) {
client := mockProviderClientWithSchema(t, mutateSchemaResponse(providerProtoSchema(), addAttributeToResource("resource", &proto.Schema_Attribute{
Name: "write_only_attr",
Type: []byte(`"string"`),
Optional: true,
WriteOnly: true,
})))
p := &GRPCProvider{
client: client,
}
client.EXPECT().ApplyResourceChange(
gomock.Any(),
gomock.Any(),
).DoAndReturn(func(_ context.Context, _ *proto.ApplyResourceChange_Request, opts ...grpc.CallOption) (*proto.ApplyResourceChange_Response, error) {
b, err := msgpack.Marshal(
cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("bar"), "write_only_attr": cty.StringVal("val")}),
cty.Object(map[string]cty.Type{"attr": cty.String, "write_only_attr": cty.String}),
)
if err != nil {
return nil, err
}
return &proto.ApplyResourceChange_Response{
NewState: &proto.DynamicValue{
Msgpack: b,
},
}, nil
})
resp := p.ApplyResourceChange(t.Context(), providers.ApplyResourceChangeRequest{
TypeName: "resource",
PriorState: cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("foo"),
"write_only_attr": cty.NullVal(cty.String),
}),
PlannedState: cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("bar"),
"write_only_attr": cty.NullVal(cty.String),
}),
Config: cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("bar"),
"write_only_attr": cty.StringVal("foo"),
}),
})
checkDiagsHasError(t, resp.Diagnostics)
expectedErr := `Resource type "resource" returned an actual value for the write-only attribute ".write_only_attr" while it is meant to be nil. This is an issue in the provider SDK.`
if gotErr := resp.Diagnostics[0].Description().Detail; expectedErr != gotErr {
t.Errorf("the expected error is not the same with the one returned.\nexpected: %s\ngot: %s", expectedErr, gotErr)
}
}
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(t.Context(), 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(t.Context(), 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_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(t.Context(), 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(t.Context(), 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) {
t.Run("success", func(t *testing.T) {
client := mockProviderClient(t)
p := &GRPCProvider{
client: client,
}
future := time.Now().Add(time.Minute)
client.EXPECT().OpenEphemeralResource(
gomock.Any(),
gomock.Any(),
).Return(&proto.OpenEphemeralResource_Response{
Result: &proto.DynamicValue{
Msgpack: []byte("\x81\xa4attr\xa3bar"),
},
Private: []byte("private data"),
RenewAt: timestamppb.New(future),
}, nil)
resp := p.OpenEphemeralResource(t.Context(), providers.OpenEphemeralResourceRequest{
TypeName: "eph",
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 diff := cmp.Diff(expected, resp.Result, typeComparer, valueComparer, equateEmpty); diff != "" {
t.Fatalf("expected to have no diff between the expected result and result from the openEphemeral. got: %s", diff)
}
if resp.RenewAt == nil || !future.Equal(*resp.RenewAt) {
t.Fatalf("unexpected renewAt. got: %s, want %s", resp.RenewAt, future)
}
if got, want := resp.Private, []byte("private data"); !slices.Equal(got, want) {
t.Fatalf("unexpected private data. got: %q, want %q", got, want)
}
})
t.Run("requested type is not in schema", func(t *testing.T) {
client := mockProviderClient(t)
p := &GRPCProvider{
client: client,
}
resp := p.OpenEphemeralResource(t.Context(), providers.OpenEphemeralResourceRequest{
TypeName: "non_existing",
Config: cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("foo"),
}),
})
checkDiagsHasError(t, resp.Diagnostics)
if got, want := resp.Diagnostics.Err().Error(), `unknown ephemeral resource "non_existing"`; !strings.Contains(got, want) {
t.Fatalf("diagnostis does not contain the expected content. got: %s; want: %s", got, want)
}
})
}
func TestGRPCProvider_RenewEphemeralResource(t *testing.T) {
ctrl := gomock.NewController(t)
client := mockproto.NewMockProviderClient(ctrl)
p := &GRPCProvider{
client: client,
}
future := time.Now().Add(time.Minute)
client.EXPECT().RenewEphemeralResource(
gomock.Any(),
gomock.Any(),
).Return(&proto.RenewEphemeralResource_Response{
Private: []byte("private data new"),
RenewAt: timestamppb.New(future),
}, nil)
resp := p.RenewEphemeralResource(t.Context(), providers.RenewEphemeralResourceRequest{
TypeName: "eph",
})
checkDiags(t, resp.Diagnostics)
if resp.RenewAt == nil || !future.Equal(*resp.RenewAt) {
t.Fatalf("unexpected renewAt. got: %s, want %s", resp.RenewAt, future)
}
if got, want := resp.Private, []byte("private data new"); !slices.Equal(got, want) {
t.Fatalf("unexpected private data. got: %q, want %q", got, want)
}
}
func TestGRPCProvider_CloseEphemeralResource(t *testing.T) {
ctrl := gomock.NewController(t)
client := mockproto.NewMockProviderClient(ctrl)
p := &GRPCProvider{
client: client,
}
client.EXPECT().CloseEphemeralResource(
gomock.Any(),
gomock.Any(),
).Return(&proto.CloseEphemeralResource_Response{}, nil)
resp := p.CloseEphemeralResource(t.Context(), providers.CloseEphemeralResourceRequest{
TypeName: "eph",
})
checkDiags(t, resp.Diagnostics)
}
func TestGRPCProvider_CallFunction(t *testing.T) {
client := mockProviderClient(t)
p := &GRPCProvider{
client: client,
}
client.EXPECT().CallFunction(
gomock.Any(),
gomock.Any(),
).Return(&proto.CallFunction_Response{
Result: &proto.DynamicValue{Json: []byte(`"foo"`)},
}, nil)
resp := p.CallFunction(t.Context(), providers.CallFunctionRequest{
Name: "fn",
Arguments: []cty.Value{cty.StringVal("bar"), cty.NilVal},
})
if resp.Error != nil {
t.Fatal(resp.Error)
}
if resp.Result != cty.StringVal("foo") {
t.Fatalf("%v", resp.Result)
}
}