From a4f07096968a0cf33a00eae8fdf4fdf6521ef11e Mon Sep 17 00:00:00 2001 From: tkuzynow Date: Thu, 29 Feb 2024 12:57:13 +0100 Subject: [PATCH 1/2] feat: saga pattern for consultant creation --- .../consultant/ConsultantAdminService.java | 40 +--- .../service/consultant/TransactionalStep.java | 4 +- ...Service.java => ConsultantCreateSaga.java} | 178 ++++++++++++++++-- .../api/facade/rollback/RollbackFacade.java | 8 +- .../api/service/ConsultantImportService.java | 6 +- .../consultant/ConsultantAdminServiceIT.java | 10 +- .../ConsultantAdminServiceTest.java | 4 +- ...iceIT.java => ConsultantCreateSagaIT.java} | 178 ++++++++++++++++-- ...=> ConsultantCreateSagaTenantAwareIT.java} | 17 +- 9 files changed, 356 insertions(+), 89 deletions(-) rename src/main/java/de/caritas/cob/userservice/api/admin/service/consultant/create/{ConsultantCreatorService.java => ConsultantCreateSaga.java} (56%) rename src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/create/{ConsultantCreatorServiceIT.java => ConsultantCreateSagaIT.java} (51%) rename src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/create/{ConsultantCreatorServiceTenantAwareIT.java => ConsultantCreateSagaTenantAwareIT.java} (91%) diff --git a/src/main/java/de/caritas/cob/userservice/api/admin/service/consultant/ConsultantAdminService.java b/src/main/java/de/caritas/cob/userservice/api/admin/service/consultant/ConsultantAdminService.java index 93876ca3a..cba6d5a3d 100644 --- a/src/main/java/de/caritas/cob/userservice/api/admin/service/consultant/ConsultantAdminService.java +++ b/src/main/java/de/caritas/cob/userservice/api/admin/service/consultant/ConsultantAdminService.java @@ -13,11 +13,10 @@ import de.caritas.cob.userservice.api.adapters.web.dto.CreateConsultantDTO; import de.caritas.cob.userservice.api.adapters.web.dto.UpdateAdminConsultantDTO; import de.caritas.cob.userservice.api.adapters.web.dto.UpdateConsultantDTO; -import de.caritas.cob.userservice.api.admin.service.consultant.create.ConsultantCreatorService; +import de.caritas.cob.userservice.api.admin.service.consultant.create.ConsultantCreateSaga; import de.caritas.cob.userservice.api.admin.service.consultant.delete.ConsultantPreDeletionService; import de.caritas.cob.userservice.api.admin.service.consultant.update.ConsultantUpdateService; import de.caritas.cob.userservice.api.exception.httpresponses.DistributedTransactionException; -import de.caritas.cob.userservice.api.exception.httpresponses.DistributedTransactionInfo; import de.caritas.cob.userservice.api.exception.httpresponses.NoContentException; import de.caritas.cob.userservice.api.exception.httpresponses.NotFoundException; import de.caritas.cob.userservice.api.helper.AuthenticatedUser; @@ -26,7 +25,6 @@ import de.caritas.cob.userservice.api.port.out.ConsultantRepository; import de.caritas.cob.userservice.api.port.out.SessionRepository; import de.caritas.cob.userservice.api.service.appointment.AppointmentService; -import java.util.List; import java.util.Map; import lombok.NonNull; import lombok.RequiredArgsConstructor; @@ -40,10 +38,9 @@ public class ConsultantAdminService { private final @NonNull ConsultantRepository consultantRepository; - private final @NonNull ConsultantCreatorService consultantCreatorService; + private final @NonNull ConsultantCreateSaga consultantCreateSaga; private final @NonNull ConsultantUpdateService consultantUpdateService; private final @NonNull ConsultantPreDeletionService consultantPreDeletionService; - private final @NonNull AppointmentService appointmentService; private final @NonNull SessionRepository sessionRepository; @@ -51,6 +48,8 @@ public class ConsultantAdminService { private final @NonNull AccountManager accountManager; + private final @NonNull AppointmentService appointmentService; + /** * Finds a {@link Consultant} by the given consultant id and throws a {@link NoContentException} * if no consultant for given id exists. @@ -90,36 +89,7 @@ private static String getDisplayNameFromUserMap(Map map) { */ public ConsultantAdminResponseDTO createNewConsultant(CreateConsultantDTO createConsultantDTO) throws DistributedTransactionException { - Consultant newConsultant = - this.consultantCreatorService.createNewConsultant(createConsultantDTO); - List completedSteps = - Lists.newArrayList( - TransactionalStep.CREATE_ACCOUNT_IN_KEYCLOAK, - TransactionalStep.CREATE_ACCOUNT_IN_ROCKETCHAT, - TransactionalStep.CREATE_CONSULTANT_IN_MARIADB); - - ConsultantAdminResponseDTO consultantAdminResponseDTO = - ConsultantResponseDTOBuilder.getInstance(newConsultant).buildResponseDTO(); - - try { - this.appointmentService.createConsultant(consultantAdminResponseDTO); - } catch (Exception e) { - log.error( - "User with id {}, who has roles {}, has created a consultant with id {} but the appointment service returned an error: {}", - authenticatedUser.getUserId(), - authenticatedUser.getRoles(), - newConsultant.getId(), - e.getMessage()); - this.consultantCreatorService.rollbackCreateNewConsultant(newConsultant); - throw new DistributedTransactionException( - e, - new DistributedTransactionInfo( - "createNewConsultant", - completedSteps, - TransactionalStep.CREATE_ACCOUNT_IN_CALCOM_OR_APPOINTMENTSERVICE)); - } - - return consultantAdminResponseDTO; + return this.consultantCreateSaga.createNewConsultant(createConsultantDTO); } /** diff --git a/src/main/java/de/caritas/cob/userservice/api/admin/service/consultant/TransactionalStep.java b/src/main/java/de/caritas/cob/userservice/api/admin/service/consultant/TransactionalStep.java index 2d8c19b4a..13542387b 100644 --- a/src/main/java/de/caritas/cob/userservice/api/admin/service/consultant/TransactionalStep.java +++ b/src/main/java/de/caritas/cob/userservice/api/admin/service/consultant/TransactionalStep.java @@ -14,5 +14,7 @@ public enum TransactionalStep { ROLLBACK_UPDATE_ROCKET_CHAT_USER_DISPLAY_NAME, - PATCH_APPOINTMENT_SERVICE_CONSULTANT; + PATCH_APPOINTMENT_SERVICE_CONSULTANT, + UPDATE_USER_PASSWORD_IN_KEYCLOAK, + UPDATE_USER_ROLES_IN_KEYCLOAK; } diff --git a/src/main/java/de/caritas/cob/userservice/api/admin/service/consultant/create/ConsultantCreatorService.java b/src/main/java/de/caritas/cob/userservice/api/admin/service/consultant/create/ConsultantCreateSaga.java similarity index 56% rename from src/main/java/de/caritas/cob/userservice/api/admin/service/consultant/create/ConsultantCreatorService.java rename to src/main/java/de/caritas/cob/userservice/api/admin/service/consultant/create/ConsultantCreateSaga.java index 13dba1c52..0afc3711d 100644 --- a/src/main/java/de/caritas/cob/userservice/api/admin/service/consultant/create/ConsultantCreatorService.java +++ b/src/main/java/de/caritas/cob/userservice/api/admin/service/consultant/create/ConsultantCreateSaga.java @@ -6,20 +6,24 @@ import static org.apache.commons.lang3.BooleanUtils.isTrue; import static org.hibernate.validator.internal.util.CollectionHelper.asSet; +import com.google.common.collect.Lists; import com.neovisionaries.i18n.LanguageCode; import de.caritas.cob.userservice.api.adapters.keycloak.dto.KeycloakCreateUserResponseDTO; import de.caritas.cob.userservice.api.adapters.rocketchat.RocketChatService; +import de.caritas.cob.userservice.api.adapters.web.dto.ConsultantAdminResponseDTO; import de.caritas.cob.userservice.api.adapters.web.dto.CreateConsultantDTO; import de.caritas.cob.userservice.api.adapters.web.dto.NotificationsSettingsDTO; import de.caritas.cob.userservice.api.adapters.web.dto.UserDTO; +import de.caritas.cob.userservice.api.admin.service.consultant.ConsultantResponseDTOBuilder; +import de.caritas.cob.userservice.api.admin.service.consultant.TransactionalStep; import de.caritas.cob.userservice.api.admin.service.consultant.validation.CreateConsultantDTOAbsenceInputAdapter; import de.caritas.cob.userservice.api.admin.service.consultant.validation.UserAccountInputValidator; import de.caritas.cob.userservice.api.admin.service.tenant.TenantAdminService; import de.caritas.cob.userservice.api.exception.httpresponses.BadRequestException; import de.caritas.cob.userservice.api.exception.httpresponses.CustomValidationHttpStatusException; -import de.caritas.cob.userservice.api.exception.httpresponses.InternalServerErrorException; +import de.caritas.cob.userservice.api.exception.httpresponses.DistributedTransactionException; +import de.caritas.cob.userservice.api.exception.httpresponses.DistributedTransactionInfo; import de.caritas.cob.userservice.api.exception.httpresponses.customheader.HttpStatusExceptionReason; -import de.caritas.cob.userservice.api.exception.rocketchat.RocketChatLoginException; import de.caritas.cob.userservice.api.facade.rollback.RollbackFacade; import de.caritas.cob.userservice.api.helper.AuthenticatedUser; import de.caritas.cob.userservice.api.helper.UserHelper; @@ -29,6 +33,7 @@ import de.caritas.cob.userservice.api.port.out.IdentityClient; import de.caritas.cob.userservice.api.service.ConsultantImportService.ImportRecord; import de.caritas.cob.userservice.api.service.ConsultantService; +import de.caritas.cob.userservice.api.service.appointment.AppointmentService; import de.caritas.cob.userservice.api.tenant.TenantContext; import de.caritas.cob.userservice.tenantadminservice.generated.web.model.TenantDTO; import java.util.Set; @@ -37,6 +42,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; /** * Creator class to generate new {@link Consultant} instances in database, keycloak and rocket chat. @@ -44,7 +50,7 @@ @Service @RequiredArgsConstructor @Slf4j -public class ConsultantCreatorService { +public class ConsultantCreateSaga { private final @NonNull IdentityClient identityClient; private final @NonNull RocketChatService rocketChatService; @@ -55,11 +61,16 @@ public class ConsultantCreatorService { private final @NonNull RollbackFacade rollbackFacade; + @Value("${feature.appointment.enabled}") + private boolean appointmentFeatureEnabled; + @Value("${multitenancy.enabled}") private boolean multiTenancyEnabled; private final @NonNull AuthenticatedUser authenticatedUser; + private final @NonNull AppointmentService appointmentService; + /** * Creates a new {@link Consultant} by {@link CreateConsultantDTO} in database, keycloak and * rocket chat. @@ -67,7 +78,8 @@ public class ConsultantCreatorService { * @param createConsultantDTO the input used for creation * @return the generated {@link Consultant} */ - public Consultant createNewConsultant(CreateConsultantDTO createConsultantDTO) { + private Consultant createNewConsultantWithoutAppointment( + CreateConsultantDTO createConsultantDTO) { assertLicensesNotExceeded(); this.userAccountInputValidator.validateAbsence( new CreateConsultantDTOAbsenceInputAdapter(createConsultantDTO)); @@ -81,6 +93,46 @@ public Consultant createNewConsultant(CreateConsultantDTO createConsultantDTO) { return createNewConsultant(consultantCreationInput, roles); } + @Transactional + public ConsultantAdminResponseDTO createNewConsultant(CreateConsultantDTO createConsultantDTO) + throws DistributedTransactionException { + Consultant newConsultant = this.createNewConsultantWithoutAppointment(createConsultantDTO); + + ConsultantAdminResponseDTO consultantAdminResponseDTO = + ConsultantResponseDTOBuilder.getInstance(newConsultant).buildResponseDTO(); + + if (appointmentFeatureEnabled) { + createConsultantInAppointmentServiceOrRollback(newConsultant, consultantAdminResponseDTO); + } + return consultantAdminResponseDTO; + } + + private void createConsultantInAppointmentServiceOrRollback( + Consultant newConsultant, ConsultantAdminResponseDTO consultantAdminResponseDTO) { + try { + this.appointmentService.createConsultant(consultantAdminResponseDTO); + } catch (Exception e) { + log.error( + "User with id {}, who has roles {}, has created a consultant with id {} but the appointment service returned an error: {}", + authenticatedUser.getUserId(), + authenticatedUser.getRoles(), + newConsultant.getId(), + e.getMessage()); + this.rollbackCreateNewConsultant(newConsultant); + throw new DistributedTransactionException( + e, + new DistributedTransactionInfo( + "createNewConsultant", + Lists.newArrayList( + TransactionalStep.CREATE_ACCOUNT_IN_KEYCLOAK, + TransactionalStep.UPDATE_USER_PASSWORD_IN_KEYCLOAK, + TransactionalStep.UPDATE_USER_ROLES_IN_KEYCLOAK, + TransactionalStep.CREATE_ACCOUNT_IN_ROCKETCHAT, + TransactionalStep.CREATE_CONSULTANT_IN_MARIADB), + TransactionalStep.CREATE_ACCOUNT_IN_CALCOM_OR_APPOINTMENTSERVICE)); + } + } + private void validateTenantId(CreateConsultantDTO createConsultantDTO) { if (authenticatedUser.isTenantSuperAdmin()) { if (createConsultantDTO.getTenantId() == null) { @@ -122,14 +174,87 @@ private Consultant createNewConsultant( String keycloakUserId = createKeycloakUser(consultantCreationInput); String password = userHelper.getRandomPassword(); - identityClient.updatePassword(keycloakUserId, password); - roles.forEach(roleName -> identityClient.updateRole(keycloakUserId, roleName)); + updateKeycloakPasswordOrRollback(consultantCreationInput, keycloakUserId, password); + updateKeyloakRolesOrRollback(roles, keycloakUserId, consultantCreationInput); String rocketChatUserId = - createRocketChatUser(consultantCreationInput, keycloakUserId, password); + createRocketChatUserOrRollback(consultantCreationInput, keycloakUserId, password); - return consultantService.saveConsultant( - buildConsultant(consultantCreationInput, keycloakUserId, rocketChatUserId)); + return createConsultantInMariaDBOrRollback( + consultantCreationInput, keycloakUserId, rocketChatUserId); + } + + private void updateKeycloakPasswordOrRollback( + ConsultantCreationInput consultantCreationInput, String keycloakUserId, String password) { + try { + identityClient.updatePassword(keycloakUserId, password); + } catch (Exception e) { + log.error( + "Unable to update password or roles for user with encoded username {}", + consultantCreationInput.getEncodedUsername()); + rollbackCreateNewConsultant( + buildConsultantDataWithUnknownRocketChatId(consultantCreationInput, keycloakUserId)); + throw new DistributedTransactionException( + e, + DistributedTransactionInfo.builder() + .completedTransactionalOperations( + Lists.newArrayList(TransactionalStep.CREATE_ACCOUNT_IN_KEYCLOAK)) + .name("createConsultant") + .failedStep(TransactionalStep.UPDATE_USER_PASSWORD_IN_KEYCLOAK) + .build()); + } + } + + private void updateKeyloakRolesOrRollback( + Set roles, String keycloakUserId, ConsultantCreationInput consultantCreationInput) { + try { + roles.forEach(roleName -> identityClient.updateRole(keycloakUserId, roleName)); + } catch (Exception e) { + log.error( + "Unable to update roles for user with keycloak id {}. Initiating user rollback.", + keycloakUserId); + rollbackCreateNewConsultant( + buildConsultantDataWithUnknownRocketChatId(consultantCreationInput, keycloakUserId)); + throw new DistributedTransactionException( + e, + DistributedTransactionInfo.builder() + .completedTransactionalOperations( + Lists.newArrayList( + TransactionalStep.CREATE_ACCOUNT_IN_KEYCLOAK, + TransactionalStep.UPDATE_USER_PASSWORD_IN_KEYCLOAK)) + .name("createConsultant") + .failedStep(TransactionalStep.UPDATE_USER_ROLES_IN_KEYCLOAK) + .build()); + } + } + + private Consultant createConsultantInMariaDBOrRollback( + ConsultantCreationInput consultantCreationInput, + String keycloakUserId, + String rocketChatUserId) { + Consultant consultant = + buildConsultant(consultantCreationInput, keycloakUserId, rocketChatUserId); + try { + return consultantService.saveConsultant(consultant); + } catch (Exception e) { + log.error( + "Unable to create consultant with encoded username {} in database. Rolling back keycloak and rocketchat user creation", + consultantCreationInput.getEncodedUsername()); + rollbackCreateNewConsultant(consultant); + + throw new DistributedTransactionException( + e, + DistributedTransactionInfo.builder() + .completedTransactionalOperations( + Lists.newArrayList( + TransactionalStep.CREATE_ACCOUNT_IN_KEYCLOAK, + TransactionalStep.UPDATE_USER_PASSWORD_IN_KEYCLOAK, + TransactionalStep.UPDATE_USER_ROLES_IN_KEYCLOAK, + TransactionalStep.CREATE_ACCOUNT_IN_ROCKETCHAT)) + .name("createConsultant") + .failedStep(TransactionalStep.CREATE_CONSULTANT_IN_MARIADB) + .build()); + } } private void assignCurrentTenantContext(CreateConsultantDTO createConsultantDTO) { @@ -159,17 +284,44 @@ private String createKeycloakUser(ConsultantCreationInput consultantCreationInpu return response.getUserId(); } - private String createRocketChatUser( + private String createRocketChatUserOrRollback( ConsultantCreationInput consultantCreationInput, String keycloakUserId, String password) { try { return this.rocketChatService.getUserID( consultantCreationInput.getEncodedUsername(), password, true); - } catch (RocketChatLoginException e) { - throw new InternalServerErrorException( - String.format("Unable to login user with id %s first time", keycloakUserId)); + } catch (Exception e) { + log.error( + "Unable to create user with encoded username {} in rocketchat. Does this user already exist?", + consultantCreationInput.getEncodedUsername()); + rollbackCreateNewConsultant( + buildConsultantDataWithUnknownRocketChatId(consultantCreationInput, keycloakUserId)); + throw new DistributedTransactionException( + e, + DistributedTransactionInfo.builder() + .completedTransactionalOperations( + Lists.newArrayList( + TransactionalStep.CREATE_ACCOUNT_IN_KEYCLOAK, + TransactionalStep.UPDATE_USER_PASSWORD_IN_KEYCLOAK, + TransactionalStep.UPDATE_USER_ROLES_IN_KEYCLOAK)) + .name("createConsultant") + .failedStep(TransactionalStep.CREATE_ACCOUNT_IN_ROCKETCHAT) + .build()); } } + private static Consultant buildConsultantDataWithUnknownRocketChatId( + ConsultantCreationInput consultantCreationInput, String keycloakUserId) { + return Consultant.builder() + .id(keycloakUserId) + .tenantId(consultantCreationInput.getTenantId()) + .rocketChatId("unknown") + .username(consultantCreationInput.getEncodedUsername()) + .firstName(consultantCreationInput.getFirstName()) + .lastName(consultantCreationInput.getLastName()) + .email(consultantCreationInput.getEmail()) + .build(); + } + private Consultant buildConsultant( ConsultantCreationInput consultantCreationInput, String keycloakUserId, diff --git a/src/main/java/de/caritas/cob/userservice/api/facade/rollback/RollbackFacade.java b/src/main/java/de/caritas/cob/userservice/api/facade/rollback/RollbackFacade.java index 26eb0cb0d..fb886cbc4 100644 --- a/src/main/java/de/caritas/cob/userservice/api/facade/rollback/RollbackFacade.java +++ b/src/main/java/de/caritas/cob/userservice/api/facade/rollback/RollbackFacade.java @@ -32,14 +32,16 @@ public class RollbackFacade { private final @NonNull DeleteUserAccountService deleteUserAccountService; public void rollbackConsultantAccount(Consultant consultant) { - log.info("Rollback consultant account: {}", consultant); + log.info( + "Initiating rollback of consultant account. Consultant id: {}", + consultant.getId(), + consultant.getUsername()); consultant.setDeleteDate(LocalDateTime.now()); List deletionWorkflowErrors = deleteUserAccountService.performConsultantDeletion(consultant); if (nonNull(deletionWorkflowErrors) && !deletionWorkflowErrors.isEmpty()) { - deletionWorkflowErrors.stream() - .forEach(e -> log.error("Consultant delete workflow error: ", e)); + .forEach(e -> log.error("Consultant delete error during rollback: ", e)); } } /** diff --git a/src/main/java/de/caritas/cob/userservice/api/service/ConsultantImportService.java b/src/main/java/de/caritas/cob/userservice/api/service/ConsultantImportService.java index 9eaaa88ad..431eb8f66 100644 --- a/src/main/java/de/caritas/cob/userservice/api/service/ConsultantImportService.java +++ b/src/main/java/de/caritas/cob/userservice/api/service/ConsultantImportService.java @@ -3,7 +3,7 @@ import static org.apache.commons.lang3.BooleanUtils.isTrue; import de.caritas.cob.userservice.api.adapters.web.dto.AgencyDTO; -import de.caritas.cob.userservice.api.admin.service.consultant.create.ConsultantCreatorService; +import de.caritas.cob.userservice.api.admin.service.consultant.create.ConsultantCreateSaga; import de.caritas.cob.userservice.api.admin.service.consultant.create.agencyrelation.ConsultantAgencyRelationCreatorService; import de.caritas.cob.userservice.api.exception.ImportException; import de.caritas.cob.userservice.api.exception.httpresponses.InternalServerErrorException; @@ -55,7 +55,7 @@ public class ConsultantImportService { private final @NonNull ConsultingTypeManager consultingTypeManager; private final @NonNull AgencyService agencyService; private final @NonNull UserHelper userHelper; - private final @NonNull ConsultantCreatorService consultantCreatorService; + private final @NonNull ConsultantCreateSaga consultantCreateSaga; private final @NonNull ConsultantAgencyRelationCreatorService consultantAgencyRelationCreatorService; @@ -226,7 +226,7 @@ public void startImport() { writeToImportLog(logMessage); if (importRecord.getConsultantId() == null) { - consultant = this.consultantCreatorService.createNewConsultant(importRecord, roles); + consultant = this.consultantCreateSaga.createNewConsultant(importRecord, roles); importRecord.setConsultantId(consultant.getId()); logMessage = "Keycloak-ID: " + consultant.getId(); diff --git a/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/ConsultantAdminServiceIT.java b/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/ConsultantAdminServiceIT.java index 06c16e2ac..8e5e40ec5 100644 --- a/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/ConsultantAdminServiceIT.java +++ b/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/ConsultantAdminServiceIT.java @@ -15,7 +15,7 @@ import de.caritas.cob.userservice.api.adapters.web.dto.CreateConsultantDTO; import de.caritas.cob.userservice.api.adapters.web.dto.HalLink.MethodEnum; import de.caritas.cob.userservice.api.adapters.web.dto.UpdateAdminConsultantDTO; -import de.caritas.cob.userservice.api.admin.service.consultant.create.ConsultantCreatorService; +import de.caritas.cob.userservice.api.admin.service.consultant.create.ConsultantCreateSaga; import de.caritas.cob.userservice.api.admin.service.consultant.update.ConsultantUpdateService; import de.caritas.cob.userservice.api.exception.httpresponses.NoContentException; import de.caritas.cob.userservice.api.model.Consultant; @@ -53,7 +53,7 @@ public class ConsultantAdminServiceIT { @Autowired private ConsultantAgencyRepository consultantAgencyRepository; - @MockBean private ConsultantCreatorService consultantCreatorService; + @MockBean private ConsultantCreateSaga consultantCreateSaga; @MockBean private ConsultantUpdateService consultantUpdateService; @@ -121,13 +121,13 @@ public void findConsultantById_Should_throwNoContentException_When_consultantIdD public void createNewConsultant_Should_useCreatorServiceAndBuildConsultantAdminResponseDTO() { CreateConsultantDTO createConsultantDTO = new EasyRandom().nextObject(CreateConsultantDTO.class); - when(this.consultantCreatorService.createNewConsultant(any())) - .thenReturn(new EasyRandom().nextObject(Consultant.class)); + when(this.consultantCreateSaga.createNewConsultant(any())) + .thenReturn(new EasyRandom().nextObject(ConsultantAdminResponseDTO.class)); ConsultantAdminResponseDTO result = this.consultantAdminService.createNewConsultant(createConsultantDTO); - verify(this.consultantCreatorService, times(1)).createNewConsultant(createConsultantDTO); + verify(this.consultantCreateSaga, times(1)).createNewConsultant(createConsultantDTO); assertThat(result.getLinks(), notNullValue()); assertThat(result.getEmbedded(), notNullValue()); } diff --git a/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/ConsultantAdminServiceTest.java b/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/ConsultantAdminServiceTest.java index 3e5a971e4..e76b505e9 100644 --- a/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/ConsultantAdminServiceTest.java +++ b/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/ConsultantAdminServiceTest.java @@ -7,7 +7,7 @@ import static org.powermock.api.mockito.PowerMockito.when; import de.caritas.cob.userservice.api.AccountManager; -import de.caritas.cob.userservice.api.admin.service.consultant.create.ConsultantCreatorService; +import de.caritas.cob.userservice.api.admin.service.consultant.create.ConsultantCreateSaga; import de.caritas.cob.userservice.api.admin.service.consultant.delete.ConsultantPreDeletionService; import de.caritas.cob.userservice.api.admin.service.consultant.update.ConsultantUpdateService; import de.caritas.cob.userservice.api.exception.httpresponses.NotFoundException; @@ -30,7 +30,7 @@ public class ConsultantAdminServiceTest { @Mock private ConsultantRepository consultantRepository; - @Mock private ConsultantCreatorService consultantCreatorService; + @Mock private ConsultantCreateSaga consultantCreateSaga; @Mock private ConsultantUpdateService consultantUpdateService; diff --git a/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/create/ConsultantCreatorServiceIT.java b/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/create/ConsultantCreateSagaIT.java similarity index 51% rename from src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/create/ConsultantCreatorServiceIT.java rename to src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/create/ConsultantCreateSagaIT.java index 1da133c48..fe15f0403 100644 --- a/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/create/ConsultantCreatorServiceIT.java +++ b/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/create/ConsultantCreateSagaIT.java @@ -12,6 +12,7 @@ import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -20,18 +21,24 @@ import de.caritas.cob.userservice.api.adapters.keycloak.KeycloakService; import de.caritas.cob.userservice.api.adapters.keycloak.dto.KeycloakCreateUserResponseDTO; import de.caritas.cob.userservice.api.adapters.rocketchat.RocketChatService; +import de.caritas.cob.userservice.api.adapters.web.dto.ConsultantDTO; import de.caritas.cob.userservice.api.adapters.web.dto.CreateConsultantDTO; import de.caritas.cob.userservice.api.admin.service.tenant.TenantAdminService; +import de.caritas.cob.userservice.api.exception.httpresponses.BadRequestException; import de.caritas.cob.userservice.api.exception.httpresponses.CustomValidationHttpStatusException; -import de.caritas.cob.userservice.api.exception.httpresponses.InternalServerErrorException; +import de.caritas.cob.userservice.api.exception.httpresponses.DistributedTransactionException; import de.caritas.cob.userservice.api.exception.rocketchat.RocketChatLoginException; +import de.caritas.cob.userservice.api.facade.rollback.RollbackFacade; import de.caritas.cob.userservice.api.model.Consultant; import de.caritas.cob.userservice.api.service.ConsultantImportService.ImportRecord; +import de.caritas.cob.userservice.api.service.appointment.AppointmentService; import de.caritas.cob.userservice.tenantadminservice.generated.web.model.Settings; import de.caritas.cob.userservice.tenantadminservice.generated.web.model.TenantDTO; import org.jeasy.random.EasyRandom; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace; @@ -39,19 +46,20 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.util.ReflectionTestUtils; @RunWith(SpringRunner.class) @SpringBootTest(classes = UserServiceApplication.class) @TestPropertySource(properties = "spring.profiles.active=testing") @AutoConfigureTestDatabase(replace = Replace.ANY) -public class ConsultantCreatorServiceIT { +public class ConsultantCreateSagaIT { private static final String DUMMY_RC_ID = "rcUserId"; private static final String VALID_USERNAME = "validUsername"; private static final String VALID_EMAILADDRESS = "valid@emailaddress.de"; private static final long TENANT_ID = 1L; - @Autowired private ConsultantCreatorService consultantCreatorService; + @Autowired private ConsultantCreateSaga consultantCreateSaga; @MockBean private RocketChatService rocketChatService; @@ -59,8 +67,17 @@ public class ConsultantCreatorServiceIT { @MockBean private TenantAdminService tenantAdminService; + @MockBean private RollbackFacade rollbackFacade; + + @MockBean private AppointmentService appointmentService; + private final EasyRandom easyRandom = new EasyRandom(); + @Before + public void setup() { + ReflectionTestUtils.setField(consultantCreateSaga, "appointmentFeatureEnabled", false); + } + @Test public void createNewConsultant_Should_returnExpectedCreatedConsultant_When_inputDataIsCorrect() throws RocketChatLoginException { @@ -73,23 +90,147 @@ public void createNewConsultant_Should_returnExpectedCreatedConsultant_When_inpu createConsultantDTO.setEmail(VALID_EMAILADDRESS); createConsultantDTO.setIsGroupchatConsultant(false); - Consultant consultant = this.consultantCreatorService.createNewConsultant(createConsultantDTO); + var consultantAdminResponseDTO = + this.consultantCreateSaga.createNewConsultant(createConsultantDTO); + ConsultantDTO consultant = consultantAdminResponseDTO.getEmbedded(); verify(keycloakService).updateRole(anyString(), eq(CONSULTANT.getValue())); assertThat(consultant, notNullValue()); assertThat(consultant.getId(), notNullValue()); - assertThat(consultant.getRocketChatId(), is(DUMMY_RC_ID)); assertThat(consultant.getAbsenceMessage(), notNullValue()); assertThat(consultant.getCreateDate(), notNullValue()); assertThat(consultant.getUpdateDate(), notNullValue()); assertThat(consultant.getUsername(), notNullValue()); - assertThat(consultant.getFirstName(), notNullValue()); - assertThat(consultant.getLastName(), notNullValue()); + assertThat(consultant.getFirstname(), notNullValue()); + assertThat(consultant.getLastname(), notNullValue()); assertThat(consultant.getEmail(), notNullValue()); - assertThat(consultant.getFullName(), notNullValue()); - assertThat(consultant.isNotificationsEnabled(), is(true)); - assertThat(consultant.getNotificationsSettings(), notNullValue()); + } + + @Test + public void createNewConsultant_Should_callRollback_When_RocketchatThrowsException() + throws RocketChatLoginException { + doThrow(BadRequestException.class) + .when(rocketChatService) + .getUserID(anyString(), anyString(), anyBoolean()); + when(keycloakService.createKeycloakUser(any(), anyString(), any())) + .thenReturn(easyRandom.nextObject(KeycloakCreateUserResponseDTO.class)); + CreateConsultantDTO createConsultantDTO = this.easyRandom.nextObject(CreateConsultantDTO.class); + createConsultantDTO.setUsername(VALID_USERNAME); + createConsultantDTO.setEmail(VALID_EMAILADDRESS); + createConsultantDTO.setIsGroupchatConsultant(false); + + try { + this.consultantCreateSaga.createNewConsultant(createConsultantDTO); + fail("Exception should be thrown"); + } catch (DistributedTransactionException ex) { + assertThat( + ex.getCustomHttpHeaders().get("X-Reason").get(0), + is("DISTRIBUTED_TRANSACTION_FAILED_ON_STEP_CREATE_ACCOUNT_IN_ROCKETCHAT")); + verify(keycloakService).updateRole(anyString(), eq(CONSULTANT.getValue())); + verify(keycloakService).updateRole(anyString(), eq(CONSULTANT.getValue())); + verify(rollbackFacade).rollbackConsultantAccount(Mockito.any(Consultant.class)); + } + } + + @Test + public void createNewConsultant_Should_callRollback_When_AppointmentServiceThrowsException() + throws RocketChatLoginException { + ReflectionTestUtils.setField(consultantCreateSaga, "appointmentFeatureEnabled", true); + doThrow(BadRequestException.class).when(appointmentService).createConsultant(any()); + when(keycloakService.createKeycloakUser(any(), anyString(), any())) + .thenReturn(easyRandom.nextObject(KeycloakCreateUserResponseDTO.class)); + when(rocketChatService.getUserID(anyString(), anyString(), anyBoolean())) + .thenReturn(DUMMY_RC_ID); + CreateConsultantDTO createConsultantDTO = this.easyRandom.nextObject(CreateConsultantDTO.class); + createConsultantDTO.setUsername(VALID_USERNAME); + createConsultantDTO.setEmail(VALID_EMAILADDRESS); + createConsultantDTO.setIsGroupchatConsultant(false); + + try { + this.consultantCreateSaga.createNewConsultant(createConsultantDTO); + fail("Exception should be thrown"); + } catch (DistributedTransactionException ex) { + assertThat( + ex.getCustomHttpHeaders().get("X-Reason").get(0), + is( + "DISTRIBUTED_TRANSACTION_FAILED_ON_STEP_CREATE_ACCOUNT_IN_CALCOM_OR_APPOINTMENTSERVICE")); + verify(keycloakService).updateRole(anyString(), eq(CONSULTANT.getValue())); + verify(keycloakService).updateRole(anyString(), eq(CONSULTANT.getValue())); + verify(rocketChatService).getUserID(anyString(), anyString(), anyBoolean()); + verify(rollbackFacade).rollbackConsultantAccount(Mockito.any(Consultant.class)); + } + } + + @Test + public void createNewConsultant_Should_callRollback_When_KeycloakUpdatePasswordThrowsException() { + when(keycloakService.createKeycloakUser(any(), anyString(), any())) + .thenReturn(easyRandom.nextObject(KeycloakCreateUserResponseDTO.class)); + doThrow(BadRequestException.class).when(keycloakService).updatePassword(any(), any()); + CreateConsultantDTO createConsultantDTO = this.easyRandom.nextObject(CreateConsultantDTO.class); + createConsultantDTO.setUsername(VALID_USERNAME); + createConsultantDTO.setEmail(VALID_EMAILADDRESS); + createConsultantDTO.setIsGroupchatConsultant(false); + + try { + this.consultantCreateSaga.createNewConsultant(createConsultantDTO); + fail("Exception should be thrown"); + } catch (DistributedTransactionException ex) { + assertThat( + ex.getCustomHttpHeaders().get("X-Reason").get(0), + is("DISTRIBUTED_TRANSACTION_FAILED_ON_STEP_UPDATE_USER_PASSWORD_IN_KEYCLOAK")); + verify(keycloakService, Mockito.never()).updateRole(anyString(), eq(CONSULTANT.getValue())); + verify(rollbackFacade).rollbackConsultantAccount(Mockito.any(Consultant.class)); + } + } + + @Test + public void createNewConsultant_Should_callRollback_When_KeycloakUpdateRoleThrowsException() + throws RocketChatLoginException { + when(keycloakService.createKeycloakUser(any(), anyString(), any())) + .thenReturn(easyRandom.nextObject(KeycloakCreateUserResponseDTO.class)); + doThrow(BadRequestException.class).when(keycloakService).updateRole(anyString(), anyString()); + CreateConsultantDTO createConsultantDTO = this.easyRandom.nextObject(CreateConsultantDTO.class); + createConsultantDTO.setUsername(VALID_USERNAME); + createConsultantDTO.setEmail(VALID_EMAILADDRESS); + createConsultantDTO.setIsGroupchatConsultant(false); + + try { + this.consultantCreateSaga.createNewConsultant(createConsultantDTO); + fail("Exception should be thrown"); + } catch (DistributedTransactionException ex) { + assertThat( + ex.getCustomHttpHeaders().get("X-Reason").get(0), + is("DISTRIBUTED_TRANSACTION_FAILED_ON_STEP_UPDATE_USER_ROLES_IN_KEYCLOAK")); + verify(rocketChatService, Mockito.never()).getUserID(anyString(), anyString(), anyBoolean()); + verify(rollbackFacade).rollbackConsultantAccount(Mockito.any(Consultant.class)); + } + } + + @Test + public void createNewConsultant_Should_callRollback_When_anyOfTheServicesThrowsException() + throws RocketChatLoginException { + doThrow(BadRequestException.class) + .when(rocketChatService) + .getUserID(anyString(), anyString(), anyBoolean()); + when(keycloakService.createKeycloakUser(any(), anyString(), any())) + .thenReturn(easyRandom.nextObject(KeycloakCreateUserResponseDTO.class)); + CreateConsultantDTO createConsultantDTO = this.easyRandom.nextObject(CreateConsultantDTO.class); + createConsultantDTO.setUsername(VALID_USERNAME); + createConsultantDTO.setEmail(VALID_EMAILADDRESS); + createConsultantDTO.setIsGroupchatConsultant(false); + + try { + this.consultantCreateSaga.createNewConsultant(createConsultantDTO); + fail("Exception should be thrown"); + } catch (DistributedTransactionException ex) { + assertThat( + ex.getCustomHttpHeaders().get("X-Reason").get(0), + is("DISTRIBUTED_TRANSACTION_FAILED_ON_STEP_CREATE_ACCOUNT_IN_ROCKETCHAT")); + verify(keycloakService).updateRole(anyString(), eq(CONSULTANT.getValue())); + verify(keycloakService).updateRole(anyString(), eq(CONSULTANT.getValue())); + verify(rollbackFacade).rollbackConsultantAccount(Mockito.any(Consultant.class)); + } } @Test @@ -111,15 +252,15 @@ public void createNewConsultant_Should_returnExpectedCreatedConsultant_When_inpu createConsultantDTO.setIsGroupchatConsultant(true); // when - Consultant consultant = consultantCreatorService.createNewConsultant(createConsultantDTO); + var consultantAdminResponseDTO = consultantCreateSaga.createNewConsultant(createConsultantDTO); // then verify(keycloakService, times(2)).updateRole(anyString(), anyString()); verify(keycloakService).updateRole(anyString(), eq(CONSULTANT.getValue())); verify(keycloakService).updateRole(anyString(), eq(GROUP_CHAT_CONSULTANT.getValue())); - assertThat(consultant, notNullValue()); - assertThat(consultant.getId(), notNullValue()); + assertThat(consultantAdminResponseDTO.getEmbedded(), notNullValue()); + assertThat(consultantAdminResponseDTO.getEmbedded().getId(), notNullValue()); } @Test @@ -135,8 +276,7 @@ public void createNewConsultant_Should_returnExpectedCreatedConsultant_When_inpu importRecord.setEmail(VALID_EMAILADDRESS); Consultant consultant = - this.consultantCreatorService.createNewConsultant( - importRecord, asSet(CONSULTANT.getValue())); + this.consultantCreateSaga.createNewConsultant(importRecord, asSet(CONSULTANT.getValue())); assertThat(consultant, notNullValue()); assertThat(consultant.getId(), notNullValue()); @@ -151,7 +291,7 @@ public void createNewConsultant_Should_returnExpectedCreatedConsultant_When_inpu assertThat(consultant.getFullName(), notNullValue()); } - @Test(expected = InternalServerErrorException.class) + @Test(expected = DistributedTransactionException.class) public void createNewConsultant_Should_throwCustomValidationHttpStatusException_When_userCanNotBeCreatedInRocketChat() throws RocketChatLoginException { @@ -165,7 +305,7 @@ public void createNewConsultant_Should_returnExpectedCreatedConsultant_When_inpu createConsultantDTO.setUsername(VALID_USERNAME); createConsultantDTO.setEmail(VALID_EMAILADDRESS); - this.consultantCreatorService.createNewConsultant(createConsultantDTO); + this.consultantCreateSaga.createNewConsultant(createConsultantDTO); } @Test(expected = CustomValidationHttpStatusException.class) @@ -181,7 +321,7 @@ public void createNewConsultant_Should_returnExpectedCreatedConsultant_When_inpu .thenReturn(keycloakResponse); CreateConsultantDTO createConsultantDTO = this.easyRandom.nextObject(CreateConsultantDTO.class); - this.consultantCreatorService.createNewConsultant(createConsultantDTO); + this.consultantCreateSaga.createNewConsultant(createConsultantDTO); } @Test @@ -190,7 +330,7 @@ public void createNewConsultant_Should_throwExpectedException_When_emailIsInvali createConsultantDTO.setEmail("invalid"); try { - this.consultantCreatorService.createNewConsultant(createConsultantDTO); + this.consultantCreateSaga.createNewConsultant(createConsultantDTO); fail("Exception should be thrown"); } catch (CustomValidationHttpStatusException e) { assertThat(e.getCustomHttpHeaders(), notNullValue()); diff --git a/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/create/ConsultantCreatorServiceTenantAwareIT.java b/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/create/ConsultantCreateSagaTenantAwareIT.java similarity index 91% rename from src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/create/ConsultantCreatorServiceTenantAwareIT.java rename to src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/create/ConsultantCreateSagaTenantAwareIT.java index 1bcce37cf..4774c7866 100644 --- a/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/create/ConsultantCreatorServiceTenantAwareIT.java +++ b/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/create/ConsultantCreateSagaTenantAwareIT.java @@ -17,6 +17,7 @@ import de.caritas.cob.userservice.api.adapters.keycloak.KeycloakService; import de.caritas.cob.userservice.api.adapters.keycloak.dto.KeycloakCreateUserResponseDTO; import de.caritas.cob.userservice.api.adapters.rocketchat.RocketChatService; +import de.caritas.cob.userservice.api.adapters.web.dto.ConsultantAdminResponseDTO; import de.caritas.cob.userservice.api.adapters.web.dto.CreateConsultantDTO; import de.caritas.cob.userservice.api.admin.service.tenant.TenantAdminService; import de.caritas.cob.userservice.api.exception.httpresponses.CustomValidationHttpStatusException; @@ -48,14 +49,14 @@ @AutoConfigureTestDatabase(replace = Replace.ANY) @TestPropertySource(properties = "multitenancy.enabled=true") @Transactional -public class ConsultantCreatorServiceTenantAwareIT { +public class ConsultantCreateSagaTenantAwareIT { private static final String DUMMY_RC_ID = "rcUserId"; private static final String VALID_USERNAME = "validUsername"; private static final String VALID_EMAILADDRESS = "valid@emailaddress.de"; private static final long TENANT_ID = 1; - @Autowired private ConsultantCreatorService consultantCreatorService; + @Autowired private ConsultantCreateSaga consultantCreateSaga; @Autowired private ConsultantRepository consultantRepository; @@ -80,7 +81,7 @@ public void tearDown() { createConsultant("username1"); createConsultant("username2"); CreateConsultantDTO createConsultantDTO = this.easyRandom.nextObject(CreateConsultantDTO.class); - this.consultantCreatorService.createNewConsultant(createConsultantDTO); + this.consultantCreateSaga.createNewConsultant(createConsultantDTO); rollbackDBState(); } @@ -110,15 +111,16 @@ public void tearDown() { createConsultantDTO.setTenantId(1L); // when - Consultant consultant = consultantCreatorService.createNewConsultant(createConsultantDTO); + ConsultantAdminResponseDTO consultant = + consultantCreateSaga.createNewConsultant(createConsultantDTO); // then verify(keycloakService, times(2)).updateRole(anyString(), anyString()); verify(keycloakService).updateRole(anyString(), eq(CONSULTANT.getValue())); verify(keycloakService).updateRole(anyString(), eq(GROUP_CHAT_CONSULTANT.getValue())); - assertThat(consultant, notNullValue()); - assertThat(consultant.getId(), notNullValue()); + assertThat(consultant.getEmbedded(), notNullValue()); + assertThat(consultant.getEmbedded().getId(), notNullValue()); } private void createConsultant(String username) { @@ -157,8 +159,7 @@ private void givenTenantApiCall() { var licensing = new Licensing(); licensing.setAllowedNumberOfUsers(2); dummyTenant.setLicensing(licensing); - ReflectionTestUtils.setField( - consultantCreatorService, "tenantAdminService", tenantAdminService); + ReflectionTestUtils.setField(consultantCreateSaga, "tenantAdminService", tenantAdminService); when(tenantAdminService.getTenantById(TenantContext.getCurrentTenant())) .thenReturn(dummyTenant); } From f1302f81db751c1efe7e611cbc475a89bcda9374 Mon Sep 17 00:00:00 2001 From: tkuzynow Date: Thu, 29 Feb 2024 14:52:55 +0100 Subject: [PATCH 2/2] fix: review comments --- .../consultant/ConsultantAdminService.java | 6 +- ...ateSaga.java => CreateConsultantSaga.java} | 56 ++++++++++--------- .../api/service/ConsultantImportService.java | 6 +- .../consultant/ConsultantAdminServiceIT.java | 8 +-- .../ConsultantAdminServiceTest.java | 4 +- ...agaIT.java => CreateConsultantSagaIT.java} | 30 +++++----- ...=> CreateConsultantSagaTenantAwareIT.java} | 10 ++-- 7 files changed, 62 insertions(+), 58 deletions(-) rename src/main/java/de/caritas/cob/userservice/api/admin/service/consultant/create/{ConsultantCreateSaga.java => CreateConsultantSaga.java} (93%) rename src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/create/{ConsultantCreateSagaIT.java => CreateConsultantSagaIT.java} (94%) rename src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/create/{ConsultantCreateSagaTenantAwareIT.java => CreateConsultantSagaTenantAwareIT.java} (95%) diff --git a/src/main/java/de/caritas/cob/userservice/api/admin/service/consultant/ConsultantAdminService.java b/src/main/java/de/caritas/cob/userservice/api/admin/service/consultant/ConsultantAdminService.java index cba6d5a3d..c7131ce98 100644 --- a/src/main/java/de/caritas/cob/userservice/api/admin/service/consultant/ConsultantAdminService.java +++ b/src/main/java/de/caritas/cob/userservice/api/admin/service/consultant/ConsultantAdminService.java @@ -13,7 +13,7 @@ import de.caritas.cob.userservice.api.adapters.web.dto.CreateConsultantDTO; import de.caritas.cob.userservice.api.adapters.web.dto.UpdateAdminConsultantDTO; import de.caritas.cob.userservice.api.adapters.web.dto.UpdateConsultantDTO; -import de.caritas.cob.userservice.api.admin.service.consultant.create.ConsultantCreateSaga; +import de.caritas.cob.userservice.api.admin.service.consultant.create.CreateConsultantSaga; import de.caritas.cob.userservice.api.admin.service.consultant.delete.ConsultantPreDeletionService; import de.caritas.cob.userservice.api.admin.service.consultant.update.ConsultantUpdateService; import de.caritas.cob.userservice.api.exception.httpresponses.DistributedTransactionException; @@ -38,7 +38,7 @@ public class ConsultantAdminService { private final @NonNull ConsultantRepository consultantRepository; - private final @NonNull ConsultantCreateSaga consultantCreateSaga; + private final @NonNull CreateConsultantSaga createConsultantSaga; private final @NonNull ConsultantUpdateService consultantUpdateService; private final @NonNull ConsultantPreDeletionService consultantPreDeletionService; @@ -89,7 +89,7 @@ private static String getDisplayNameFromUserMap(Map map) { */ public ConsultantAdminResponseDTO createNewConsultant(CreateConsultantDTO createConsultantDTO) throws DistributedTransactionException { - return this.consultantCreateSaga.createNewConsultant(createConsultantDTO); + return createConsultantSaga.createNewConsultant(createConsultantDTO); } /** diff --git a/src/main/java/de/caritas/cob/userservice/api/admin/service/consultant/create/ConsultantCreateSaga.java b/src/main/java/de/caritas/cob/userservice/api/admin/service/consultant/create/CreateConsultantSaga.java similarity index 93% rename from src/main/java/de/caritas/cob/userservice/api/admin/service/consultant/create/ConsultantCreateSaga.java rename to src/main/java/de/caritas/cob/userservice/api/admin/service/consultant/create/CreateConsultantSaga.java index 0afc3711d..e66ea92bc 100644 --- a/src/main/java/de/caritas/cob/userservice/api/admin/service/consultant/create/ConsultantCreateSaga.java +++ b/src/main/java/de/caritas/cob/userservice/api/admin/service/consultant/create/CreateConsultantSaga.java @@ -1,12 +1,12 @@ package de.caritas.cob.userservice.api.admin.service.consultant.create; +import static com.google.common.collect.Lists.newArrayList; import static de.caritas.cob.userservice.api.config.auth.UserRole.CONSULTANT; import static de.caritas.cob.userservice.api.config.auth.UserRole.GROUP_CHAT_CONSULTANT; import static de.caritas.cob.userservice.api.helper.json.JsonSerializationUtils.serializeToJsonString; import static org.apache.commons.lang3.BooleanUtils.isTrue; import static org.hibernate.validator.internal.util.CollectionHelper.asSet; -import com.google.common.collect.Lists; import com.neovisionaries.i18n.LanguageCode; import de.caritas.cob.userservice.api.adapters.keycloak.dto.KeycloakCreateUserResponseDTO; import de.caritas.cob.userservice.api.adapters.rocketchat.RocketChatService; @@ -50,8 +50,9 @@ @Service @RequiredArgsConstructor @Slf4j -public class ConsultantCreateSaga { +public class CreateConsultantSaga { + private static final String CREATE_CONSULTANT = "createConsultant"; private final @NonNull IdentityClient identityClient; private final @NonNull RocketChatService rocketChatService; private final @NonNull ConsultantService consultantService; @@ -71,13 +72,6 @@ public class ConsultantCreateSaga { private final @NonNull AppointmentService appointmentService; - /** - * Creates a new {@link Consultant} by {@link CreateConsultantDTO} in database, keycloak and - * rocket chat. - * - * @param createConsultantDTO the input used for creation - * @return the generated {@link Consultant} - */ private Consultant createNewConsultantWithoutAppointment( CreateConsultantDTO createConsultantDTO) { assertLicensesNotExceeded(); @@ -93,6 +87,14 @@ private Consultant createNewConsultantWithoutAppointment( return createNewConsultant(consultantCreationInput, roles); } + /** + * Creates a new {@link Consultant} by {@link CreateConsultantDTO} in database, keycloak and + * rocket chat, and optionally in the appointment service (calcom), provided the appointment + * feature is enabled. + * + * @param createConsultantDTO the input used for creation + * @return the generated {@link Consultant} + */ @Transactional public ConsultantAdminResponseDTO createNewConsultant(CreateConsultantDTO createConsultantDTO) throws DistributedTransactionException { @@ -121,15 +123,17 @@ private void createConsultantInAppointmentServiceOrRollback( this.rollbackCreateNewConsultant(newConsultant); throw new DistributedTransactionException( e, - new DistributedTransactionInfo( - "createNewConsultant", - Lists.newArrayList( - TransactionalStep.CREATE_ACCOUNT_IN_KEYCLOAK, - TransactionalStep.UPDATE_USER_PASSWORD_IN_KEYCLOAK, - TransactionalStep.UPDATE_USER_ROLES_IN_KEYCLOAK, - TransactionalStep.CREATE_ACCOUNT_IN_ROCKETCHAT, - TransactionalStep.CREATE_CONSULTANT_IN_MARIADB), - TransactionalStep.CREATE_ACCOUNT_IN_CALCOM_OR_APPOINTMENTSERVICE)); + DistributedTransactionInfo.builder() + .name("createNewConsultant") + .completedTransactionalOperations( + newArrayList( + TransactionalStep.CREATE_ACCOUNT_IN_KEYCLOAK, + TransactionalStep.UPDATE_USER_PASSWORD_IN_KEYCLOAK, + TransactionalStep.UPDATE_USER_ROLES_IN_KEYCLOAK, + TransactionalStep.CREATE_ACCOUNT_IN_ROCKETCHAT, + TransactionalStep.CREATE_CONSULTANT_IN_MARIADB)) + .failedStep(TransactionalStep.CREATE_ACCOUNT_IN_CALCOM_OR_APPOINTMENTSERVICE) + .build()); } } @@ -197,9 +201,9 @@ private void updateKeycloakPasswordOrRollback( throw new DistributedTransactionException( e, DistributedTransactionInfo.builder() + .name(CREATE_CONSULTANT) .completedTransactionalOperations( - Lists.newArrayList(TransactionalStep.CREATE_ACCOUNT_IN_KEYCLOAK)) - .name("createConsultant") + newArrayList(TransactionalStep.CREATE_ACCOUNT_IN_KEYCLOAK)) .failedStep(TransactionalStep.UPDATE_USER_PASSWORD_IN_KEYCLOAK) .build()); } @@ -219,10 +223,10 @@ private void updateKeyloakRolesOrRollback( e, DistributedTransactionInfo.builder() .completedTransactionalOperations( - Lists.newArrayList( + newArrayList( TransactionalStep.CREATE_ACCOUNT_IN_KEYCLOAK, TransactionalStep.UPDATE_USER_PASSWORD_IN_KEYCLOAK)) - .name("createConsultant") + .name(CREATE_CONSULTANT) .failedStep(TransactionalStep.UPDATE_USER_ROLES_IN_KEYCLOAK) .build()); } @@ -245,13 +249,13 @@ private Consultant createConsultantInMariaDBOrRollback( throw new DistributedTransactionException( e, DistributedTransactionInfo.builder() + .name(CREATE_CONSULTANT) .completedTransactionalOperations( - Lists.newArrayList( + newArrayList( TransactionalStep.CREATE_ACCOUNT_IN_KEYCLOAK, TransactionalStep.UPDATE_USER_PASSWORD_IN_KEYCLOAK, TransactionalStep.UPDATE_USER_ROLES_IN_KEYCLOAK, TransactionalStep.CREATE_ACCOUNT_IN_ROCKETCHAT)) - .name("createConsultant") .failedStep(TransactionalStep.CREATE_CONSULTANT_IN_MARIADB) .build()); } @@ -299,11 +303,11 @@ private String createRocketChatUserOrRollback( e, DistributedTransactionInfo.builder() .completedTransactionalOperations( - Lists.newArrayList( + newArrayList( TransactionalStep.CREATE_ACCOUNT_IN_KEYCLOAK, TransactionalStep.UPDATE_USER_PASSWORD_IN_KEYCLOAK, TransactionalStep.UPDATE_USER_ROLES_IN_KEYCLOAK)) - .name("createConsultant") + .name(CREATE_CONSULTANT) .failedStep(TransactionalStep.CREATE_ACCOUNT_IN_ROCKETCHAT) .build()); } diff --git a/src/main/java/de/caritas/cob/userservice/api/service/ConsultantImportService.java b/src/main/java/de/caritas/cob/userservice/api/service/ConsultantImportService.java index 431eb8f66..04abace2f 100644 --- a/src/main/java/de/caritas/cob/userservice/api/service/ConsultantImportService.java +++ b/src/main/java/de/caritas/cob/userservice/api/service/ConsultantImportService.java @@ -3,7 +3,7 @@ import static org.apache.commons.lang3.BooleanUtils.isTrue; import de.caritas.cob.userservice.api.adapters.web.dto.AgencyDTO; -import de.caritas.cob.userservice.api.admin.service.consultant.create.ConsultantCreateSaga; +import de.caritas.cob.userservice.api.admin.service.consultant.create.CreateConsultantSaga; import de.caritas.cob.userservice.api.admin.service.consultant.create.agencyrelation.ConsultantAgencyRelationCreatorService; import de.caritas.cob.userservice.api.exception.ImportException; import de.caritas.cob.userservice.api.exception.httpresponses.InternalServerErrorException; @@ -55,7 +55,7 @@ public class ConsultantImportService { private final @NonNull ConsultingTypeManager consultingTypeManager; private final @NonNull AgencyService agencyService; private final @NonNull UserHelper userHelper; - private final @NonNull ConsultantCreateSaga consultantCreateSaga; + private final @NonNull CreateConsultantSaga createConsultantSaga; private final @NonNull ConsultantAgencyRelationCreatorService consultantAgencyRelationCreatorService; @@ -226,7 +226,7 @@ public void startImport() { writeToImportLog(logMessage); if (importRecord.getConsultantId() == null) { - consultant = this.consultantCreateSaga.createNewConsultant(importRecord, roles); + consultant = this.createConsultantSaga.createNewConsultant(importRecord, roles); importRecord.setConsultantId(consultant.getId()); logMessage = "Keycloak-ID: " + consultant.getId(); diff --git a/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/ConsultantAdminServiceIT.java b/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/ConsultantAdminServiceIT.java index 8e5e40ec5..b11cfa990 100644 --- a/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/ConsultantAdminServiceIT.java +++ b/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/ConsultantAdminServiceIT.java @@ -15,7 +15,7 @@ import de.caritas.cob.userservice.api.adapters.web.dto.CreateConsultantDTO; import de.caritas.cob.userservice.api.adapters.web.dto.HalLink.MethodEnum; import de.caritas.cob.userservice.api.adapters.web.dto.UpdateAdminConsultantDTO; -import de.caritas.cob.userservice.api.admin.service.consultant.create.ConsultantCreateSaga; +import de.caritas.cob.userservice.api.admin.service.consultant.create.CreateConsultantSaga; import de.caritas.cob.userservice.api.admin.service.consultant.update.ConsultantUpdateService; import de.caritas.cob.userservice.api.exception.httpresponses.NoContentException; import de.caritas.cob.userservice.api.model.Consultant; @@ -53,7 +53,7 @@ public class ConsultantAdminServiceIT { @Autowired private ConsultantAgencyRepository consultantAgencyRepository; - @MockBean private ConsultantCreateSaga consultantCreateSaga; + @MockBean private CreateConsultantSaga createConsultantSaga; @MockBean private ConsultantUpdateService consultantUpdateService; @@ -121,13 +121,13 @@ public void findConsultantById_Should_throwNoContentException_When_consultantIdD public void createNewConsultant_Should_useCreatorServiceAndBuildConsultantAdminResponseDTO() { CreateConsultantDTO createConsultantDTO = new EasyRandom().nextObject(CreateConsultantDTO.class); - when(this.consultantCreateSaga.createNewConsultant(any())) + when(this.createConsultantSaga.createNewConsultant(any())) .thenReturn(new EasyRandom().nextObject(ConsultantAdminResponseDTO.class)); ConsultantAdminResponseDTO result = this.consultantAdminService.createNewConsultant(createConsultantDTO); - verify(this.consultantCreateSaga, times(1)).createNewConsultant(createConsultantDTO); + verify(this.createConsultantSaga, times(1)).createNewConsultant(createConsultantDTO); assertThat(result.getLinks(), notNullValue()); assertThat(result.getEmbedded(), notNullValue()); } diff --git a/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/ConsultantAdminServiceTest.java b/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/ConsultantAdminServiceTest.java index e76b505e9..1ac07aa16 100644 --- a/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/ConsultantAdminServiceTest.java +++ b/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/ConsultantAdminServiceTest.java @@ -7,7 +7,7 @@ import static org.powermock.api.mockito.PowerMockito.when; import de.caritas.cob.userservice.api.AccountManager; -import de.caritas.cob.userservice.api.admin.service.consultant.create.ConsultantCreateSaga; +import de.caritas.cob.userservice.api.admin.service.consultant.create.CreateConsultantSaga; import de.caritas.cob.userservice.api.admin.service.consultant.delete.ConsultantPreDeletionService; import de.caritas.cob.userservice.api.admin.service.consultant.update.ConsultantUpdateService; import de.caritas.cob.userservice.api.exception.httpresponses.NotFoundException; @@ -30,7 +30,7 @@ public class ConsultantAdminServiceTest { @Mock private ConsultantRepository consultantRepository; - @Mock private ConsultantCreateSaga consultantCreateSaga; + @Mock private CreateConsultantSaga createConsultantSaga; @Mock private ConsultantUpdateService consultantUpdateService; diff --git a/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/create/ConsultantCreateSagaIT.java b/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/create/CreateConsultantSagaIT.java similarity index 94% rename from src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/create/ConsultantCreateSagaIT.java rename to src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/create/CreateConsultantSagaIT.java index fe15f0403..3f5da1633 100644 --- a/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/create/ConsultantCreateSagaIT.java +++ b/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/create/CreateConsultantSagaIT.java @@ -52,14 +52,14 @@ @SpringBootTest(classes = UserServiceApplication.class) @TestPropertySource(properties = "spring.profiles.active=testing") @AutoConfigureTestDatabase(replace = Replace.ANY) -public class ConsultantCreateSagaIT { +public class CreateConsultantSagaIT { private static final String DUMMY_RC_ID = "rcUserId"; private static final String VALID_USERNAME = "validUsername"; private static final String VALID_EMAILADDRESS = "valid@emailaddress.de"; private static final long TENANT_ID = 1L; - @Autowired private ConsultantCreateSaga consultantCreateSaga; + @Autowired private CreateConsultantSaga createConsultantSaga; @MockBean private RocketChatService rocketChatService; @@ -75,7 +75,7 @@ public class ConsultantCreateSagaIT { @Before public void setup() { - ReflectionTestUtils.setField(consultantCreateSaga, "appointmentFeatureEnabled", false); + ReflectionTestUtils.setField(createConsultantSaga, "appointmentFeatureEnabled", false); } @Test @@ -91,7 +91,7 @@ public void createNewConsultant_Should_returnExpectedCreatedConsultant_When_inpu createConsultantDTO.setIsGroupchatConsultant(false); var consultantAdminResponseDTO = - this.consultantCreateSaga.createNewConsultant(createConsultantDTO); + this.createConsultantSaga.createNewConsultant(createConsultantDTO); ConsultantDTO consultant = consultantAdminResponseDTO.getEmbedded(); verify(keycloakService).updateRole(anyString(), eq(CONSULTANT.getValue())); @@ -121,7 +121,7 @@ public void createNewConsultant_Should_callRollback_When_RocketchatThrowsExcepti createConsultantDTO.setIsGroupchatConsultant(false); try { - this.consultantCreateSaga.createNewConsultant(createConsultantDTO); + this.createConsultantSaga.createNewConsultant(createConsultantDTO); fail("Exception should be thrown"); } catch (DistributedTransactionException ex) { assertThat( @@ -136,7 +136,7 @@ public void createNewConsultant_Should_callRollback_When_RocketchatThrowsExcepti @Test public void createNewConsultant_Should_callRollback_When_AppointmentServiceThrowsException() throws RocketChatLoginException { - ReflectionTestUtils.setField(consultantCreateSaga, "appointmentFeatureEnabled", true); + ReflectionTestUtils.setField(createConsultantSaga, "appointmentFeatureEnabled", true); doThrow(BadRequestException.class).when(appointmentService).createConsultant(any()); when(keycloakService.createKeycloakUser(any(), anyString(), any())) .thenReturn(easyRandom.nextObject(KeycloakCreateUserResponseDTO.class)); @@ -148,7 +148,7 @@ public void createNewConsultant_Should_callRollback_When_AppointmentServiceThrow createConsultantDTO.setIsGroupchatConsultant(false); try { - this.consultantCreateSaga.createNewConsultant(createConsultantDTO); + this.createConsultantSaga.createNewConsultant(createConsultantDTO); fail("Exception should be thrown"); } catch (DistributedTransactionException ex) { assertThat( @@ -173,7 +173,7 @@ public void createNewConsultant_Should_callRollback_When_KeycloakUpdatePasswordT createConsultantDTO.setIsGroupchatConsultant(false); try { - this.consultantCreateSaga.createNewConsultant(createConsultantDTO); + this.createConsultantSaga.createNewConsultant(createConsultantDTO); fail("Exception should be thrown"); } catch (DistributedTransactionException ex) { assertThat( @@ -196,7 +196,7 @@ public void createNewConsultant_Should_callRollback_When_KeycloakUpdateRoleThrow createConsultantDTO.setIsGroupchatConsultant(false); try { - this.consultantCreateSaga.createNewConsultant(createConsultantDTO); + this.createConsultantSaga.createNewConsultant(createConsultantDTO); fail("Exception should be thrown"); } catch (DistributedTransactionException ex) { assertThat( @@ -221,7 +221,7 @@ public void createNewConsultant_Should_callRollback_When_anyOfTheServicesThrowsE createConsultantDTO.setIsGroupchatConsultant(false); try { - this.consultantCreateSaga.createNewConsultant(createConsultantDTO); + this.createConsultantSaga.createNewConsultant(createConsultantDTO); fail("Exception should be thrown"); } catch (DistributedTransactionException ex) { assertThat( @@ -252,7 +252,7 @@ public void createNewConsultant_Should_callRollback_When_anyOfTheServicesThrowsE createConsultantDTO.setIsGroupchatConsultant(true); // when - var consultantAdminResponseDTO = consultantCreateSaga.createNewConsultant(createConsultantDTO); + var consultantAdminResponseDTO = createConsultantSaga.createNewConsultant(createConsultantDTO); // then verify(keycloakService, times(2)).updateRole(anyString(), anyString()); @@ -276,7 +276,7 @@ public void createNewConsultant_Should_callRollback_When_anyOfTheServicesThrowsE importRecord.setEmail(VALID_EMAILADDRESS); Consultant consultant = - this.consultantCreateSaga.createNewConsultant(importRecord, asSet(CONSULTANT.getValue())); + this.createConsultantSaga.createNewConsultant(importRecord, asSet(CONSULTANT.getValue())); assertThat(consultant, notNullValue()); assertThat(consultant.getId(), notNullValue()); @@ -305,7 +305,7 @@ public void createNewConsultant_Should_callRollback_When_anyOfTheServicesThrowsE createConsultantDTO.setUsername(VALID_USERNAME); createConsultantDTO.setEmail(VALID_EMAILADDRESS); - this.consultantCreateSaga.createNewConsultant(createConsultantDTO); + this.createConsultantSaga.createNewConsultant(createConsultantDTO); } @Test(expected = CustomValidationHttpStatusException.class) @@ -321,7 +321,7 @@ public void createNewConsultant_Should_callRollback_When_anyOfTheServicesThrowsE .thenReturn(keycloakResponse); CreateConsultantDTO createConsultantDTO = this.easyRandom.nextObject(CreateConsultantDTO.class); - this.consultantCreateSaga.createNewConsultant(createConsultantDTO); + this.createConsultantSaga.createNewConsultant(createConsultantDTO); } @Test @@ -330,7 +330,7 @@ public void createNewConsultant_Should_throwExpectedException_When_emailIsInvali createConsultantDTO.setEmail("invalid"); try { - this.consultantCreateSaga.createNewConsultant(createConsultantDTO); + this.createConsultantSaga.createNewConsultant(createConsultantDTO); fail("Exception should be thrown"); } catch (CustomValidationHttpStatusException e) { assertThat(e.getCustomHttpHeaders(), notNullValue()); diff --git a/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/create/ConsultantCreateSagaTenantAwareIT.java b/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/create/CreateConsultantSagaTenantAwareIT.java similarity index 95% rename from src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/create/ConsultantCreateSagaTenantAwareIT.java rename to src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/create/CreateConsultantSagaTenantAwareIT.java index 4774c7866..2624605f1 100644 --- a/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/create/ConsultantCreateSagaTenantAwareIT.java +++ b/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/create/CreateConsultantSagaTenantAwareIT.java @@ -49,14 +49,14 @@ @AutoConfigureTestDatabase(replace = Replace.ANY) @TestPropertySource(properties = "multitenancy.enabled=true") @Transactional -public class ConsultantCreateSagaTenantAwareIT { +public class CreateConsultantSagaTenantAwareIT { private static final String DUMMY_RC_ID = "rcUserId"; private static final String VALID_USERNAME = "validUsername"; private static final String VALID_EMAILADDRESS = "valid@emailaddress.de"; private static final long TENANT_ID = 1; - @Autowired private ConsultantCreateSaga consultantCreateSaga; + @Autowired private CreateConsultantSaga createConsultantSaga; @Autowired private ConsultantRepository consultantRepository; @@ -81,7 +81,7 @@ public void tearDown() { createConsultant("username1"); createConsultant("username2"); CreateConsultantDTO createConsultantDTO = this.easyRandom.nextObject(CreateConsultantDTO.class); - this.consultantCreateSaga.createNewConsultant(createConsultantDTO); + this.createConsultantSaga.createNewConsultant(createConsultantDTO); rollbackDBState(); } @@ -112,7 +112,7 @@ public void tearDown() { // when ConsultantAdminResponseDTO consultant = - consultantCreateSaga.createNewConsultant(createConsultantDTO); + createConsultantSaga.createNewConsultant(createConsultantDTO); // then verify(keycloakService, times(2)).updateRole(anyString(), anyString()); @@ -159,7 +159,7 @@ private void givenTenantApiCall() { var licensing = new Licensing(); licensing.setAllowedNumberOfUsers(2); dummyTenant.setLicensing(licensing); - ReflectionTestUtils.setField(consultantCreateSaga, "tenantAdminService", tenantAdminService); + ReflectionTestUtils.setField(createConsultantSaga, "tenantAdminService", tenantAdminService); when(tenantAdminService.getTenantById(TenantContext.getCurrentTenant())) .thenReturn(dummyTenant); }