From a5dadb686bc90e05988f82f3f26908ebd1b436e2 Mon Sep 17 00:00:00 2001 From: Jovan Manojlovic <152610450+fivetran-jovanmanojlovic@users.noreply.github.com> Date: Wed, 14 Aug 2024 18:22:48 +0200 Subject: [PATCH] Add revert functionality (#334) * Inital solution for revert changes after failTeamConnectorMembership * Implement rest of resources * Add tests and logs * Fix tests * Fix typo and add log for failed reverts --- .../resources/team_connector_membership.go | 475 +++++++++++------- .../team_connector_membership_test.go | 113 ++++- .../resources/team_group_membership.go | 114 ++++- .../resources/team_group_membership_test.go | 107 +++- .../resources/team_user_membership.go | 117 ++++- .../resources/team_user_membership_test.go | 100 +++- .../resources/user_connector_membership.go | 117 ++++- .../user_connector_membership_test.go | 99 +++- .../resources/user_group_membership.go | 120 ++++- .../resources/user_group_membership_test.go | 102 +++- 10 files changed, 1245 insertions(+), 219 deletions(-) diff --git a/fivetran/framework/resources/team_connector_membership.go b/fivetran/framework/resources/team_connector_membership.go index d268e489..4175fa8a 100644 --- a/fivetran/framework/resources/team_connector_membership.go +++ b/fivetran/framework/resources/team_connector_membership.go @@ -1,23 +1,23 @@ package resources import ( - "context" - "fmt" - - "github.com/fivetran/terraform-provider-fivetran/fivetran/framework/core" - "github.com/fivetran/terraform-provider-fivetran/fivetran/framework/core/model" - fivetranSchema "github.com/fivetran/terraform-provider-fivetran/fivetran/framework/core/schema" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "context" + "fmt" + + "github.com/fivetran/terraform-provider-fivetran/fivetran/framework/core" + "github.com/fivetran/terraform-provider-fivetran/fivetran/framework/core/model" + fivetranSchema "github.com/fivetran/terraform-provider-fivetran/fivetran/framework/core/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) func TeamConnectorMembership() resource.Resource { - return &teamConnectorMembership{} + return &teamConnectorMembership{} } type teamConnectorMembership struct { - core.ProviderResource + core.ProviderResource } // Ensure the implementation satisfies the desired interfaces. @@ -25,200 +25,307 @@ var _ resource.ResourceWithConfigure = &teamConnectorMembership{} var _ resource.ResourceWithImportState = &teamConnectorMembership{} func (r *teamConnectorMembership) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_team_connector_membership" + resp.TypeName = req.ProviderTypeName + "_team_connector_membership" } func (r *teamConnectorMembership) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = fivetranSchema.TeamConnectorMembershipResource() + resp.Schema = fivetranSchema.TeamConnectorMembershipResource() } func (r *teamConnectorMembership) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } func (r *teamConnectorMembership) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - if r.GetClient() == nil { - resp.Diagnostics.AddError( - "Unconfigured Fivetran Client", - "Please report this issue to the provider developers.", - ) - - return - } - - var data model.TeamConnectorMemberships - - resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) - - for _, connector := range data.Connector.Elements() { - if connectorElement, ok := connector.(basetypes.ObjectValue); ok { - svc := r.GetClient().NewTeamConnectorMembershipCreate() - svc.TeamId(data.TeamId.ValueString()) - svc.ConnectorId(connectorElement.Attributes()["connector_id"].(basetypes.StringValue).ValueString()) - svc.Role(connectorElement.Attributes()["role"].(basetypes.StringValue).ValueString()) - if teamConnectorResponse, err := svc.Do(ctx); err != nil { - resp.Diagnostics.AddError( - "Unable to Create Team Connector Memberships Resource.", - fmt.Sprintf("%v; code: %v; message: %v", err, teamConnectorResponse.Code, teamConnectorResponse.Message), - ) - - return - } - } - } - - teamConnectorResponse, err := data.ReadFromSource(ctx, r.GetClient(), data.TeamId.ValueString()) - if err != nil { - resp.Diagnostics.AddError( - "Unable to Create Team Connector Memberships Resource.", - fmt.Sprintf("%v; code: %v", err, teamConnectorResponse.Code), - ) - - return - } - - data.ReadFromResponse(ctx, teamConnectorResponse) - - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + if r.GetClient() == nil { + resp.Diagnostics.AddError( + "Unconfigured Fivetran Client", + "Please report this issue to the provider developers.", + ) + + return + } + + var data model.TeamConnectorMemberships + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + savedConnectors := make([]string, 0, len(data.Connector.Elements())) + for _, connector := range data.Connector.Elements() { + if connectorElement, ok := connector.(basetypes.ObjectValue); ok { + connectorId := connectorElement.Attributes()["connector_id"].(basetypes.StringValue).ValueString() + svc := r.GetClient().NewTeamConnectorMembershipCreate() + svc.TeamId(data.TeamId.ValueString()) + svc.ConnectorId(connectorId) + svc.Role(connectorElement.Attributes()["role"].(basetypes.StringValue).ValueString()) + if teamConnectorResponse, err := svc.Do(ctx); err != nil { + resp.Diagnostics.AddError( + "Unable to Create Team Connector Memberships Resource.", + fmt.Sprintf("%v; code: %v; message: %v", err, teamConnectorResponse.Code, teamConnectorResponse.Message), + ) + + creRvertMsg, creRevertErr := r.RevertCreated(ctx, savedConnectors, data.TeamId.ValueString()) + resp.Diagnostics.AddWarning("Action reverted", creRvertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", creRevertErr) + return + } + savedConnectors = append(savedConnectors, connectorId) + } + } + + teamConnectorResponse, err := data.ReadFromSource(ctx, r.GetClient(), data.TeamId.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Unable to Create Team Connector Memberships Resource.", + fmt.Sprintf("%v; code: %v", err, teamConnectorResponse.Code), + ) + + return + } + + data.ReadFromResponse(ctx, teamConnectorResponse) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } func (r *teamConnectorMembership) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - if r.GetClient() == nil { - resp.Diagnostics.AddError( - "Unconfigured Fivetran Client", - "Please report this issue to the provider developers.", - ) + if r.GetClient() == nil { + resp.Diagnostics.AddError( + "Unconfigured Fivetran Client", + "Please report this issue to the provider developers.", + ) - return - } + return + } - var data model.TeamConnectorMemberships - resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + var data model.TeamConnectorMemberships + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - teamConnectorResponse, err := data.ReadFromSource(ctx, r.GetClient(), data.TeamId.ValueString()) - if err != nil { - resp.Diagnostics.AddError( - "Unable to Read Team Connector Memberships Resource.", - fmt.Sprintf("%v; code: %v", err, teamConnectorResponse.Code), - ) + teamConnectorResponse, err := data.ReadFromSource(ctx, r.GetClient(), data.TeamId.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Unable to Read Team Connector Memberships Resource.", + fmt.Sprintf("%v; code: %v", err, teamConnectorResponse.Code), + ) - return - } + return + } - data.ReadFromResponse(ctx, teamConnectorResponse) + data.ReadFromResponse(ctx, teamConnectorResponse) - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } func (r *teamConnectorMembership) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - if r.GetClient() == nil { - resp.Diagnostics.AddError( - "Unconfigured Fivetran Client", - "Please report this issue to the provider developers.", - ) - - return - } - - var plan, state model.TeamConnectorMemberships - - resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) - resp.Diagnostics.Append(req.State.Get(ctx, &state)...) - - planConnectorsMap := make(map[string]string) - for _, connector := range plan.Connector.Elements() { - if connectorElement, ok := connector.(basetypes.ObjectValue); ok { - planConnectorsMap[connectorElement.Attributes()["connector_id"].(basetypes.StringValue).ValueString()] = connectorElement.Attributes()["role"].(basetypes.StringValue).ValueString() - } - } - - stateConnectorsMap := make(map[string]string) - for _, connector := range state.Connector.Elements() { - if connectorElement, ok := connector.(basetypes.ObjectValue); ok { - stateConnectorsMap[connectorElement.Attributes()["connector_id"].(basetypes.StringValue).ValueString()] = connectorElement.Attributes()["role"].(basetypes.StringValue).ValueString() - } - } - - /* sync */ - for stateKey, stateValue := range stateConnectorsMap { - role, found := planConnectorsMap[stateKey] - - if !found { - if updateResponse, err := r.GetClient().NewTeamConnectorMembershipDelete().TeamId(plan.TeamId.ValueString()).ConnectorId(stateKey).Do(ctx); err != nil { - resp.Diagnostics.AddError( - "Unable to Update Team Connector Membership Resource.", - fmt.Sprintf("%v; code: %v; message: %v", err, updateResponse.Code, updateResponse.Message), - ) - return - } - } else if role != stateValue { - if updateResponse, err := r.GetClient().NewTeamConnectorMembershipModify().TeamId(plan.TeamId.ValueString()).ConnectorId(stateKey).Role(role).Do(ctx); err != nil { - resp.Diagnostics.AddError( - "Unable to Update Team Connector Membership Resource.", - fmt.Sprintf("%v; code: %v; message: %v", err, updateResponse.Code, updateResponse.Message), - ) - return - } - } - } - - for planKey, planValue := range planConnectorsMap { - _, exists := stateConnectorsMap[planKey] - - if !exists { - if updateResponse, err := r.GetClient().NewTeamConnectorMembershipCreate().TeamId(plan.TeamId.ValueString()).ConnectorId(planKey).Role(planValue).Do(ctx); err != nil { - resp.Diagnostics.AddError( - "Unable to Update Team Connector Membership Resource.", - fmt.Sprintf("%v; code: %v; message: %v", err, updateResponse.Code, updateResponse.Message), - ) - return - } - } - } - - teamConnectorResponse, err := plan.ReadFromSource(ctx, r.GetClient(), plan.TeamId.ValueString()) - if err != nil { - resp.Diagnostics.AddError( - "Unable to Create Team Connector Memberships Resource.", - fmt.Sprintf("%v; code: %v", err, teamConnectorResponse.Code), - ) - - return - } - - plan.ReadFromResponse(ctx, teamConnectorResponse) - - resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + if r.GetClient() == nil { + resp.Diagnostics.AddError( + "Unconfigured Fivetran Client", + "Please report this issue to the provider developers.", + ) + + return + } + + var plan, state model.TeamConnectorMemberships + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + planConnectorsMap := make(map[string]string) + for _, connector := range plan.Connector.Elements() { + if connectorElement, ok := connector.(basetypes.ObjectValue); ok { + planConnectorsMap[connectorElement.Attributes()["connector_id"].(basetypes.StringValue).ValueString()] = connectorElement.Attributes()["role"].(basetypes.StringValue).ValueString() + } + } + + stateConnectorsMap := make(map[string]string) + for _, connector := range state.Connector.Elements() { + if connectorElement, ok := connector.(basetypes.ObjectValue); ok { + stateConnectorsMap[connectorElement.Attributes()["connector_id"].(basetypes.StringValue).ValueString()] = connectorElement.Attributes()["role"].(basetypes.StringValue).ValueString() + } + } + + /* sync */ + deletedConnectors := make([]string, 0) + modifiedConnectors := make([]string, 0) + for stateKey, stateValue := range stateConnectorsMap { + role, found := planConnectorsMap[stateKey] + + if !found { + if updateResponse, err := r.GetClient().NewTeamConnectorMembershipDelete().TeamId(plan.TeamId.ValueString()).ConnectorId(stateKey).Do(ctx); err != nil { + resp.Diagnostics.AddError( + "Unable to Update Team Connector Membership Resource.", + fmt.Sprintf("%v; code: %v; message: %v", err, updateResponse.Code, updateResponse.Message), + ) + + delRevertMsg, delRevertErr := r.RevertDeleted(ctx, deletedConnectors, plan.TeamId.ValueString(), stateConnectorsMap) + resp.Diagnostics.AddWarning("Action reverted", delRevertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", delRevertErr) + + return + } + deletedConnectors = append(deletedConnectors, stateKey) + } else if role != stateValue { + if updateResponse, err := r.GetClient().NewTeamConnectorMembershipModify().TeamId(plan.TeamId.ValueString()).ConnectorId(stateKey).Role(role).Do(ctx); err != nil { + resp.Diagnostics.AddError( + "Unable to Update Team Connector Membership Resource.", + fmt.Sprintf("%v; code: %v; message: %v", err, updateResponse.Code, updateResponse.Message), + ) + + delRevertMsg, delRevertErr := r.RevertDeleted(ctx, deletedConnectors, plan.TeamId.ValueString(), stateConnectorsMap) + resp.Diagnostics.AddWarning("Action reverted", delRevertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", delRevertErr) + + modRevertMsg, modRevertErr := r.RevertModified(ctx, modifiedConnectors, plan.TeamId.ValueString(), stateConnectorsMap) + resp.Diagnostics.AddWarning("Action reverted", modRevertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", modRevertErr) + return + } + modifiedConnectors = append(modifiedConnectors, stateKey) + } + } + + createdConnectors := make([]string, 0) + for planKey, planValue := range planConnectorsMap { + _, exists := stateConnectorsMap[planKey] + + if !exists { + if updateResponse, err := r.GetClient().NewTeamConnectorMembershipCreate().TeamId(plan.TeamId.ValueString()).ConnectorId(planKey).Role(planValue).Do(ctx); err != nil { + resp.Diagnostics.AddError( + "Unable to Update Team Connector Membership Resource.", + fmt.Sprintf("%v; code: %v; message: %v", err, updateResponse.Code, updateResponse.Message), + ) + + delRevertMsg, delRevertErr := r.RevertDeleted(ctx, deletedConnectors, plan.TeamId.ValueString(), stateConnectorsMap) + resp.Diagnostics.AddWarning("Action reverted", delRevertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", delRevertErr) + + modRevertMsg, modRevertErr := r.RevertModified(ctx, modifiedConnectors, plan.TeamId.ValueString(), stateConnectorsMap) + resp.Diagnostics.AddWarning("Action reverted", modRevertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", modRevertErr) + + creRvertMsg, creRevertErr := r.RevertCreated(ctx, createdConnectors, plan.TeamId.ValueString()) + resp.Diagnostics.AddWarning("Action reverted", creRvertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", creRevertErr) + return + } + createdConnectors = append(createdConnectors, planKey) + } + } + + teamConnectorResponse, err := plan.ReadFromSource(ctx, r.GetClient(), plan.TeamId.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Unable to Create Team Connector Memberships Resource.", + fmt.Sprintf("%v; code: %v", err, teamConnectorResponse.Code), + ) + + return + } + + plan.ReadFromResponse(ctx, teamConnectorResponse) + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) } func (r *teamConnectorMembership) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - if r.GetClient() == nil { - resp.Diagnostics.AddError( - "Unconfigured Fivetran Client", - "Please report this issue to the provider developers.", - ) - - return - } - - var data model.TeamConnectorMemberships - - resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - - for _, connector := range data.Connector.Elements() { - if connectorElement, ok := connector.(basetypes.ObjectValue); ok { - svc := r.GetClient().NewTeamConnectorMembershipDelete() - svc.TeamId(data.TeamId.ValueString()) - svc.ConnectorId(connectorElement.Attributes()["connector_id"].(basetypes.StringValue).ValueString()) - - if deleteResponse, err := svc.Do(ctx); err != nil { - resp.Diagnostics.AddError( - "Unable to Delete Team Connector Memberships Resource.", - fmt.Sprintf("%v; code: %v; message: %v", err, deleteResponse.Code, deleteResponse.Message), - ) - - return - } - } - } -} \ No newline at end of file + if r.GetClient() == nil { + resp.Diagnostics.AddError( + "Unconfigured Fivetran Client", + "Please report this issue to the provider developers.", + ) + return + } + + var data, state model.TeamConnectorMemberships + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + stateConnectorsMap := make(map[string]string) + for _, connector := range state.Connector.Elements() { + if connectorElement, ok := connector.(basetypes.ObjectValue); ok { + stateConnectorsMap[connectorElement.Attributes()["connector_id"].(basetypes.StringValue).ValueString()] = connectorElement.Attributes()["role"].(basetypes.StringValue).ValueString() + } + } + + deletedConnectors := make([]string, 0) + for _, connector := range data.Connector.Elements() { + if connectorElement, ok := connector.(basetypes.ObjectValue); ok { + connectorId := connectorElement.Attributes()["connector_id"].(basetypes.StringValue).ValueString() + svc := r.GetClient().NewTeamConnectorMembershipDelete() + svc.TeamId(data.TeamId.ValueString()) + svc.ConnectorId(connectorId) + + if deleteResponse, err := svc.Do(ctx); err != nil { + resp.Diagnostics.AddError( + "Unable to Delete Team Connector Memberships Resource.", + fmt.Sprintf("%v; code: %v; message: %v", err, deleteResponse.Code, deleteResponse.Message), + ) + + delRevertMsg, delRevertErr := r.RevertDeleted(ctx, deletedConnectors, data.TeamId.ValueString(), stateConnectorsMap); + resp.Diagnostics.AddWarning("Action reverted", delRevertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", delRevertErr) + return + } + deletedConnectors = append(deletedConnectors, connectorId) + } + } +} + +func (r *teamConnectorMembership) RevertDeleted(ctx context.Context, toRevert []string, teamId string, stateConnectorsMap map[string]string) (string, string) { + reverted := []string{} + failed := []string{} + for _, connectorId := range toRevert { + svc := r.GetClient().NewTeamConnectorMembershipCreate() + svc.TeamId(teamId) + svc.ConnectorId(connectorId) + svc.Role(stateConnectorsMap[connectorId]) + + if _, err := svc.Do(ctx); err != nil { + failed = append(failed, connectorId) + } else { + reverted = append(reverted, connectorId) + } + } + return fmt.Sprintf("Delete action reverted for connectors: %v", reverted), + fmt.Sprintf("Delete for revert action failed for connectors: %v", failed) +} + +func (r *teamConnectorMembership) RevertModified(ctx context.Context, toRevert []string, teamId string, stateConnectorsMap map[string]string) (string, string) { + reverted := []string{} + failed := []string{} + for _, connectorId := range toRevert { + svc := r.GetClient().NewTeamConnectorMembershipModify() + svc.TeamId(teamId) + svc.ConnectorId(connectorId) + svc.Role(stateConnectorsMap[connectorId]) + + if _, err := svc.Do(ctx); err != nil { + failed = append(failed, connectorId) + } else { + reverted = append(reverted, connectorId) + } + } + return fmt.Sprintf("Modify action reverted for connectors: %v", reverted), + fmt.Sprintf("Modify for revert action failed for connectors: %v", failed) +} + +func (r *teamConnectorMembership) RevertCreated(ctx context.Context, toRevert []string, teamId string) (string, string) { + reverted := []string{} + failed := []string{} + for _, connectorId := range toRevert { + svc := r.GetClient().NewTeamConnectorMembershipDelete() + svc.TeamId(teamId) + svc.ConnectorId(connectorId) + + if _, err := svc.Do(ctx); err != nil { + failed = append(failed, connectorId) + } else { + reverted = append(reverted, connectorId) + } + reverted = append(reverted, connectorId) + } + return fmt.Sprintf("Created action reverted for connectors: %v", reverted), + fmt.Sprintf("Created for revert action failed for connectors: %v", failed) +} diff --git a/fivetran/framework/resources/team_connector_membership_test.go b/fivetran/framework/resources/team_connector_membership_test.go index 92d26d7d..79f8c21d 100644 --- a/fivetran/framework/resources/team_connector_membership_test.go +++ b/fivetran/framework/resources/team_connector_membership_test.go @@ -1,11 +1,13 @@ package resources_test import ( + "fmt" "net/http" + "regexp" "testing" - - tfmock "github.com/fivetran/terraform-provider-fivetran/fivetran/tests/mock" + "github.com/fivetran/go-fivetran/tests/mock" + tfmock "github.com/fivetran/terraform-provider-fivetran/fivetran/tests/mock" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" ) @@ -14,9 +16,11 @@ var ( teamConnectorMembershipPostHandler *mock.Handler teamConnectorMembershipPatchHandler *mock.Handler teamConnectorMembershipDeleteHandler *mock.Handler - teamConnectorMembershipData map[string]interface{} - teamConnectorMembershipListData map[string]interface{} - teamConnectorMembershipResponse string + teamConnectorMembershipData map[string]interface{} + teamConnectorMembershipListData map[string]interface{} + teamConnectorMembershipResponse string + teamConnectorMembershipResponse2 string + teamConnectormembershipDeleteCount int ) func setupMockClientTeamConnectorMembershipResource(t *testing.T) { @@ -120,3 +124,102 @@ func TestConnectorMembershipResourceTeamMock(t *testing.T) { }, ) } + +func setupMockClientTeamConnectorMembershipResourceNotFound(t *testing.T) { + tfmock.MockClient().Reset() + teamConnectorMembershipResponse = + `{ + "id": "test_connector", + "role": "Connector Reviewer", + "created_at": "2020-05-25T15:26:47.306509Z" + }` + teamConnectorMembershipResponse2 = + `{ + "id": "test_connector", + "role": "Connector Reviewer", + "created_at": "2020-05-25T15:26:47.306509Z" + }` + + teamConnectorMembershipResponse = `{ + "items": [ + { + "id": "test_connector", + "role": "Connector Reviewer", + "created_at": "2020-05-25T15:26:47.306509Z" + } + ], + "next_cursor": null}` + + callCount := 0 + teamConnectorMembershipPostHandler = tfmock.MockClient().When(http.MethodPost, "/v1/teams/test_team/connectors").ThenCall( + func(req *http.Request) (*http.Response, error) { + callCount++ + if callCount != 1 { + teamConnectorMembershipData = tfmock.CreateMapFromJsonString(t, teamConnectorMembershipResponse) + return tfmock.FivetranSuccessResponse(t, req, http.StatusCreated, "Connector membership has been created", teamConnectorMembershipData), nil + } + teamConnectorMembershipData = tfmock.CreateMapFromJsonString(t, teamConnectorMembershipResponse) + return tfmock.FivetranSuccessResponse(t, req, http.StatusNotFound, "Connector membership not found", teamConnectorMembershipData), nil + }, + ) + + teamConnectormembershipDeleteCount := 0; + teamConnectorMembershipDeleteHandler = tfmock.MockClient().When(http.MethodDelete, "/v1/teams/test_team/connectors/test_connector").ThenCall( + func(req *http.Request) (*http.Response, error) { + teamConnectormembershipDeleteCount++ + return tfmock.FivetranSuccessResponse(t, req, 200, "Connector membership has been deleted", nil), nil + }, + ) + + tfmock.MockClient().When(http.MethodGet, "/v1/teams/test_team/connectors").ThenCall( + func(req *http.Request) (*http.Response, error) { + teamConnectorMembershipListData = tfmock.CreateMapFromJsonString(t, teamConnectorMembershipResponse) + response := tfmock.FivetranSuccessResponse(t, req, http.StatusOK, "", teamConnectorMembershipListData) + return response, nil + }, + ) + +} + +func TestConnectorMembershipResourceTeamMockNotFound(t *testing.T) { + step1 := resource.TestStep{ + Config: ` + resource "fivetran_team_connector_membership" "test_team_connector_membership" { + provider = fivetran-provider + + team_id = "test_team" + + connector { + connector_id = "test_connector" + role = "Connector Reviewer" + } + connector { + connector_id = "test_connector2" + role = "Connector Reviewer" + } + + }`, + ExpectError: regexp.MustCompile(`Error: Unable to Create Team Connector Memberships Resource`), + } + + resource.Test( + t, + resource.TestCase{ + PreCheck: func() { + setupMockClientTeamConnectorMembershipResourceNotFound(t) + }, + ProtoV6ProviderFactories: tfmock.ProtoV6ProviderFactories, + CheckDestroy: func(s *terraform.State) error { + tfmock.AssertEqual(t, teamConnectorMembershipDeleteHandler.Interactions, 1) + if (teamConnectormembershipDeleteCount != 1) { + return fmt.Errorf("Failed acctions are not reverted.") + } + return nil + }, + + Steps: []resource.TestStep{ + step1, + }, + }, + ) +} diff --git a/fivetran/framework/resources/team_group_membership.go b/fivetran/framework/resources/team_group_membership.go index 04880614..e01874eb 100644 --- a/fivetran/framework/resources/team_group_membership.go +++ b/fivetran/framework/resources/team_group_membership.go @@ -50,20 +50,27 @@ func (r *teamGroupMembership) Create(ctx context.Context, req resource.CreateReq resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + savedGroups := make([]string, 0, len(data.Group.Elements())) for _, group := range data.Group.Elements() { if groupElement, ok := group.(basetypes.ObjectValue); ok { + groupId := groupElement.Attributes()["group_id"].(basetypes.StringValue).ValueString() svc := r.GetClient().NewTeamGroupMembershipCreate() svc.TeamId(data.TeamId.ValueString()) - svc.GroupId(groupElement.Attributes()["group_id"].(basetypes.StringValue).ValueString()) + svc.GroupId(groupId) svc.Role(groupElement.Attributes()["role"].(basetypes.StringValue).ValueString()) if teamGroupResponse, err := svc.Do(ctx); err != nil { resp.Diagnostics.AddError( "Unable to Create Team Group Memberships Resource.", fmt.Sprintf("%v; code: %v; message: %v", err, teamGroupResponse.Code, teamGroupResponse.Message), ) + + creRvertMsg, creRevertErr := r.RevertCreated(ctx, savedGroups, data.TeamId.ValueString()) + resp.Diagnostics.AddWarning("Action reverted", creRvertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", creRevertErr) return } + savedGroups = append(savedGroups, groupId) } } @@ -140,6 +147,8 @@ func (r *teamGroupMembership) Update(ctx context.Context, req resource.UpdateReq } /* sync */ + deletedGroups := make([]string, 0) + modifiedGroups := make([]string, 0) for stateKey, stateValue := range stateGroupsMap { role, found := planGroupsMap[stateKey] @@ -149,19 +158,34 @@ func (r *teamGroupMembership) Update(ctx context.Context, req resource.UpdateReq "Unable to Update Team Group Membership Resource.", fmt.Sprintf("%v; code: %v; message: %v", err, updateResponse.Code, updateResponse.Message), ) + + delRevertMsg, delRevertErr := r.RevertDeleted(ctx, deletedGroups, plan.TeamId.ValueString(), stateGroupsMap) + resp.Diagnostics.AddWarning("Action reverted", delRevertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", delRevertErr) return } + deletedGroups = append(deletedGroups, stateKey) } else if role != stateValue { if updateResponse, err := r.GetClient().NewTeamGroupMembershipModify().TeamId(plan.TeamId.ValueString()).GroupId(stateKey).Role(role).Do(ctx); err != nil { resp.Diagnostics.AddError( "Unable to Update Team Group Membership Resource.", fmt.Sprintf("%v; code: %v; message: %v", err, updateResponse.Code, updateResponse.Message), ) + + delRevertMsg, delRevertErr := r.RevertDeleted(ctx, deletedGroups, plan.TeamId.ValueString(), stateGroupsMap) + resp.Diagnostics.AddWarning("Action reverted", delRevertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", delRevertErr) + + modRevertMsg, modRevertErr := r.RevertModified(ctx, modifiedGroups, plan.TeamId.ValueString(), stateGroupsMap) + resp.Diagnostics.AddWarning("Action reverted", modRevertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", modRevertErr) return } + modifiedGroups = append(modifiedGroups, stateKey) } } + createdGroups := make([]string, 0) for planKey, planValue := range planGroupsMap { _, exists := stateGroupsMap[planKey] @@ -171,8 +195,22 @@ func (r *teamGroupMembership) Update(ctx context.Context, req resource.UpdateReq "Unable to Update Team Group Membership Resource.", fmt.Sprintf("%v; code: %v; message: %v", err, updateResponse.Code, updateResponse.Message), ) + + delRevertMsg, delRevertErr := r.RevertDeleted(ctx, deletedGroups, plan.TeamId.ValueString(), stateGroupsMap) + resp.Diagnostics.AddWarning("Action reverted", delRevertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", delRevertErr) + + modRevertMsg, modRevertErr := r.RevertModified(ctx, modifiedGroups, plan.TeamId.ValueString(), stateGroupsMap) + resp.Diagnostics.AddWarning("Action reverted", modRevertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", modRevertErr) + + creRvertMsg, creRevertErr := r.RevertCreated(ctx, createdGroups, plan.TeamId.ValueString()) + resp.Diagnostics.AddWarning("Action reverted", creRvertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", creRevertErr) + return } + createdGroups = append(createdGroups, planKey) } } @@ -182,7 +220,6 @@ func (r *teamGroupMembership) Update(ctx context.Context, req resource.UpdateReq "Unable to Create Team Group Memberships Resource.", fmt.Sprintf("%v; code: %v", err, teamGroupResponse.Code), ) - return } @@ -201,15 +238,25 @@ func (r *teamGroupMembership) Delete(ctx context.Context, req resource.DeleteReq return } - var data model.TeamGroupMemberships + var data, state model.TeamGroupMemberships resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + stateGroupsMap := make(map[string]string) + for _, group := range state.Group.Elements() { + if groupElement, ok := group.(basetypes.ObjectValue); ok { + stateGroupsMap[groupElement.Attributes()["group_id"].(basetypes.StringValue).ValueString()] = groupElement.Attributes()["role"].(basetypes.StringValue).ValueString() + } + } + + deletedGroups := make([]string, 0) for _, group := range data.Group.Elements() { if groupElement, ok := group.(basetypes.ObjectValue); ok { + groupId := groupElement.Attributes()["group_id"].(basetypes.StringValue).ValueString() svc := r.GetClient().NewTeamGroupMembershipDelete() svc.TeamId(data.TeamId.ValueString()) - svc.GroupId(groupElement.Attributes()["group_id"].(basetypes.StringValue).ValueString()) + svc.GroupId(groupId) if deleteResponse, err := svc.Do(ctx); err != nil { resp.Diagnostics.AddError( @@ -217,8 +264,65 @@ func (r *teamGroupMembership) Delete(ctx context.Context, req resource.DeleteReq fmt.Sprintf("%v; code: %v; message: %v", err, deleteResponse.Code, deleteResponse.Message), ) + delRevertMsg, delRevertErr := r.RevertDeleted(ctx, deletedGroups, data.TeamId.ValueString(), stateGroupsMap); + resp.Diagnostics.AddWarning("Action reverted", delRevertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", delRevertErr) return } + deletedGroups = append(deletedGroups, groupId) } } -} \ No newline at end of file +} + +func (r *teamGroupMembership) RevertDeleted(ctx context.Context, toRevert []string, teamId string, stateGroupsMap map[string]string) (string, string) { + reverted := []string{} + failed := []string{} + for _, groupId := range toRevert { + svc := r.GetClient().NewTeamGroupMembershipCreate() + svc.TeamId(teamId) + svc.GroupId(groupId) + svc.Role(stateGroupsMap[groupId]) + if _, err := svc.Do(ctx); err != nil { + failed = append(failed, groupId) + } else { + reverted = append(reverted, groupId) + } + } + return fmt.Sprintf("Delete action reverted for groups: %v", reverted), + fmt.Sprintf("Delete for revert action failed for groups: %v", failed) +} + +func (r *teamGroupMembership) RevertModified(ctx context.Context, toRevert []string, teamId string, stateGroupsMap map[string]string) (string, string) { + reverted := []string{} + failed := []string{} + for _, groupId := range toRevert { + svc := r.GetClient().NewTeamGroupMembershipModify() + svc.TeamId(teamId) + svc.GroupId(groupId) + svc.Role(stateGroupsMap[groupId]) + if _, err := svc.Do(ctx); err != nil { + failed = append(failed, groupId) + } else { + reverted = append(reverted, groupId) + } + } + return fmt.Sprintf("Modify action reverted for groups: %v", reverted), + fmt.Sprintf("Modify for revert action failed for groups: %v", failed) +} + +func (r *teamGroupMembership) RevertCreated(ctx context.Context, toRevert []string, teamId string)(string, string) { + reverted := []string{} + failed := []string{} + for _, groupId := range toRevert { + svc := r.GetClient().NewTeamGroupMembershipDelete() + svc.TeamId(teamId) + svc.GroupId(groupId) + if _, err := svc.Do(ctx); err != nil { + failed = append(failed, groupId) + } else { + reverted = append(reverted, groupId) + } + } + return fmt.Sprintf("Create action reverted for groups: %v", reverted), + fmt.Sprintf("Create for revert action failed for groups: %v", failed) +} diff --git a/fivetran/framework/resources/team_group_membership_test.go b/fivetran/framework/resources/team_group_membership_test.go index ff39408c..d24f8c6e 100644 --- a/fivetran/framework/resources/team_group_membership_test.go +++ b/fivetran/framework/resources/team_group_membership_test.go @@ -1,11 +1,13 @@ package resources_test import ( + "fmt" "net/http" + "regexp" "testing" - - tfmock "github.com/fivetran/terraform-provider-fivetran/fivetran/tests/mock" + "github.com/fivetran/go-fivetran/tests/mock" + tfmock "github.com/fivetran/terraform-provider-fivetran/fivetran/tests/mock" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" ) @@ -17,6 +19,8 @@ var ( teamGroupMembershipData map[string]interface{} teamGroupMembershipListData map[string]interface{} teamGroupMembershipResponse string + teamGroupMembershipResponse2 string + teamGroupmembershipDeleteCount int ) func setupMockClientTeamGroupMembershipResource(t *testing.T) { @@ -121,3 +125,102 @@ func TestGroupMembershipResourceTeamMock(t *testing.T) { }, ) } + + +func setupMockClientTeamGroupMembershipResourceNotFound(t *testing.T) { + tfmock.MockClient().Reset() + teamGroupMembershipResponse = + `{ + "id": "test_group", + "role": "Destination Reviewer", + "created_at": "2020-05-25T15:26:47.306509Z" + }` + + teamGroupMembershipResponse = + `{ + "items": [ + { + "id": "test_group", + "role": "Destination Reviewer", + "created_at": "2020-05-25T15:26:47.306509Z" + } + ], + "next_cursor": null}` + + teamGroupMembershipResponse2 = + `{ + "items": [ + { + "id": "test_group2", + "role": "Destination Reviewer", + "created_at": "2020-05-25T15:26:47.306509Z" + } + ], + "next_cursor": null}` + + callCount := 0 + teamGroupMembershipPostHandler = tfmock.MockClient().When(http.MethodPost, "/v1/teams/test_team/groups").ThenCall( + func(req *http.Request) (*http.Response, error) { + callCount++ + if callCount != 1 { + teamGroupMembershipData = tfmock.CreateMapFromJsonString(t, teamGroupMembershipResponse) + return tfmock.FivetranSuccessResponse(t, req, http.StatusCreated, "Group membership has been created", teamGroupMembershipData), nil + } + teamGroupMembershipData = tfmock.CreateMapFromJsonString(t, teamGroupMembershipResponse) + return tfmock.FivetranSuccessResponse(t, req, http.StatusNotFound, "Group not found", teamGroupMembershipData), nil + }, + ) + + teamGroupmembershipDeleteCount := 0; + teamGroupMembershipDeleteHandler = tfmock.MockClient().When(http.MethodDelete, "/v1/teams/test_team/groups/test_group").ThenCall( + func(req *http.Request) (*http.Response, error) { + teamGroupmembershipDeleteCount++ + return tfmock.FivetranSuccessResponse(t, req, 200, "Group membership has been deleted", nil), nil + }, + ) +} + + +func TestGroupMembershipResourceTeamMockNotFound(t *testing.T) { + step1 := resource.TestStep{ + Config: ` + resource "fivetran_team_group_membership" "test_team_group_membership" { + provider = fivetran-provider + + team_id = "test_team" + + group { + group_id = "test_group" + role = "Destination Reviewer" + } + group { + group_id = "test_group2" + role = "Destination Reviewer" + } + + }`, + ExpectError: regexp.MustCompile(`Error: Unable to Create Team Group Memberships Resource`), + } + + resource.Test( + t, + resource.TestCase{ + PreCheck: func() { + setupMockClientTeamGroupMembershipResourceNotFound(t) + }, + ProtoV6ProviderFactories: tfmock.ProtoV6ProviderFactories, + CheckDestroy: func(s *terraform.State) error { + tfmock.AssertEqual(t, teamGroupMembershipDeleteHandler.Interactions, 1) + if (teamGroupmembershipDeleteCount != 1) { + return fmt.Errorf("Failed acctions are not reverted.") + } + return nil + }, + + Steps: []resource.TestStep{ + step1, + }, + }, + ) +} + diff --git a/fivetran/framework/resources/team_user_membership.go b/fivetran/framework/resources/team_user_membership.go index 9514b071..7bb39813 100644 --- a/fivetran/framework/resources/team_user_membership.go +++ b/fivetran/framework/resources/team_user_membership.go @@ -50,11 +50,14 @@ func (r *teamUserMembership) Create(ctx context.Context, req resource.CreateRequ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + savedUsers := make([]string, 0, len(data.User.Elements())) for _, user := range data.User.Elements() { if userElement, ok := user.(basetypes.ObjectValue); ok { + userId := userElement.Attributes()["user_id"].(basetypes.StringValue).ValueString() svc := r.GetClient().NewTeamUserMembershipCreate() svc.TeamId(data.TeamId.ValueString()) - svc.UserId(userElement.Attributes()["user_id"].(basetypes.StringValue).ValueString()) + svc.UserId(userId) svc.Role(userElement.Attributes()["role"].(basetypes.StringValue).ValueString()) if teamUserResponse, err := svc.Do(ctx); err != nil { resp.Diagnostics.AddError( @@ -62,8 +65,13 @@ func (r *teamUserMembership) Create(ctx context.Context, req resource.CreateRequ fmt.Sprintf("%v; code: %v; message: %v", err, teamUserResponse.Code, teamUserResponse.Message), ) + creRvertMsg, creRevertErr := r.RevertCreated(ctx, savedUsers, data.TeamId.ValueString()) + resp.Diagnostics.AddWarning("Action reverted", creRvertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", creRevertErr) return } + + savedUsers = append(savedUsers, userId) } } @@ -140,6 +148,8 @@ func (r *teamUserMembership) Update(ctx context.Context, req resource.UpdateRequ } /* sync */ + deletedUsers := make([]string, 0) + modifiedUsers := make([]string, 0) for stateKey, stateValue := range stateUsersMap { role, found := planUsersMap[stateKey] @@ -149,19 +159,36 @@ func (r *teamUserMembership) Update(ctx context.Context, req resource.UpdateRequ "Unable to Update Team User Membership Resource.", fmt.Sprintf("%v; code: %v; message: %v", err, updateResponse.Code, updateResponse.Message), ) + + delRevertMsg, delRevertErr := r.RevertDeleted(ctx, deletedUsers, plan.TeamId.ValueString(), stateUsersMap) + resp.Diagnostics.AddWarning("Action reverted", delRevertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", delRevertErr) return } + + deletedUsers = append(deletedUsers, stateKey) } else if role != stateValue { if updateResponse, err := r.GetClient().NewTeamUserMembershipModify().TeamId(plan.TeamId.ValueString()).UserId(stateKey).Role(role).Do(ctx); err != nil { resp.Diagnostics.AddError( "Unable to Update Team User Membership Resource.", fmt.Sprintf("%v; code: %v; message: %v", err, updateResponse.Code, updateResponse.Message), ) + + delRevertMsg, delRevertErr := r.RevertDeleted(ctx, deletedUsers, plan.TeamId.ValueString(), stateUsersMap) + resp.Diagnostics.AddWarning("Action reverted", delRevertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", delRevertErr) + + modRevertMsg, modRevertErr := r.RevertModified(ctx, modifiedUsers, plan.TeamId.ValueString(), stateUsersMap) + resp.Diagnostics.AddWarning("Action reverted", modRevertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", modRevertErr) return } + modifiedUsers = append(modifiedUsers, stateKey) } } + + createdUsers := make([]string, 0) for planKey, planValue := range planUsersMap { _, exists := stateUsersMap[planKey] @@ -171,6 +198,18 @@ func (r *teamUserMembership) Update(ctx context.Context, req resource.UpdateRequ "Unable to Update Team User Membership Resource.", fmt.Sprintf("%v; code: %v; message: %v", err, updateResponse.Code, updateResponse.Message), ) + + delRevertMsg, delRevertErr := r.RevertDeleted(ctx, deletedUsers, plan.TeamId.ValueString(), stateUsersMap) + resp.Diagnostics.AddWarning("Action reverted", delRevertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", delRevertErr) + + modRevertMsg, modRevertErr := r.RevertModified(ctx, modifiedUsers, plan.TeamId.ValueString(), stateUsersMap) + resp.Diagnostics.AddWarning("Action reverted", modRevertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", modRevertErr) + + creRvertMsg, creRevertErr := r.RevertCreated(ctx, createdUsers, plan.TeamId.ValueString()) + resp.Diagnostics.AddWarning("Action reverted", creRvertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", creRevertErr) return } } @@ -201,24 +240,94 @@ func (r *teamUserMembership) Delete(ctx context.Context, req resource.DeleteRequ return } - var data model.TeamUserMemberships + var data, state model.TeamUserMemberships resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + stateUsersMap := make(map[string]string) + for _, user := range state.User.Elements() { + if userElement, ok := user.(basetypes.ObjectValue); ok { + stateUsersMap[userElement.Attributes()["user_id"].(basetypes.StringValue).ValueString()] = userElement.Attributes()["role"].(basetypes.StringValue).ValueString() + } + } + deletedUsers := make([]string, 0) for _, user := range data.User.Elements() { if userElement, ok := user.(basetypes.ObjectValue); ok { + userId := userElement.Attributes()["user_id"].(basetypes.StringValue).ValueString() svc := r.GetClient().NewTeamUserMembershipDelete() svc.TeamId(data.TeamId.ValueString()) - svc.UserId(userElement.Attributes()["user_id"].(basetypes.StringValue).ValueString()) + svc.UserId(userId) if deleteResponse, err := svc.Do(ctx); err != nil { resp.Diagnostics.AddError( "Unable to Delete Team User Memberships Resource.", fmt.Sprintf("%v; code: %v; message: %v", err, deleteResponse.Code, deleteResponse.Message), ) + delRevertMsg, delRevertErr := r.RevertDeleted(ctx, deletedUsers, data.TeamId.ValueString(), stateUsersMap); + resp.Diagnostics.AddWarning("Action reverted", delRevertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", delRevertErr) return } + deletedUsers = append(deletedUsers, userId) } } -} \ No newline at end of file +} + +func (r *teamUserMembership) RevertDeleted(ctx context.Context, toRevert []string, teamId string, stateUserMap map[string]string) (string, string){ + reverted := []string{} + failed := []string{} + for _, userId := range toRevert { + svc := r.GetClient().NewTeamUserMembershipCreate() + svc.TeamId(teamId) + svc.UserId(userId) + svc.Role(stateUserMap[userId]) + + if _, err := svc.Do(ctx); err != nil { + failed = append(failed, userId) + } else { + reverted = append(reverted, userId) + } + } + return fmt.Sprintf("Delete action reverted for users: %v", reverted), + fmt.Sprintf("Delete for revert action failed for users: %v", failed) +} + +func (r *teamUserMembership) RevertModified(ctx context.Context, toRevert []string, teamId string, stateUserMap map[string]string) (string, string) { + reverted := []string{} + failed := []string{} + for _, userId := range toRevert { + svc := r.GetClient().NewTeamUserMembershipModify() + svc.TeamId(teamId) + svc.UserId(userId) + svc.Role(stateUserMap[userId]) + + if _, err := svc.Do(ctx); err != nil { + failed = append(failed, userId) + } else { + reverted = append(reverted, userId) + } + } + return fmt.Sprintf("Modify action reverted for users: %v", reverted), + fmt.Sprintf("Modify for revert action failed for users: %v", failed) +} + +func (r *teamUserMembership) RevertCreated(ctx context.Context, toRevert []string, teamId string) (string, string) { + reverted := []string{} + failed := []string{} + for _, userId := range toRevert { + svc := r.GetClient().NewTeamUserMembershipDelete() + svc.TeamId(teamId) + svc.UserId(userId) + + if _, err := svc.Do(ctx); err != nil { + failed = append(failed, userId) + } else { + reverted = append(reverted, userId) + } + } + return fmt.Sprintf("Create action reverted for users: %v", reverted), + fmt.Sprintf("Create for revert action failed for users: %v", failed) +} diff --git a/fivetran/framework/resources/team_user_membership_test.go b/fivetran/framework/resources/team_user_membership_test.go index 0ea99885..c43d0e44 100644 --- a/fivetran/framework/resources/team_user_membership_test.go +++ b/fivetran/framework/resources/team_user_membership_test.go @@ -1,11 +1,13 @@ package resources_test import ( + "fmt" "net/http" + "regexp" "testing" - - tfmock "github.com/fivetran/terraform-provider-fivetran/fivetran/tests/mock" + "github.com/fivetran/go-fivetran/tests/mock" + tfmock "github.com/fivetran/terraform-provider-fivetran/fivetran/tests/mock" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" ) @@ -17,6 +19,8 @@ var ( teamUserMembershipData map[string]interface{} teamUserMembershipListData map[string]interface{} teamUserMembershipResponse string + teamUserMembershipResponse2 string + teamUsermembershipDeleteCount int ) func setupMockClientTeamUserMembershipResource(t *testing.T) { @@ -118,3 +122,95 @@ func TestUserMembershipResourceTeamMock(t *testing.T) { }, ) } + + +func setupMockClientTeamUserMembershipResourceNotFound(t *testing.T) { + tfmock.MockClient().Reset() + teamUserMembershipResponse = + `{ + "user_id": "test_user", + "role": "Team Member" + }` + + teamUserMembershipResponse2 = + `{ + "user_id": "test_user2", + "role": "Team Member" + }` + + teamUserMembershipResponse = + `{ + "items": [ + { + "user_id": "test_user", + "role": "Team Manager" + } + ], + "next_cursor": null}` + + + callCount := 0 + teamUserMembershipPostHandler = tfmock.MockClient().When(http.MethodPost, "/v1/teams/test_team/users").ThenCall( + func(req *http.Request) (*http.Response, error) { + callCount++ + if callCount != 0 { + teamUserMembershipData = tfmock.CreateMapFromJsonString(t, teamUserMembershipResponse) + return tfmock.FivetranSuccessResponse(t, req, http.StatusCreated, "User membership has been created", teamUserMembershipData), nil + } + teamUserMembershipData = tfmock.CreateMapFromJsonString(t, teamUserMembershipResponse2) + return tfmock.FivetranSuccessResponse(t, req, http.StatusNotFound, "User not found", teamUserMembershipData), nil + }, + ) + + teamUsermembershipDeleteCount := 0; + teamUserMembershipDeleteHandler = tfmock.MockClient().When(http.MethodDelete, "/v1/teams/test_team/users/test_user").ThenCall( + func(req *http.Request) (*http.Response, error) { + teamUsermembershipDeleteCount++ + return tfmock.FivetranSuccessResponse(t, req, 200, "User membership has been deleted", nil), nil + }, + ) +} + +func TestUserMembershipResourceTeamMockNotFound(t *testing.T) { + step1 := resource.TestStep{ + Config: ` + resource "fivetran_team_user_membership" "test_team_user_membership" { + provider = fivetran-provider + + team_id = "test_team" + + user { + user_id = "test_user" + role = "Team Manager" + } + user { + user_id = "test_user2" + role = "Team Manager" + } + }`, + + ExpectError: regexp.MustCompile(`Error: Unable to Create Team User Memberships Resource`), + + } + + resource.Test( + t, + resource.TestCase{ + PreCheck: func() { + setupMockClientTeamUserMembershipResourceNotFound(t) + }, + ProtoV6ProviderFactories: tfmock.ProtoV6ProviderFactories, + CheckDestroy: func(s *terraform.State) error { + tfmock.AssertEqual(t, teamUserMembershipDeleteHandler.Interactions, 1) + if (teamUsermembershipDeleteCount != 1) { + return fmt.Errorf("Failed acctions are not reverted.") + } + return nil + }, + + Steps: []resource.TestStep{ + step1, + }, + }, + ) +} diff --git a/fivetran/framework/resources/user_connector_membership.go b/fivetran/framework/resources/user_connector_membership.go index afe7a63f..4bf73372 100644 --- a/fivetran/framework/resources/user_connector_membership.go +++ b/fivetran/framework/resources/user_connector_membership.go @@ -49,12 +49,14 @@ func (r *userConnectorMembership) Create(ctx context.Context, req resource.Creat var data model.UserConnectorMemberships resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) - - for _, connector := range data.Connector.Elements() { + + savedConnectors := make([]string, 0, len(data.Connector.Elements())) + for _, connector := range data.Connector.Elements() { if connectorElement, ok := connector.(basetypes.ObjectValue); ok { + connectorId := connectorElement.Attributes()["connector_id"].(basetypes.StringValue).ValueString() svc := r.GetClient().NewUserConnectorMembershipCreate() svc.UserId(data.UserId.ValueString()) - svc.ConnectorId(connectorElement.Attributes()["connector_id"].(basetypes.StringValue).ValueString()) + svc.ConnectorId(connectorId) svc.Role(connectorElement.Attributes()["role"].(basetypes.StringValue).ValueString()) if userConnectorResponse, err := svc.Do(ctx); err != nil { resp.Diagnostics.AddError( @@ -62,8 +64,13 @@ func (r *userConnectorMembership) Create(ctx context.Context, req resource.Creat fmt.Sprintf("%v; code: %v; message: %v", err, userConnectorResponse.Code, userConnectorResponse.Message), ) + creRvertMsg, creRevertErr := r.RevertCreated(ctx, savedConnectors, data.UserId.ValueString()) + resp.Diagnostics.AddWarning("Action reverted", creRvertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", creRevertErr) return } + + savedConnectors = append(savedConnectors, connectorId) } } @@ -140,6 +147,8 @@ func (r *userConnectorMembership) Update(ctx context.Context, req resource.Updat } /* sync */ + deletedConnectors := make([]string, 0) + modifiedConnectors := make([]string, 0) for stateKey, stateValue := range stateConnectorsMap { role, found := planConnectorsMap[stateKey] @@ -149,19 +158,34 @@ func (r *userConnectorMembership) Update(ctx context.Context, req resource.Updat "Unable to Update User Connector Membership Resource.", fmt.Sprintf("%v; code: %v; message: %v", err, updateResponse.Code, updateResponse.Message), ) + + delRevertMsg, delRevertErr := r.RevertDeleted(ctx, deletedConnectors, plan.UserId.ValueString(), stateConnectorsMap) + resp.Diagnostics.AddWarning("Action reverted", delRevertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", delRevertErr) return } + deletedConnectors = append(deletedConnectors, stateKey) } else if role != stateValue { if updateResponse, err := r.GetClient().NewUserConnectorMembershipModify().UserId(plan.UserId.ValueString()).ConnectorId(stateKey).Role(role).Do(ctx); err != nil { resp.Diagnostics.AddError( "Unable to Update User Connector Membership Resource.", fmt.Sprintf("%v; code: %v; message: %v", err, updateResponse.Code, updateResponse.Message), ) + + delRevertMsg, delRevertErr := r.RevertDeleted(ctx, deletedConnectors, plan.UserId.ValueString(), stateConnectorsMap) + resp.Diagnostics.AddWarning("Action reverted", delRevertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", delRevertErr) + + modRevertMsg, modRevertErr := r.RevertModified(ctx, modifiedConnectors, plan.UserId.ValueString(), stateConnectorsMap) + resp.Diagnostics.AddWarning("Action reverted", modRevertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", modRevertErr) return } + modifiedConnectors = append(modifiedConnectors, stateKey) } } + createdConnectors := make([]string, 0) for planKey, planValue := range planConnectorsMap { _, exists := stateConnectorsMap[planKey] @@ -171,8 +195,21 @@ func (r *userConnectorMembership) Update(ctx context.Context, req resource.Updat "Unable to Update User Connector Membership Resource.", fmt.Sprintf("%v; code: %v; message: %v", err, updateResponse.Code, updateResponse.Message), ) + + delRevertMsg, delRevertErr := r.RevertDeleted(ctx, deletedConnectors, plan.UserId.ValueString(), stateConnectorsMap) + resp.Diagnostics.AddWarning("Action reverted", delRevertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", delRevertErr) + + modRevertMsg, modRevertErr := r.RevertModified(ctx, modifiedConnectors, plan.UserId.ValueString(), stateConnectorsMap) + resp.Diagnostics.AddWarning("Action reverted", modRevertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", modRevertErr) + + creRvertMsg, creRevertErr := r.RevertCreated(ctx, createdConnectors, plan.UserId.ValueString()) + resp.Diagnostics.AddWarning("Action reverted", creRvertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", creRevertErr) return } + createdConnectors = append(createdConnectors, planKey) } } @@ -201,15 +238,25 @@ func (r *userConnectorMembership) Delete(ctx context.Context, req resource.Delet return } - var data model.UserConnectorMemberships + var data, state model.UserConnectorMemberships resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + stateConnectorsMap := make(map[string]string) + for _, connector := range state.Connector.Elements() { + if connectorElement, ok := connector.(basetypes.ObjectValue); ok { + stateConnectorsMap[connectorElement.Attributes()["connector_id"].(basetypes.StringValue).ValueString()] = connectorElement.Attributes()["role"].(basetypes.StringValue).ValueString() + } + } + + deletedConnectors := make([]string, 0) for _, connector := range data.Connector.Elements() { if connectorElement, ok := connector.(basetypes.ObjectValue); ok { + connectorId := connectorElement.Attributes()["connector_id"].(basetypes.StringValue).ValueString() svc := r.GetClient().NewUserConnectorMembershipDelete() svc.UserId(data.UserId.ValueString()) - svc.ConnectorId(connectorElement.Attributes()["connector_id"].(basetypes.StringValue).ValueString()) + svc.ConnectorId(connectorId) if deleteResponse, err := svc.Do(ctx); err != nil { resp.Diagnostics.AddError( @@ -217,8 +264,66 @@ func (r *userConnectorMembership) Delete(ctx context.Context, req resource.Delet fmt.Sprintf("%v; code: %v; message: %v", err, deleteResponse.Code, deleteResponse.Message), ) + delRevertMsg, delRevertErr := r.RevertDeleted(ctx, deletedConnectors, data.UserId.ValueString(), stateConnectorsMap); + resp.Diagnostics.AddWarning("Action reverted", delRevertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", delRevertErr) return } + deletedConnectors = append(deletedConnectors, connectorId) } } -} \ No newline at end of file +} + + +func (r *userConnectorMembership) RevertDeleted(ctx context.Context, toRevert []string, userId string, stateConnectorsMap map[string]string) (string, string) { + reverted := []string{} + failed := []string{} + for _, connectorId := range toRevert { + svc := r.GetClient().NewUserConnectorMembershipCreate() + svc.UserId(userId) + svc.ConnectorId(connectorId) + svc.Role(stateConnectorsMap[connectorId]) + if _, err := svc.Do(ctx); err != nil { + failed = append(failed, userId) + } else { + reverted = append(reverted, userId) + } + } + return fmt.Sprintf("Delete action reverted for connectors: %v", reverted), + fmt.Sprintf("Delete for revert action failed for connectors: %v", failed) +} + +func (r *userConnectorMembership) RevertModified(ctx context.Context, toRevert []string, userId string, stateConnectorsMap map[string]string) (string, string) { + reverted := []string{} + failed := []string{} + for _, connectorId := range toRevert { + svc := r.GetClient().NewUserConnectorMembershipModify() + svc.UserId(userId) + svc.ConnectorId(connectorId) + svc.Role(stateConnectorsMap[connectorId]) + if _, err := svc.Do(ctx); err != nil { + failed = append(failed, userId) + } else { + reverted = append(reverted, userId) + } + } + return fmt.Sprintf("Modify action reverted for connectors: %v", reverted), + fmt.Sprintf("Modify for revert action failed for connectors: %v", failed) +} + +func (r *userConnectorMembership) RevertCreated(ctx context.Context, toRevert []string, userId string) (string, string) { + reverted := []string{} + failed := []string{} + for _, connectorId := range toRevert { + svc := r.GetClient().NewUserConnectorMembershipDelete() + svc.UserId(userId) + svc.ConnectorId(connectorId) + if _, err := svc.Do(ctx); err != nil { + failed = append(failed, userId) + } else { + reverted = append(reverted, userId) + } + } + return fmt.Sprintf("Create action reverted for connectors: %v", reverted), + fmt.Sprintf("Create for revert action failed for connectors: %v", failed) +} diff --git a/fivetran/framework/resources/user_connector_membership_test.go b/fivetran/framework/resources/user_connector_membership_test.go index a9bd2f5c..ffdf74fe 100644 --- a/fivetran/framework/resources/user_connector_membership_test.go +++ b/fivetran/framework/resources/user_connector_membership_test.go @@ -1,11 +1,13 @@ package resources_test import ( + "fmt" "net/http" + "regexp" "testing" - - tfmock "github.com/fivetran/terraform-provider-fivetran/fivetran/tests/mock" + "github.com/fivetran/go-fivetran/tests/mock" + tfmock "github.com/fivetran/terraform-provider-fivetran/fivetran/tests/mock" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" ) @@ -17,6 +19,8 @@ var ( userConnectorMembershipData map[string]interface{} userConnectorMembershipListData map[string]interface{} userConnectorMembershipResponse string + userConnectorMembershipResponse2 string + userConnectorMembershipdeleteCount int ) func setupMockClientUserConnectorMembershipResource(t *testing.T) { @@ -120,3 +124,94 @@ func TestConnectorMembershipResourceUserMock(t *testing.T) { }, ) } + +func setupMockClientUserConnectorMembershipResourceNotFound(t *testing.T) { + tfmock.MockClient().Reset() + userConnectorMembershipResponse = + `{ + "id": "test_connector", + "role": "Connector Reviewer", + "created_at": "2020-05-25T15:26:47.306509Z" + }` + + userConnectorMembershipResponse = + `{ + "id": "test_connector2", + "role": "Connector Reviewer", + "created_at": "2020-05-25T15:26:47.306509Z" + }` + + userConnectorMembershipResponse = `{ + "items": [ + { + "id": "test_connector", + "role": "Connector Reviewer", + "created_at": "2020-05-25T15:26:47.306509Z" + } + ], + "next_cursor": null}` + + + callCount := 0 + userConnectorMembershipPostHandler = tfmock.MockClient().When(http.MethodPost, "/v1/users/test_user/connectors").ThenCall( + func(req *http.Request) (*http.Response, error) { + callCount++; + if callCount != 0 { + userConnectorMembershipData = tfmock.CreateMapFromJsonString(t, userConnectorMembershipResponse) + return tfmock.FivetranSuccessResponse(t, req, http.StatusCreated, "Connector membership has been created", userConnectorMembershipData), nil + } + userConnectorMembershipData = tfmock.CreateMapFromJsonString(t, userConnectorMembershipResponse2) + return tfmock.FivetranSuccessResponse(t, req, http.StatusCreated, "Connector not found", userConnectorMembershipData), nil + }, + ) + + userConnectorMembershipdeleteCount := 0 + userConnectorMembershipDeleteHandler = tfmock.MockClient().When(http.MethodDelete, "/v1/users/test_user/connectors/test_connector").ThenCall( + func(req *http.Request) (*http.Response, error) { + userConnectorMembershipdeleteCount++ + return tfmock.FivetranSuccessResponse(t, req, 200, "Connector membership has been deleted", nil), nil + }, + ) +} + +func TestConnectorMembershipResourceUserMockNotfound(t *testing.T) { + step1 := resource.TestStep{ + Config: ` + resource "fivetran_user_connector_membership" "test_user_connector_membership" { + provider = fivetran-provider + + user_id = "test_user" + + connector { + connector_id = "test_connector" + role = "Connector Reviewer" + } + connector { + connector_id = "test_connector2" + role = "Connector Reviewer" + } + }`, + ExpectError: regexp.MustCompile(`Error: Unable to Create User Connector Memberships Resource`), + } + + resource.Test( + t, + resource.TestCase{ + PreCheck: func() { + setupMockClientUserConnectorMembershipResourceNotFound(t) + }, + ProtoV6ProviderFactories: tfmock.ProtoV6ProviderFactories, + CheckDestroy: func(s *terraform.State) error { + tfmock.AssertEqual(t, userConnectorMembershipDeleteHandler.Interactions, 1) + if (userConnectorMembershipdeleteCount != 1) { + return fmt.Errorf("Failed acctions are not reverted.") + } + return nil + }, + + Steps: []resource.TestStep{ + step1, + }, + }, + ) +} diff --git a/fivetran/framework/resources/user_group_membership.go b/fivetran/framework/resources/user_group_membership.go index cc92b01a..08f73318 100644 --- a/fivetran/framework/resources/user_group_membership.go +++ b/fivetran/framework/resources/user_group_membership.go @@ -50,11 +50,13 @@ func (r *userGroupMembership) Create(ctx context.Context, req resource.CreateReq resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + savedGroups := make([]string, 0) for _, group := range data.Group.Elements() { if groupElement, ok := group.(basetypes.ObjectValue); ok { + groupId := groupElement.Attributes()["group_id"].(basetypes.StringValue).ValueString() svc := r.GetClient().NewUserGroupMembershipCreate() svc.UserId(data.UserId.ValueString()) - svc.GroupId(groupElement.Attributes()["group_id"].(basetypes.StringValue).ValueString()) + svc.GroupId(groupId) svc.Role(groupElement.Attributes()["role"].(basetypes.StringValue).ValueString()) if userGroupResponse, err := svc.Do(ctx); err != nil { resp.Diagnostics.AddError( @@ -62,8 +64,13 @@ func (r *userGroupMembership) Create(ctx context.Context, req resource.CreateReq fmt.Sprintf("%v; code: %v; message: %v", err, userGroupResponse.Code, userGroupResponse.Message), ) + creRvertMsg, creRevertErr := r.RevertCreated(ctx, savedGroups, data.UserId.ValueString()) + resp.Diagnostics.AddWarning("Action reverted", creRvertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", creRevertErr) return } + + savedGroups = append(savedGroups, groupId) } } @@ -139,7 +146,9 @@ func (r *userGroupMembership) Update(ctx context.Context, req resource.UpdateReq } } - /* sync */ + /* sync */ + deletedGroups := make([]string, 0) + modifiedGroups := make([]string, 0) for stateKey, stateValue := range stateGroupsMap { role, found := planGroupsMap[stateKey] @@ -149,19 +158,35 @@ func (r *userGroupMembership) Update(ctx context.Context, req resource.UpdateReq "Unable to Update User Group Membership Resource.", fmt.Sprintf("%v; code: %v; message: %v", err, updateResponse.Code, updateResponse.Message), ) + + delRevertMsg, delRevertErr := r.RevertDeleted(ctx, deletedGroups, plan.UserId.ValueString(), stateGroupsMap) + resp.Diagnostics.AddWarning("Action reverted", delRevertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", delRevertErr) return - } + } + deletedGroups = append(deletedGroups, stateKey) } else if role != stateValue { if updateResponse, err := r.GetClient().NewUserGroupMembershipModify().UserId(plan.UserId.ValueString()).GroupId(stateKey).Role(role).Do(ctx); err != nil { resp.Diagnostics.AddError( "Unable to Update User Group Membership Resource.", fmt.Sprintf("%v; code: %v; message: %v", err, updateResponse.Code, updateResponse.Message), - ) + ) + + delRevertMsg, delRevertErr := r.RevertDeleted(ctx, deletedGroups, plan.UserId.ValueString(), stateGroupsMap) + resp.Diagnostics.AddWarning("Action reverted", delRevertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", delRevertErr) + + modRevertMsg, modRevertErr := r.RevertModified(ctx, modifiedGroups, plan.UserId.ValueString(), stateGroupsMap) + resp.Diagnostics.AddWarning("Action reverted", modRevertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", modRevertErr) + return } + modifiedGroups = append(modifiedGroups, stateKey) } } + createdGroups := make([]string, 0) for planKey, planValue := range planGroupsMap { _, exists := stateGroupsMap[planKey] @@ -171,8 +196,21 @@ func (r *userGroupMembership) Update(ctx context.Context, req resource.UpdateReq "Unable to Update User Group Membership Resource.", fmt.Sprintf("%v; code: %v; message: %v", err, updateResponse.Code, updateResponse.Message), ) + + delRevertMsg, delRevertErr := r.RevertDeleted(ctx, deletedGroups, plan.UserId.ValueString(), stateGroupsMap) + resp.Diagnostics.AddWarning("Action reverted", delRevertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", delRevertErr) + + modRevertMsg, modRevertErr := r.RevertModified(ctx, modifiedGroups, plan.UserId.ValueString(), stateGroupsMap) + resp.Diagnostics.AddWarning("Action reverted", modRevertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", modRevertErr) + + creRvertMsg, creRevertErr := r.RevertCreated(ctx, createdGroups, plan.UserId.ValueString()) + resp.Diagnostics.AddWarning("Action reverted", creRvertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", creRevertErr) return } + createdGroups = append(createdGroups, planKey) } } @@ -201,15 +239,25 @@ func (r *userGroupMembership) Delete(ctx context.Context, req resource.DeleteReq return } - var data model.UserGroupMemberships + var data, state model.UserGroupMemberships resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + stateGroupsMap := make(map[string]string) + for _, group := range state.Group.Elements() { + if groupElement, ok := group.(basetypes.ObjectValue); ok { + stateGroupsMap[groupElement.Attributes()["group_id"].(basetypes.StringValue).ValueString()] = groupElement.Attributes()["role"].(basetypes.StringValue).ValueString() + } + } + deletedGroups := make([]string, 0) for _, group := range data.Group.Elements() { if groupElement, ok := group.(basetypes.ObjectValue); ok { + groupId := groupElement.Attributes()["group_id"].(basetypes.StringValue).ValueString() svc := r.GetClient().NewUserGroupMembershipDelete() svc.UserId(data.UserId.ValueString()) - svc.GroupId(groupElement.Attributes()["group_id"].(basetypes.StringValue).ValueString()) + svc.GroupId(groupId) if deleteResponse, err := svc.Do(ctx); err != nil { resp.Diagnostics.AddError( @@ -217,8 +265,66 @@ func (r *userGroupMembership) Delete(ctx context.Context, req resource.DeleteReq fmt.Sprintf("%v; code: %v; message: %v", err, deleteResponse.Code, deleteResponse.Message), ) + delRevertMsg, delRevertErr := r.RevertDeleted(ctx, deletedGroups, data.UserId.ValueString(), stateGroupsMap); + resp.Diagnostics.AddWarning("Action reverted", delRevertMsg) + resp.Diagnostics.AddWarning("Action reverted failed", delRevertErr) return } + deletedGroups = append(deletedGroups, groupId) } } -} \ No newline at end of file +} + +func (r *userGroupMembership) RevertDeleted(ctx context.Context, toRevert []string, userId string, stateGroupsMap map[string]string) (string, string) { + reverted := []string{} + failed := []string{} + for _, groupId := range toRevert { + svc := r.GetClient().NewUserGroupMembershipCreate() + svc.UserId(userId) + svc.GroupId(groupId) + svc.Role(stateGroupsMap[groupId]) + if _, err := svc.Do(ctx); err != nil { + failed = append(failed, groupId) + } else { + reverted = append(reverted, groupId) + } + } + return fmt.Sprintf("Delete action reverted for groups: %v", reverted), + fmt.Sprintf("Delete for revert action failed for groups: %v", failed) +} + +func (r *userGroupMembership) RevertModified(ctx context.Context, toRevert []string, userId string, stateGroupsMap map[string]string) (string, string) { + reverted := []string{} + failed := []string{} + for _, groupId := range toRevert { + svc := r.GetClient().NewUserGroupMembershipModify() + svc.UserId(userId) + svc.GroupId(groupId) + svc.Role(stateGroupsMap[groupId]) + if _, err := svc.Do(ctx); err != nil { + failed = append(failed, groupId) + } else { + reverted = append(reverted, groupId) + } + } + return fmt.Sprintf("Modify action reverted for groups: %v", reverted), + fmt.Sprintf("Modify for revert action failed for groups: %v", failed) +} + +func (r *userGroupMembership) RevertCreated(ctx context.Context, toRevert []string, userId string) (string, string) { + reverted := []string{} + failed := []string{} + for _, groupId := range toRevert { + svc := r.GetClient().NewUserGroupMembershipDelete() + svc.UserId(userId) + svc.GroupId(groupId) + if _, err := svc.Do(ctx); err != nil { + failed = append(failed, groupId) + } else { + reverted = append(reverted, groupId) + } + } + return fmt.Sprintf("Create action reverted for groups: %v", reverted), + fmt.Sprintf("Create for revert action failed for groups: %v", failed) +} + diff --git a/fivetran/framework/resources/user_group_membership_test.go b/fivetran/framework/resources/user_group_membership_test.go index 94a8c47e..415d6dd5 100644 --- a/fivetran/framework/resources/user_group_membership_test.go +++ b/fivetran/framework/resources/user_group_membership_test.go @@ -1,11 +1,13 @@ package resources_test import ( + "fmt" "net/http" + "regexp" "testing" - - tfmock "github.com/fivetran/terraform-provider-fivetran/fivetran/tests/mock" + "github.com/fivetran/go-fivetran/tests/mock" + tfmock "github.com/fivetran/terraform-provider-fivetran/fivetran/tests/mock" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" ) @@ -17,6 +19,8 @@ var ( userGroupMembershipData map[string]interface{} userGroupMembershipListData map[string]interface{} userGroupMembershipResponse string + userGroupMembershipResponse2 string + userGroupmembershipdeleteCount int ) func setupMockClientUserGroupMembershipResource(t *testing.T) { @@ -121,3 +125,97 @@ func TestGroupMembershipResourceUserMock(t *testing.T) { }, ) } + +func setupMockClientUserGroupMembershipResourceNotFoud(t *testing.T) { + tfmock.MockClient().Reset() + userGroupMembershipResponse = + `{ + "id": "test_group", + "role": "Destination Reviewer", + "created_at": "2020-05-25T15:26:47.306509Z" + }` + + userGroupMembershipResponse2 = + `{ + "id": "test_group2", + "role": "Destination Reviewer", + "created_at": "2020-05-25T15:26:47.306509Z" + }` + + + userGroupMembershipResponse = + `{ + "items": [ + { + "id": "test_group", + "role": "Destination Reviewer", + "created_at": "2020-05-25T15:26:47.306509Z" + } + ], + "next_cursor": null}` + + callCount := 0 + userGroupMembershipPostHandler = tfmock.MockClient().When(http.MethodPost, "/v1/users/test_user/groups").ThenCall( + func(req *http.Request) (*http.Response, error) { + callCount++ + if callCount != 1 { + userGroupMembershipData = tfmock.CreateMapFromJsonString(t, userGroupMembershipResponse) + return tfmock.FivetranSuccessResponse(t, req, http.StatusCreated, "Group membership has been created", userGroupMembershipData), nil + } + + userGroupMembershipData = tfmock.CreateMapFromJsonString(t, userGroupMembershipResponse2) + return tfmock.FivetranSuccessResponse(t, req, http.StatusNotFound, "Group not found", userGroupMembershipData), nil + }, + ) + + userGroupmembershipdeleteCount := 0 + userGroupMembershipDeleteHandler = tfmock.MockClient().When(http.MethodDelete, "/v1/users/test_user/groups/test_group").ThenCall( + func(req *http.Request) (*http.Response, error) { + userGroupmembershipdeleteCount++ + return tfmock.FivetranSuccessResponse(t, req, 200, "Group membership has been deleted", nil), nil + }, + ) +} + +func TestGroupMembershipResourceUserMockNotFound(t *testing.T) { + step1 := resource.TestStep{ + Config: ` + resource "fivetran_user_group_membership" "test_user_group_membership" { + provider = fivetran-provider + + user_id = "test_user" + + group { + group_id = "test_group" + role = "Destination Reviewer" + } + + group { + group_id = "test_group2" + role = "Destination Reviewer" + } + }`, + ExpectError: regexp.MustCompile(`Error: Unable to Create User Group Memberships Resource`), + } + + resource.Test( + t, + resource.TestCase{ + PreCheck: func() { + setupMockClientUserGroupMembershipResourceNotFoud(t) + }, + ProtoV6ProviderFactories: tfmock.ProtoV6ProviderFactories, + CheckDestroy: func(s *terraform.State) error { + tfmock.AssertEqual(t, userGroupMembershipDeleteHandler.Interactions, 1) + if (userGroupmembershipdeleteCount != 1) { + return fmt.Errorf("Failed acctions are not reverted.") + } + return nil + }, + + Steps: []resource.TestStep{ + step1, + }, + }, + ) +}