// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package plugin import ( "bytes" "fmt" "testing" "github.com/golang/mock/gomock" "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform/internal/configs/hcl2shim" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/tfdiags" "github.com/zclconf/go-cty/cty" mockproto "github.com/hashicorp/terraform/internal/plugin/mock_proto" proto "github.com/hashicorp/terraform/internal/tfplugin5" ) var _ providers.Interface = (*GRPCProvider)(nil) func mockProviderClient(t *testing.T) *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(providerProtoSchema(), 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": &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, }, }, }, }, }, } } func TestGRPCProvider_GetSchema(t *testing.T) { p := &GRPCProvider{ client: mockProviderClient(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().GetSchema( 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().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 := &GRPCProvider{ client: client, } resp := p.GetProviderSchema() checkDiagsHasError(t, resp.Diagnostics) } func TestGRPCProvider_PrepareProviderConfig(t *testing.T) { client := mockProviderClient(t) p := &GRPCProvider{ client: client, } client.EXPECT().PrepareProviderConfig( gomock.Any(), gomock.Any(), ).Return(&proto.PrepareProviderConfig_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().ValidateResourceTypeConfig( gomock.Any(), gomock.Any(), ).Return(&proto.ValidateResourceTypeConfig_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_ValidateDataSourceConfig(t *testing.T) { client := mockProviderClient(t) p := &GRPCProvider{ client: client, } client.EXPECT().ValidateDataSourceConfig( gomock.Any(), gomock.Any(), ).Return(&proto.ValidateDataSourceConfig_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_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_Configure(t *testing.T) { client := mockProviderClient(t) p := &GRPCProvider{ client: client, } client.EXPECT().Configure( gomock.Any(), gomock.Any(), ).Return(&proto.Configure_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().Stop( gomock.Any(), gomock.Any(), ).Return(&proto.Stop_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_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_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)) } }