// Copyright (c) The OpenTofu Authors // SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2023 HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package plugin import ( "bytes" "context" "fmt" "slices" "strings" "testing" "time" "github.com/google/go-cmp/cmp" "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/legacy/hcl2shim" mockproto "github.com/opentofu/opentofu/internal/plugin/mock_proto" "github.com/opentofu/opentofu/internal/providers" "github.com/opentofu/opentofu/internal/tfdiags" proto "github.com/opentofu/opentofu/internal/tfplugin5" ) var _ providers.Interface = (*GRPCProvider)(nil) 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().GetSchema( gomock.Any(), gomock.Any(), gomock.Any(), ).Return(schema, nil) return client } func newGRPCProvider(client proto.ProviderClient) *GRPCProvider { return &GRPCProvider{ client: client, SchemaCache: providers.NewSchemaCache(), } } 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": &proto.Schema{ Version: 1, Block: &proto.Schema_Block{ Attributes: []*proto.Schema_Attribute{ { Name: "attr", Type: []byte(`"string"`), Required: true, }, }, }, }, }, DataSourceSchemas: map[string]*proto.Schema{ "data": &proto.Schema{ 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 := newGRPCProvider(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().GetSchema( gomock.Any(), gomock.Any(), gomock.Any(), ).Return(&proto.GetProviderSchema_Response{}, fmt.Errorf("test error")) p := newGRPCProvider(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) cache := providers.NewSchemaCache() mockedProviderResponse := &proto.Schema{Version: 2, Block: &proto.Schema_Block{}} client.EXPECT().GetSchema( 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 := newGRPCProvider(client) p.SchemaCache = cache 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 = newGRPCProvider(client) p.SchemaCache = cache 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) mockedProviderResponse := &proto.Schema{Version: 2, Block: &proto.Schema_Block{}} client.EXPECT().GetSchema( 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 := newGRPCProvider(client) 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 = newGRPCProvider(client) 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)) } } // 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().GetSchema( 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 := newGRPCProvider(client) resp := p.GetProviderSchema(t.Context()) checkDiagsHasError(t, resp.Diagnostics) } func TestGRPCProvider_PrepareProviderConfig(t *testing.T) { client := mockProviderClient(t) p := newGRPCProvider(client) client.EXPECT().PrepareProviderConfig( gomock.Any(), gomock.Any(), ).Return(&proto.PrepareProviderConfig_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 := newGRPCProvider(client) client.EXPECT().ValidateResourceTypeConfig( gomock.Any(), gomock.Any(), ).Return(&proto.ValidateResourceTypeConfig_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_ValidateDataSourceConfig(t *testing.T) { client := mockProviderClient(t) p := newGRPCProvider(client) client.EXPECT().ValidateDataSourceConfig( gomock.Any(), gomock.Any(), ).Return(&proto.ValidateDataSourceConfig_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 := newGRPCProvider(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 := newGRPCProvider(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 := newGRPCProvider(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 := newGRPCProvider(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 := newGRPCProvider(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 := newGRPCProvider(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 := newGRPCProvider(client) client.EXPECT().Configure( gomock.Any(), gomock.Any(), ).Return(&proto.Configure_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 := newGRPCProvider(client) client.EXPECT().Stop( gomock.Any(), gomock.Any(), ).Return(&proto.Stop_Response{}, nil) err := p.Stop(t.Context()) if err != nil { t.Fatal(err) } } func TestGRPCProvider_ReadResource(t *testing.T) { client := mockProviderClient(t) p := newGRPCProvider(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 := newGRPCProvider(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 := newGRPCProvider(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 := newGRPCProvider(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 := newGRPCProvider(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 := newGRPCProvider(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 := newGRPCProvider(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 := newGRPCProvider(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 := newGRPCProvider(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 := newGRPCProvider(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 := newGRPCProvider(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 := newGRPCProvider(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 := newGRPCProvider(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 := newGRPCProvider(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 := newGRPCProvider(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 := newGRPCProvider(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 := newGRPCProvider(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 := newGRPCProvider(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 := newGRPCProvider(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) } }