From 7681b35acac4301775c2bc8b9512cd4928722299 Mon Sep 17 00:00:00 2001 From: Oriol Date: Fri, 5 Jan 2024 12:09:45 +0100 Subject: [PATCH] test: Add unit tests to advanced_cluster (#1809) * add tests for refresh functions of advanced cluster * use mockery --- .mockery.yaml | 2 + .../common_advanced_cluster.go | 10 +- .../common_advanced_cluster_test.go | 215 ++++++++++++++++++ .../service_advanced_cluster.go | 35 +++ .../resource_cloud_backup_snapshot.go | 2 +- internal/service/cluster/resource_cluster.go | 6 +- ...resource_private_endpoint_regional_mode.go | 2 +- .../resource_privatelink_endpoint_service.go | 4 +- internal/testutil/mocksvc/cluster_service.go | 146 ++++++++++++ 9 files changed, 410 insertions(+), 12 deletions(-) create mode 100644 internal/service/advancedcluster/service_advanced_cluster.go create mode 100644 internal/testutil/mocksvc/cluster_service.go diff --git a/.mockery.yaml b/.mockery.yaml index defc1945aa..de719f7fb9 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -9,7 +9,9 @@ packages: ? github.com/mongodb/terraform-provider-mongodbatlas/internal/service/searchdeployment ? github.com/mongodb/terraform-provider-mongodbatlas/internal/service/encryptionatrest ? github.com/mongodb/terraform-provider-mongodbatlas/internal/service/project + ? github.com/mongodb/terraform-provider-mongodbatlas/internal/service/advancedcluster : interfaces: DeploymentService: EarService: GroupProjectService: + ClusterService: diff --git a/internal/service/advancedcluster/common_advanced_cluster.go b/internal/service/advancedcluster/common_advanced_cluster.go index f58bff7499..2beb8c9c4d 100644 --- a/internal/service/advancedcluster/common_advanced_cluster.go +++ b/internal/service/advancedcluster/common_advanced_cluster.go @@ -129,7 +129,7 @@ func UpgradeCluster(ctx context.Context, conn *matlas.Client, request *matlas.Cl stateConf := &retry.StateChangeConf{ Pending: []string{"CREATING", "UPDATING", "REPAIRING"}, Target: []string{"IDLE"}, - Refresh: ResourceClusterRefreshFunc(ctx, name, projectID, conn), + Refresh: ResourceClusterRefreshFunc(ctx, name, projectID, ServiceFromClient(conn)), Timeout: timeout, MinTimeout: 30 * time.Second, Delay: 1 * time.Minute, @@ -144,9 +144,9 @@ func UpgradeCluster(ctx context.Context, conn *matlas.Client, request *matlas.Cl return cluster, resp, nil } -func ResourceClusterRefreshFunc(ctx context.Context, name, projectID string, client *matlas.Client) retry.StateRefreshFunc { +func ResourceClusterRefreshFunc(ctx context.Context, name, projectID string, client ClusterService) retry.StateRefreshFunc { return func() (any, string, error) { - c, resp, err := client.Clusters.Get(ctx, projectID, name) + c, resp, err := client.Get(ctx, projectID, name) if err != nil && strings.Contains(err.Error(), "reset by peer") { return nil, "REPEATING", nil @@ -172,9 +172,9 @@ func ResourceClusterRefreshFunc(ctx context.Context, name, projectID string, cli } } -func ResourceClusterListAdvancedRefreshFunc(ctx context.Context, projectID string, client *matlas.Client) retry.StateRefreshFunc { +func ResourceClusterListAdvancedRefreshFunc(ctx context.Context, projectID string, client ClusterService) retry.StateRefreshFunc { return func() (any, string, error) { - clusters, resp, err := client.AdvancedClusters.List(ctx, projectID, nil) + clusters, resp, err := client.List(ctx, projectID, nil) if err != nil && strings.Contains(err.Error(), "reset by peer") { return nil, "REPEATING", nil diff --git a/internal/service/advancedcluster/common_advanced_cluster_test.go b/internal/service/advancedcluster/common_advanced_cluster_test.go index 4b5a0d8f37..d8b4ae8549 100644 --- a/internal/service/advancedcluster/common_advanced_cluster_test.go +++ b/internal/service/advancedcluster/common_advanced_cluster_test.go @@ -1,13 +1,31 @@ package advancedcluster_test import ( + "context" + "net/http" "testing" "github.com/go-test/deep" "github.com/mongodb/terraform-provider-mongodbatlas/internal/service/advancedcluster" + "github.com/mongodb/terraform-provider-mongodbatlas/internal/testutil/mocksvc" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" matlas "go.mongodb.org/atlas/mongodbatlas" ) +var ( + dummyClusterName = "clusterName" + dummyProjectID = "projectId" + genericError = matlas.NewArgError("error", "generic") + advancedClusters = []*matlas.AdvancedCluster{{StateName: "NOT IDLE"}} +) + +type Result struct { + response any + error error + state string +} + func TestRemoveLabel(t *testing.T) { toRemove := matlas.Label{Key: "To Remove", Value: "To remove value"} @@ -30,3 +48,200 @@ func TestRemoveLabel(t *testing.T) { t.Fatalf("Bad removeLabel return \n got = %#v\nwant = %#v \ndiff = %#v", got, expected, diff) } } + +func TestResourceClusterRefreshFunc(t *testing.T) { + testCases := []struct { + mockCluster *matlas.Cluster + mockResponse *matlas.Response + expectedResult Result + mockError error + name string + expectedError bool + }{ + { + name: "Error in the API call: reset by peer", + mockError: matlas.NewArgError("error", "reset by peer"), + expectedError: false, + expectedResult: Result{ + response: nil, + state: "REPEATING", + error: nil, + }, + }, + { + name: "Generic error in the API call", + mockError: genericError, + expectedError: true, + expectedResult: Result{ + response: nil, + state: "", + error: genericError, + }, + }, + { + name: "Error in the API call: HTTP 404", + mockError: genericError, + mockResponse: &matlas.Response{Response: &http.Response{StatusCode: 404}, Links: nil, Raw: nil}, + expectedError: false, + expectedResult: Result{ + response: "", + state: "DELETED", + error: nil, + }, + }, + { + name: "Error in the API call: HTTP 503", + mockError: genericError, + mockResponse: &matlas.Response{Response: &http.Response{StatusCode: 503}, Links: nil, Raw: nil}, + expectedError: false, + expectedResult: Result{ + response: "", + state: "PENDING", + error: nil, + }, + }, + { + name: "Error in the API call: Neither HTTP 503 or 404", + mockError: genericError, + mockResponse: &matlas.Response{Response: &http.Response{StatusCode: 400}, Links: nil, Raw: nil}, + expectedError: true, + expectedResult: Result{ + response: nil, + state: "", + error: genericError, + }, + }, + { + name: "Successful", + mockCluster: &matlas.Cluster{StateName: "stateName"}, + mockResponse: &matlas.Response{Response: &http.Response{StatusCode: 200}, Links: nil, Raw: nil}, + expectedError: false, + expectedResult: Result{ + response: &matlas.Cluster{StateName: "stateName"}, + state: "stateName", + error: nil, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testObject := mocksvc.NewClusterService(t) + + testObject.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(tc.mockCluster, tc.mockResponse, tc.mockError) + + result, stateName, err := advancedcluster.ResourceClusterRefreshFunc(context.Background(), dummyClusterName, dummyProjectID, testObject)() + if (err != nil) != tc.expectedError { + t.Errorf("Case %s: Received unexpected error: %v", tc.name, err) + } + + assert.Equal(t, tc.expectedResult.error, err) + assert.Equal(t, tc.expectedResult.response, result) + assert.Equal(t, tc.expectedResult.state, stateName) + }) + } +} + +func TestResourceListAdvancedRefreshFunc(t *testing.T) { + testCases := []struct { + mockCluster *matlas.AdvancedClustersResponse + mockResponse *matlas.Response + expectedResult Result + mockError error + name string + expectedError bool + }{ + { + name: "Error in the API call: reset by peer", + mockError: matlas.NewArgError("error", "reset by peer"), + expectedError: false, + expectedResult: Result{ + response: nil, + state: "REPEATING", + error: nil, + }, + }, + { + name: "Generic error in the API call", + mockError: genericError, + expectedError: true, + expectedResult: Result{ + response: nil, + state: "", + error: genericError, + }, + }, + { + name: "Error in the API call: HTTP 404", + mockError: genericError, + mockResponse: &matlas.Response{Response: &http.Response{StatusCode: 404}, Links: nil, Raw: nil}, + expectedError: false, + expectedResult: Result{ + response: "", + state: "DELETED", + error: nil, + }, + }, + { + name: "Error in the API call: HTTP 503", + mockError: genericError, + mockResponse: &matlas.Response{Response: &http.Response{StatusCode: 503}, Links: nil, Raw: nil}, + expectedError: false, + expectedResult: Result{ + response: "", + state: "PENDING", + error: nil, + }, + }, + { + name: "Error in the API call: Neither HTTP 503 or 404", + mockError: genericError, + mockResponse: &matlas.Response{Response: &http.Response{StatusCode: 400}, Links: nil, Raw: nil}, + expectedError: true, + expectedResult: Result{ + response: nil, + state: "", + error: genericError, + }, + }, + { + name: "Successful but with at least one cluster not idle", + mockCluster: &matlas.AdvancedClustersResponse{Results: advancedClusters}, + mockResponse: &matlas.Response{Response: &http.Response{StatusCode: 200}, Links: nil, Raw: nil}, + expectedError: false, + expectedResult: Result{ + response: advancedClusters[0], + state: "PENDING", + error: nil, + }, + }, + { + name: "Successful", + mockCluster: &matlas.AdvancedClustersResponse{}, + mockResponse: &matlas.Response{Response: &http.Response{StatusCode: 200}, Links: nil, Raw: nil}, + expectedError: false, + expectedResult: Result{ + response: &matlas.AdvancedClustersResponse{}, + state: "IDLE", + error: nil, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testObject := mocksvc.NewClusterService(t) + + testObject.On("List", mock.Anything, mock.Anything, mock.Anything).Return(tc.mockCluster, tc.mockResponse, tc.mockError) + + result, stateName, err := advancedcluster.ResourceClusterListAdvancedRefreshFunc(context.Background(), dummyProjectID, testObject)() + if (err != nil) != tc.expectedError { + t.Errorf("Case %s: Received unexpected error: %v", tc.name, err) + } + + assert.Equal(t, tc.expectedResult.error, err) + assert.Equal(t, tc.expectedResult.response, result) + assert.Equal(t, tc.expectedResult.state, stateName) + }) + } +} diff --git a/internal/service/advancedcluster/service_advanced_cluster.go b/internal/service/advancedcluster/service_advanced_cluster.go new file mode 100644 index 0000000000..95571fe8ef --- /dev/null +++ b/internal/service/advancedcluster/service_advanced_cluster.go @@ -0,0 +1,35 @@ +package advancedcluster + +import ( + "context" + + matlas "go.mongodb.org/atlas/mongodbatlas" +) + +type ClusterService interface { + Get(ctx context.Context, groupID, clusterName string) (*matlas.Cluster, *matlas.Response, error) + List(ctx context.Context, groupID string, options *matlas.ListOptions) (*matlas.AdvancedClustersResponse, *matlas.Response, error) + GetAdvancedCluster(ctx context.Context, groupID, clusterName string) (*matlas.AdvancedCluster, *matlas.Response, error) +} + +type ClusterServiceFromClient struct { + client *matlas.Client +} + +func (a *ClusterServiceFromClient) Get(ctx context.Context, groupID, clusterName string) (*matlas.Cluster, *matlas.Response, error) { + return a.client.Clusters.Get(ctx, groupID, clusterName) +} + +func (a *ClusterServiceFromClient) GetAdvancedCluster(ctx context.Context, groupID, clusterName string) (*matlas.AdvancedCluster, *matlas.Response, error) { + return a.client.AdvancedClusters.Get(ctx, groupID, clusterName) +} + +func (a *ClusterServiceFromClient) List(ctx context.Context, groupID string, options *matlas.ListOptions) (*matlas.AdvancedClustersResponse, *matlas.Response, error) { + return a.client.AdvancedClusters.List(ctx, groupID, options) +} + +func ServiceFromClient(client *matlas.Client) ClusterService { + return &ClusterServiceFromClient{ + client: client, + } +} diff --git a/internal/service/cloudbackupsnapshot/resource_cloud_backup_snapshot.go b/internal/service/cloudbackupsnapshot/resource_cloud_backup_snapshot.go index 60d18666c8..ff754c5c26 100644 --- a/internal/service/cloudbackupsnapshot/resource_cloud_backup_snapshot.go +++ b/internal/service/cloudbackupsnapshot/resource_cloud_backup_snapshot.go @@ -217,7 +217,7 @@ func resourceMongoDBAtlasCloudBackupSnapshotCreate(ctx context.Context, d *schem stateConf := &retry.StateChangeConf{ Pending: []string{"CREATING", "UPDATING", "REPAIRING", "REPEATING"}, Target: []string{"IDLE"}, - Refresh: advancedcluster.ResourceClusterRefreshFunc(ctx, d.Get("cluster_name").(string), d.Get("project_id").(string), conn), + Refresh: advancedcluster.ResourceClusterRefreshFunc(ctx, d.Get("cluster_name").(string), d.Get("project_id").(string), advancedcluster.ServiceFromClient(conn)), Timeout: 10 * time.Minute, MinTimeout: 10 * time.Second, Delay: 3 * time.Minute, diff --git a/internal/service/cluster/resource_cluster.go b/internal/service/cluster/resource_cluster.go index 4014ac77a8..858c3c5a5f 100644 --- a/internal/service/cluster/resource_cluster.go +++ b/internal/service/cluster/resource_cluster.go @@ -531,7 +531,7 @@ func resourceMongoDBAtlasClusterCreate(ctx context.Context, d *schema.ResourceDa stateConf := &retry.StateChangeConf{ Pending: []string{"CREATING", "UPDATING", "REPAIRING", "REPEATING", "PENDING"}, Target: []string{"IDLE"}, - Refresh: advancedcluster.ResourceClusterRefreshFunc(ctx, d.Get("name").(string), projectID, conn), + Refresh: advancedcluster.ResourceClusterRefreshFunc(ctx, d.Get("name").(string), projectID, advancedcluster.ServiceFromClient(conn)), Timeout: timeout, MinTimeout: 1 * time.Minute, Delay: 3 * time.Minute, @@ -1001,7 +1001,7 @@ func resourceMongoDBAtlasClusterDelete(ctx context.Context, d *schema.ResourceDa stateConf := &retry.StateChangeConf{ Pending: []string{"IDLE", "CREATING", "UPDATING", "REPAIRING", "DELETING"}, Target: []string{"DELETED"}, - Refresh: advancedcluster.ResourceClusterRefreshFunc(ctx, clusterName, projectID, conn), + Refresh: advancedcluster.ResourceClusterRefreshFunc(ctx, clusterName, projectID, advancedcluster.ServiceFromClient(conn)), Timeout: d.Timeout(schema.TimeoutDelete), MinTimeout: 30 * time.Second, Delay: 1 * time.Minute, // Wait 30 secs before starting @@ -1375,7 +1375,7 @@ func updateCluster(ctx context.Context, conn *matlas.Client, request *matlas.Clu stateConf := &retry.StateChangeConf{ Pending: []string{"CREATING", "UPDATING", "REPAIRING"}, Target: []string{"IDLE"}, - Refresh: advancedcluster.ResourceClusterRefreshFunc(ctx, name, projectID, conn), + Refresh: advancedcluster.ResourceClusterRefreshFunc(ctx, name, projectID, advancedcluster.ServiceFromClient(conn)), Timeout: timeout, MinTimeout: 30 * time.Second, Delay: 1 * time.Minute, diff --git a/internal/service/privateendpointregionalmode/resource_private_endpoint_regional_mode.go b/internal/service/privateendpointregionalmode/resource_private_endpoint_regional_mode.go index 416af576d0..67d322f1c7 100644 --- a/internal/service/privateendpointregionalmode/resource_private_endpoint_regional_mode.go +++ b/internal/service/privateendpointregionalmode/resource_private_endpoint_regional_mode.go @@ -111,7 +111,7 @@ func resourceMongoDBAtlasPrivateEndpointRegionalModeUpdate(ctx context.Context, stateConf := &retry.StateChangeConf{ Pending: []string{"REPEATING", "PENDING"}, Target: []string{"IDLE", "DELETED"}, - Refresh: advancedcluster.ResourceClusterListAdvancedRefreshFunc(ctx, projectID, conn), + Refresh: advancedcluster.ResourceClusterListAdvancedRefreshFunc(ctx, projectID, advancedcluster.ServiceFromClient(conn)), Timeout: d.Timeout(timeoutKey.(string)), MinTimeout: 5 * time.Second, Delay: 3 * time.Second, diff --git a/internal/service/privatelinkendpointservice/resource_privatelink_endpoint_service.go b/internal/service/privatelinkendpointservice/resource_privatelink_endpoint_service.go index 5e7e837ae2..086a7596ae 100644 --- a/internal/service/privatelinkendpointservice/resource_privatelink_endpoint_service.go +++ b/internal/service/privatelinkendpointservice/resource_privatelink_endpoint_service.go @@ -192,7 +192,7 @@ func resourceMongoDBAtlasPrivateEndpointServiceLinkCreate(ctx context.Context, d clusterConf := &retry.StateChangeConf{ Pending: []string{"REPEATING", "PENDING"}, Target: []string{"IDLE", "DELETED"}, - Refresh: advancedcluster.ResourceClusterListAdvancedRefreshFunc(ctx, projectID, conn), + Refresh: advancedcluster.ResourceClusterListAdvancedRefreshFunc(ctx, projectID, advancedcluster.ServiceFromClient(conn)), Timeout: d.Timeout(schema.TimeoutCreate), MinTimeout: 5 * time.Second, Delay: 5 * time.Minute, @@ -316,7 +316,7 @@ func resourceMongoDBAtlasPrivateEndpointServiceLinkDelete(ctx context.Context, d clusterConf := &retry.StateChangeConf{ Pending: []string{"REPEATING", "PENDING"}, Target: []string{"IDLE", "DELETED"}, - Refresh: advancedcluster.ResourceClusterListAdvancedRefreshFunc(ctx, projectID, conn), + Refresh: advancedcluster.ResourceClusterListAdvancedRefreshFunc(ctx, projectID, advancedcluster.ServiceFromClient(conn)), Timeout: d.Timeout(schema.TimeoutDelete), MinTimeout: 5 * time.Second, Delay: 5 * time.Minute, diff --git a/internal/testutil/mocksvc/cluster_service.go b/internal/testutil/mocksvc/cluster_service.go new file mode 100644 index 0000000000..5ebff17576 --- /dev/null +++ b/internal/testutil/mocksvc/cluster_service.go @@ -0,0 +1,146 @@ +// Code generated by mockery. DO NOT EDIT. + +package mocksvc + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + mongodbatlas "go.mongodb.org/atlas/mongodbatlas" +) + +// ClusterService is an autogenerated mock type for the ClusterService type +type ClusterService struct { + mock.Mock +} + +// Get provides a mock function with given fields: ctx, groupID, clusterName +func (_m *ClusterService) Get(ctx context.Context, groupID string, clusterName string) (*mongodbatlas.Cluster, *mongodbatlas.Response, error) { + ret := _m.Called(ctx, groupID, clusterName) + + if len(ret) == 0 { + panic("no return value specified for Get") + } + + var r0 *mongodbatlas.Cluster + var r1 *mongodbatlas.Response + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*mongodbatlas.Cluster, *mongodbatlas.Response, error)); ok { + return rf(ctx, groupID, clusterName) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *mongodbatlas.Cluster); ok { + r0 = rf(ctx, groupID, clusterName) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*mongodbatlas.Cluster) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) *mongodbatlas.Response); ok { + r1 = rf(ctx, groupID, clusterName) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*mongodbatlas.Response) + } + } + + if rf, ok := ret.Get(2).(func(context.Context, string, string) error); ok { + r2 = rf(ctx, groupID, clusterName) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// GetAdvancedCluster provides a mock function with given fields: ctx, groupID, clusterName +func (_m *ClusterService) GetAdvancedCluster(ctx context.Context, groupID string, clusterName string) (*mongodbatlas.AdvancedCluster, *mongodbatlas.Response, error) { + ret := _m.Called(ctx, groupID, clusterName) + + if len(ret) == 0 { + panic("no return value specified for GetAdvancedCluster") + } + + var r0 *mongodbatlas.AdvancedCluster + var r1 *mongodbatlas.Response + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*mongodbatlas.AdvancedCluster, *mongodbatlas.Response, error)); ok { + return rf(ctx, groupID, clusterName) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *mongodbatlas.AdvancedCluster); ok { + r0 = rf(ctx, groupID, clusterName) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*mongodbatlas.AdvancedCluster) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) *mongodbatlas.Response); ok { + r1 = rf(ctx, groupID, clusterName) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*mongodbatlas.Response) + } + } + + if rf, ok := ret.Get(2).(func(context.Context, string, string) error); ok { + r2 = rf(ctx, groupID, clusterName) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// List provides a mock function with given fields: ctx, groupID, options +func (_m *ClusterService) List(ctx context.Context, groupID string, options *mongodbatlas.ListOptions) (*mongodbatlas.AdvancedClustersResponse, *mongodbatlas.Response, error) { + ret := _m.Called(ctx, groupID, options) + + if len(ret) == 0 { + panic("no return value specified for List") + } + + var r0 *mongodbatlas.AdvancedClustersResponse + var r1 *mongodbatlas.Response + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, string, *mongodbatlas.ListOptions) (*mongodbatlas.AdvancedClustersResponse, *mongodbatlas.Response, error)); ok { + return rf(ctx, groupID, options) + } + if rf, ok := ret.Get(0).(func(context.Context, string, *mongodbatlas.ListOptions) *mongodbatlas.AdvancedClustersResponse); ok { + r0 = rf(ctx, groupID, options) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*mongodbatlas.AdvancedClustersResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, *mongodbatlas.ListOptions) *mongodbatlas.Response); ok { + r1 = rf(ctx, groupID, options) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*mongodbatlas.Response) + } + } + + if rf, ok := ret.Get(2).(func(context.Context, string, *mongodbatlas.ListOptions) error); ok { + r2 = rf(ctx, groupID, options) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// NewClusterService creates a new instance of ClusterService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewClusterService(t interface { + mock.TestingT + Cleanup(func()) +}) *ClusterService { + mock := &ClusterService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +}