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..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,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.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; -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 CreateConsultantSaga createConsultantSaga; 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 createConsultantSaga.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/CreateConsultantSaga.java similarity index 55% 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/CreateConsultantSaga.java index 13dba1c52..e66ea92bc 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/CreateConsultantSaga.java @@ -1,5 +1,6 @@ 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; @@ -9,17 +10,20 @@ 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,8 +50,9 @@ @Service @RequiredArgsConstructor @Slf4j -public class ConsultantCreatorService { +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; @@ -55,19 +62,18 @@ 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; - /** - * 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} - */ - public Consultant createNewConsultant(CreateConsultantDTO createConsultantDTO) { + private final @NonNull AppointmentService appointmentService; + + private Consultant createNewConsultantWithoutAppointment( + CreateConsultantDTO createConsultantDTO) { assertLicensesNotExceeded(); this.userAccountInputValidator.validateAbsence( new CreateConsultantDTOAbsenceInputAdapter(createConsultantDTO)); @@ -81,6 +87,56 @@ public Consultant createNewConsultant(CreateConsultantDTO createConsultantDTO) { 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 { + 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, + 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()); + } + } + private void validateTenantId(CreateConsultantDTO createConsultantDTO) { if (authenticatedUser.isTenantSuperAdmin()) { if (createConsultantDTO.getTenantId() == null) { @@ -122,14 +178,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() + .name(CREATE_CONSULTANT) + .completedTransactionalOperations( + newArrayList(TransactionalStep.CREATE_ACCOUNT_IN_KEYCLOAK)) + .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( + newArrayList( + TransactionalStep.CREATE_ACCOUNT_IN_KEYCLOAK, + TransactionalStep.UPDATE_USER_PASSWORD_IN_KEYCLOAK)) + .name(CREATE_CONSULTANT) + .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() + .name(CREATE_CONSULTANT) + .completedTransactionalOperations( + newArrayList( + TransactionalStep.CREATE_ACCOUNT_IN_KEYCLOAK, + TransactionalStep.UPDATE_USER_PASSWORD_IN_KEYCLOAK, + TransactionalStep.UPDATE_USER_ROLES_IN_KEYCLOAK, + TransactionalStep.CREATE_ACCOUNT_IN_ROCKETCHAT)) + .failedStep(TransactionalStep.CREATE_CONSULTANT_IN_MARIADB) + .build()); + } } private void assignCurrentTenantContext(CreateConsultantDTO createConsultantDTO) { @@ -159,17 +288,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( + newArrayList( + TransactionalStep.CREATE_ACCOUNT_IN_KEYCLOAK, + TransactionalStep.UPDATE_USER_PASSWORD_IN_KEYCLOAK, + TransactionalStep.UPDATE_USER_ROLES_IN_KEYCLOAK)) + .name(CREATE_CONSULTANT) + .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..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.ConsultantCreatorService; +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 ConsultantCreatorService consultantCreatorService; + 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.consultantCreatorService.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 06c16e2ac..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.ConsultantCreatorService; +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 ConsultantCreatorService consultantCreatorService; + @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.consultantCreatorService.createNewConsultant(any())) - .thenReturn(new EasyRandom().nextObject(Consultant.class)); + when(this.createConsultantSaga.createNewConsultant(any())) + .thenReturn(new EasyRandom().nextObject(ConsultantAdminResponseDTO.class)); ConsultantAdminResponseDTO result = this.consultantAdminService.createNewConsultant(createConsultantDTO); - verify(this.consultantCreatorService, 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 3e5a971e4..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.ConsultantCreatorService; +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 ConsultantCreatorService consultantCreatorService; + @Mock private CreateConsultantSaga createConsultantSaga; @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/CreateConsultantSagaIT.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/CreateConsultantSagaIT.java index 1da133c48..3f5da1633 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/CreateConsultantSagaIT.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 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 ConsultantCreatorService consultantCreatorService; + @Autowired private CreateConsultantSaga createConsultantSaga; @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(createConsultantSaga, "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.createConsultantSaga.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.createConsultantSaga.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(createConsultantSaga, "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.createConsultantSaga.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.createConsultantSaga.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.createConsultantSaga.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.createConsultantSaga.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 = createConsultantSaga.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.createConsultantSaga.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.createConsultantSaga.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.createConsultantSaga.createNewConsultant(createConsultantDTO); } @Test @@ -190,7 +330,7 @@ public void createNewConsultant_Should_throwExpectedException_When_emailIsInvali createConsultantDTO.setEmail("invalid"); try { - this.consultantCreatorService.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/ConsultantCreatorServiceTenantAwareIT.java b/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/create/CreateConsultantSagaTenantAwareIT.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/CreateConsultantSagaTenantAwareIT.java index 1bcce37cf..2624605f1 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/CreateConsultantSagaTenantAwareIT.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 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 ConsultantCreatorService consultantCreatorService; + @Autowired private CreateConsultantSaga createConsultantSaga; @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.createConsultantSaga.createNewConsultant(createConsultantDTO); rollbackDBState(); } @@ -110,15 +111,16 @@ public void tearDown() { createConsultantDTO.setTenantId(1L); // when - Consultant consultant = consultantCreatorService.createNewConsultant(createConsultantDTO); + ConsultantAdminResponseDTO consultant = + createConsultantSaga.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(createConsultantSaga, "tenantAdminService", tenantAdminService); when(tenantAdminService.getTenantById(TenantContext.getCurrentTenant())) .thenReturn(dummyTenant); }