From a5f0fbc093643e6d155f7805a73814381c494485 Mon Sep 17 00:00:00 2001 From: tkuzynow Date: Tue, 9 Jan 2024 17:02:18 +0100 Subject: [PATCH 1/7] feat: fix rollback policy for the case where consultant cannot be created in appointment service --- .../ApiResponseEntityExceptionHandler.java | 12 +++++- .../consultant/ConsultantAdminService.java | 30 +++++++++++++- .../service/consultant/TransactionalStep.java | 10 +++++ .../create/ConsultantCreatorService.java | 8 ++++ .../CustomValidationHttpStatusException.java | 6 +-- .../DistributedTransactionException.java | 40 +++++++++++++++++++ .../DistributedTransactionInfo.java | 19 +++++++++ .../api/facade/rollback/RollbackFacade.java | 20 ++++++++++ .../userservice/api/service/LogService.java | 8 +++- .../appointment/AppointmentService.java | 40 ++++++++++++------- .../delete/model/DeletionWorkflowError.java | 2 + .../service/DeleteUserAccountService.java | 2 +- .../keycloak/KeycloakServiceTest.java | 18 +++++---- .../admin/create/CreateAdminServiceIT.java | 4 +- .../admin/update/UpdateAdminServiceIT.java | 2 +- .../ConsultantAgencyAdminUserServiceTest.java | 2 +- ...ntAgencyDeletionValidationServiceTest.java | 4 +- .../create/ConsultantCreatorServiceIT.java | 4 +- .../ConsultantPreDeletionServiceTest.java | 2 +- .../update/ConsultantUpdateServiceBase.java | 4 +- .../UserAccountInputValidatorTest.java | 8 ++-- .../api/facade/CreateUserFacadeTest.java | 8 ++-- .../facade/rollback/RollbackFacadeTest.java | 15 +++++++ 23 files changed, 220 insertions(+), 48 deletions(-) create mode 100644 src/main/java/de/caritas/cob/userservice/api/admin/service/consultant/TransactionalStep.java create mode 100644 src/main/java/de/caritas/cob/userservice/api/exception/httpresponses/DistributedTransactionException.java create mode 100644 src/main/java/de/caritas/cob/userservice/api/exception/httpresponses/DistributedTransactionInfo.java diff --git a/src/main/java/de/caritas/cob/userservice/api/adapters/web/controller/interceptor/ApiResponseEntityExceptionHandler.java b/src/main/java/de/caritas/cob/userservice/api/adapters/web/controller/interceptor/ApiResponseEntityExceptionHandler.java index 97e29010a..40ead6482 100644 --- a/src/main/java/de/caritas/cob/userservice/api/adapters/web/controller/interceptor/ApiResponseEntityExceptionHandler.java +++ b/src/main/java/de/caritas/cob/userservice/api/adapters/web/controller/interceptor/ApiResponseEntityExceptionHandler.java @@ -6,6 +6,7 @@ import de.caritas.cob.userservice.api.exception.httpresponses.ConflictException; import de.caritas.cob.userservice.api.exception.httpresponses.CreateEnquiryMessageException; import de.caritas.cob.userservice.api.exception.httpresponses.CustomValidationHttpStatusException; +import de.caritas.cob.userservice.api.exception.httpresponses.DistributedTransactionException; import de.caritas.cob.userservice.api.exception.httpresponses.ForbiddenException; import de.caritas.cob.userservice.api.exception.httpresponses.InternalServerErrorException; import de.caritas.cob.userservice.api.exception.httpresponses.NoContentException; @@ -55,6 +56,14 @@ public ResponseEntity handleJPAConstraintViolationException( return handleExceptionInternal(ex, null, new HttpHeaders(), HttpStatus.CONFLICT, request); } + @ExceptionHandler({DistributedTransactionException.class}) + public ResponseEntity handleDistributedTransactionException( + final DistributedTransactionException ex, final WebRequest request) { + log.error("Distributed transaction failed to complete", ex); + return handleExceptionInternal( + ex, null, ex.getCustomHttpHeaders(), HttpStatus.FAILED_DEPENDENCY, request); + } + @ExceptionHandler({CreateEnquiryMessageException.class}) public ResponseEntity handleCreateEnquiryMessageException( final CreateEnquiryMessageException ex, final WebRequest request) { @@ -88,7 +97,8 @@ public ResponseEntity handleCustomBadRequest( final CustomValidationHttpStatusException ex, final WebRequest request) { ex.executeLogging(); - return handleExceptionInternal(ex, null, ex.getCustomHttpHeader(), ex.getHttpStatus(), request); + return handleExceptionInternal( + ex, null, ex.getCustomHttpHeaders(), ex.getHttpStatus(), request); } /** 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 027486d38..d555b073c 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 @@ -15,6 +15,8 @@ import de.caritas.cob.userservice.api.admin.service.consultant.create.ConsultantCreatorService; 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; @@ -23,10 +25,12 @@ 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 lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClientException; /** Service class for admin operations on {@link Consultant} objects. */ @Service @@ -69,14 +73,36 @@ public ConsultantAdminResponseDTO findConsultantById(String consultantId) { * @return the generated and persisted {@link Consultant} representation as {@link * ConsultantAdminResponseDTO} */ - public ConsultantAdminResponseDTO createNewConsultant(CreateConsultantDTO createConsultantDTO) { + 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(); - this.appointmentService.createConsultant(consultantAdminResponseDTO); + try { + this.appointmentService.createConsultant(consultantAdminResponseDTO); + } catch (RestClientException 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; } 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 new file mode 100644 index 000000000..93cbbaa43 --- /dev/null +++ b/src/main/java/de/caritas/cob/userservice/api/admin/service/consultant/TransactionalStep.java @@ -0,0 +1,10 @@ +package de.caritas.cob.userservice.api.admin.service.consultant; + +public enum TransactionalStep { + CREATE_ACCOUNT_IN_KEYCLOAK, + CREATE_CONSULTANT_IN_MARIADB, + + CREATE_ACCOUNT_IN_ROCKETCHAT, + + CREATE_ACCOUNT_IN_CALCOM_OR_APPOINTMENTSERVICE +} 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/ConsultantCreatorService.java index 1c5a71c38..13dba1c52 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/ConsultantCreatorService.java @@ -20,6 +20,7 @@ import de.caritas.cob.userservice.api.exception.httpresponses.InternalServerErrorException; 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; import de.caritas.cob.userservice.api.helper.UsernameTranscoder; @@ -52,6 +53,8 @@ public class ConsultantCreatorService { private final @NonNull UserAccountInputValidator userAccountInputValidator; private final @NonNull TenantAdminService tenantAdminService; + private final @NonNull RollbackFacade rollbackFacade; + @Value("${multitenancy.enabled}") private boolean multiTenancyEnabled; @@ -235,4 +238,9 @@ private void addGroupChatConsultantRole( roles.add(GROUP_CHAT_CONSULTANT.getValue()); } } + + public void rollbackCreateNewConsultant(Consultant newConsultant) { + log.info("Rollback creation of consultant with id {}", newConsultant.getId()); + rollbackFacade.rollbackConsultantAccount(newConsultant); + } } diff --git a/src/main/java/de/caritas/cob/userservice/api/exception/httpresponses/CustomValidationHttpStatusException.java b/src/main/java/de/caritas/cob/userservice/api/exception/httpresponses/CustomValidationHttpStatusException.java index 7f5a0a471..60c05acb0 100644 --- a/src/main/java/de/caritas/cob/userservice/api/exception/httpresponses/CustomValidationHttpStatusException.java +++ b/src/main/java/de/caritas/cob/userservice/api/exception/httpresponses/CustomValidationHttpStatusException.java @@ -10,7 +10,7 @@ @Getter public class CustomValidationHttpStatusException extends CustomHttpStatusException { - private final HttpHeaders customHttpHeader; + private final HttpHeaders customHttpHeaders; private final HttpStatus httpStatus; /** @@ -21,7 +21,7 @@ public class CustomValidationHttpStatusException extends CustomHttpStatusExcepti */ public CustomValidationHttpStatusException(HttpStatusExceptionReason httpStatusExceptionReason) { super(); - this.customHttpHeader = new CustomHttpHeader(httpStatusExceptionReason).buildHeader(); + this.customHttpHeaders = new CustomHttpHeader(httpStatusExceptionReason).buildHeader(); this.httpStatus = HttpStatus.BAD_REQUEST; } @@ -35,7 +35,7 @@ public CustomValidationHttpStatusException(HttpStatusExceptionReason httpStatusE public CustomValidationHttpStatusException( HttpStatusExceptionReason reason, HttpStatus httpStatus) { super(); - this.customHttpHeader = new CustomHttpHeader(reason).buildHeader(); + this.customHttpHeaders = new CustomHttpHeader(reason).buildHeader(); this.httpStatus = httpStatus; } } diff --git a/src/main/java/de/caritas/cob/userservice/api/exception/httpresponses/DistributedTransactionException.java b/src/main/java/de/caritas/cob/userservice/api/exception/httpresponses/DistributedTransactionException.java new file mode 100644 index 000000000..54732fef1 --- /dev/null +++ b/src/main/java/de/caritas/cob/userservice/api/exception/httpresponses/DistributedTransactionException.java @@ -0,0 +1,40 @@ +package de.caritas.cob.userservice.api.exception.httpresponses; + +import de.caritas.cob.userservice.api.service.LogService; +import org.springframework.http.HttpHeaders; + +public class DistributedTransactionException extends CustomHttpStatusException { + + private final HttpHeaders customHttpHeaders; + + public DistributedTransactionException( + Exception e, DistributedTransactionInfo distributedTransactionInfo) { + super( + getFormattedMessageWithDistributedTransactionInfo(distributedTransactionInfo), + e, + LogService::logError); + this.customHttpHeaders = + buildCustomHeaders( + "DISTRIBUTED_TRANSACTION_FAILED_ON_STEP_" + + distributedTransactionInfo.getFailedStep().name()); + } + + private HttpHeaders buildCustomHeaders(String message) { + HttpHeaders headers = new HttpHeaders(); + headers.add("X-Reason", message); + return headers; + } + + private static String getFormattedMessageWithDistributedTransactionInfo( + DistributedTransactionInfo distributedTransactionInfo) { + return String.format( + "Distributed transaction %s failed. Completed transactional operations: %s. Failed step: %s", + distributedTransactionInfo.getName(), + distributedTransactionInfo.getCompletedTransactionalOperations(), + distributedTransactionInfo.getFailedStep()); + } + + public HttpHeaders getCustomHttpHeaders() { + return customHttpHeaders; + } +} diff --git a/src/main/java/de/caritas/cob/userservice/api/exception/httpresponses/DistributedTransactionInfo.java b/src/main/java/de/caritas/cob/userservice/api/exception/httpresponses/DistributedTransactionInfo.java new file mode 100644 index 000000000..60fd1b1b9 --- /dev/null +++ b/src/main/java/de/caritas/cob/userservice/api/exception/httpresponses/DistributedTransactionInfo.java @@ -0,0 +1,19 @@ +package de.caritas.cob.userservice.api.exception.httpresponses; + +import de.caritas.cob.userservice.api.admin.service.consultant.TransactionalStep; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Data +@Builder +@AllArgsConstructor +public class DistributedTransactionInfo { + + String name; + List completedTransactionalOperations; + TransactionalStep failedStep; +} 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 a2031e09e..26eb0cb0d 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 @@ -2,12 +2,18 @@ import static java.util.Objects.nonNull; +import de.caritas.cob.userservice.api.model.Consultant; import de.caritas.cob.userservice.api.port.out.IdentityClient; import de.caritas.cob.userservice.api.service.UserAgencyService; import de.caritas.cob.userservice.api.service.session.SessionService; import de.caritas.cob.userservice.api.service.user.UserService; +import de.caritas.cob.userservice.api.workflow.delete.model.DeletionWorkflowError; +import de.caritas.cob.userservice.api.workflow.delete.service.DeleteUserAccountService; +import java.time.LocalDateTime; +import java.util.List; import lombok.NonNull; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; /* @@ -15,6 +21,7 @@ */ @Service @RequiredArgsConstructor +@Slf4j public class RollbackFacade { private final @NonNull IdentityClient identityClient; @@ -22,6 +29,19 @@ public class RollbackFacade { private final @NonNull SessionService sessionService; private final @NonNull UserService userService; + private final @NonNull DeleteUserAccountService deleteUserAccountService; + + public void rollbackConsultantAccount(Consultant consultant) { + log.info("Rollback consultant account: {}", consultant); + 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)); + } + } /** * Deletes the provided user in Keycloak, MariaDB and its related session or user-chat/agency * relations depending on the provided {@link RollbackUserAccountInformation}. diff --git a/src/main/java/de/caritas/cob/userservice/api/service/LogService.java b/src/main/java/de/caritas/cob/userservice/api/service/LogService.java index 7c665313a..7f6d3c6b6 100644 --- a/src/main/java/de/caritas/cob/userservice/api/service/LogService.java +++ b/src/main/java/de/caritas/cob/userservice/api/service/LogService.java @@ -106,12 +106,18 @@ public static void logInfo(Exception exception) { LOGGER.info(getStackTrace(exception)); } + public static void logError(Exception exception) { + LOGGER.error(getStackTrace(exception)); + } + /** * Logs an warning message. * * @param exception The exception */ public static void logWarn(Exception exception) { - LOGGER.warn(getStackTrace(exception)); + if (LOGGER.isWarnEnabled()) { + LOGGER.warn(getStackTrace(exception)); + } } } diff --git a/src/main/java/de/caritas/cob/userservice/api/service/appointment/AppointmentService.java b/src/main/java/de/caritas/cob/userservice/api/service/appointment/AppointmentService.java index 20be35694..6e7feabb5 100644 --- a/src/main/java/de/caritas/cob/userservice/api/service/appointment/AppointmentService.java +++ b/src/main/java/de/caritas/cob/userservice/api/service/appointment/AppointmentService.java @@ -1,5 +1,6 @@ package de.caritas.cob.userservice.api.service.appointment; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import de.caritas.cob.userservice.api.adapters.web.dto.ConsultantAdminResponseDTO; @@ -28,6 +29,7 @@ import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestClientException; /** Service class to communicate with the AppointmentService. */ @Component @@ -52,7 +54,8 @@ public class AppointmentService { @Value("${feature.appointment.enabled}") private boolean appointmentFeatureEnabled; - public void createConsultant(ConsultantAdminResponseDTO consultantAdminResponseDTO) { + public void createConsultant(ConsultantAdminResponseDTO consultantAdminResponseDTO) + throws RestClientException { if (!appointmentFeatureEnabled) { return; } @@ -62,29 +65,38 @@ public void createConsultant(ConsultantAdminResponseDTO consultantAdminResponseD ConsultantApi appointmentConsultantApi = this.appointmentConsultantServiceApiControllerFactory.createControllerApi(); addTechnicalUserHeaders(appointmentConsultantApi.getApiClient()); - try { - de.caritas.cob.userservice.appointmentservice.generated.web.model.ConsultantDTO consultant = - mapper.readValue( - mapper.writeValueAsString(consultantAdminResponseDTO.getEmbedded()), - de.caritas.cob.userservice.appointmentservice.generated.web.model.ConsultantDTO - .class); - appointmentConsultantApi.createConsultant(consultant); - } catch (Exception e) { - log.error(e.getMessage()); - } + de.caritas.cob.userservice.appointmentservice.generated.web.model.ConsultantDTO consultant = + getConsultantDTO(consultantAdminResponseDTO, mapper); + appointmentConsultantApi.createConsultant(consultant); + } + } + + private de.caritas.cob.userservice.appointmentservice.generated.web.model.ConsultantDTO + getConsultantDTO(ConsultantAdminResponseDTO consultantAdminResponseDTO, ObjectMapper mapper) { + de.caritas.cob.userservice.appointmentservice.generated.web.model.ConsultantDTO consultant = + null; + try { + consultant = + mapper.readValue( + mapper.writeValueAsString(consultantAdminResponseDTO.getEmbedded()), + de.caritas.cob.userservice.appointmentservice.generated.web.model.ConsultantDTO + .class); + } catch (JsonProcessingException e) { + throw new IllegalStateException(e); } + return consultant; } public void syncConsultantData(Consultant consultant) { - ConsultantAdminResponseDTO ConsultantAdminResponseDTO = new ConsultantAdminResponseDTO(); + ConsultantAdminResponseDTO consultantAdminResponseDTO = new ConsultantAdminResponseDTO(); ConsultantDTO consultantEmbeded = new ConsultantDTO(); consultantEmbeded.setId(consultant.getId()); consultantEmbeded.setFirstname(consultant.getFirstName()); consultantEmbeded.setLastname(consultant.getLastName()); consultantEmbeded.setEmail(consultant.getEmail()); consultantEmbeded.setAbsent(consultant.isAbsent()); - ConsultantAdminResponseDTO.setEmbedded(consultantEmbeded); - updateConsultant(ConsultantAdminResponseDTO); + consultantAdminResponseDTO.setEmbedded(consultantEmbeded); + updateConsultant(consultantAdminResponseDTO); } public void updateConsultant(ConsultantAdminResponseDTO consultantAdminResponseDTO) { diff --git a/src/main/java/de/caritas/cob/userservice/api/workflow/delete/model/DeletionWorkflowError.java b/src/main/java/de/caritas/cob/userservice/api/workflow/delete/model/DeletionWorkflowError.java index 32682a453..6573d0a4c 100644 --- a/src/main/java/de/caritas/cob/userservice/api/workflow/delete/model/DeletionWorkflowError.java +++ b/src/main/java/de/caritas/cob/userservice/api/workflow/delete/model/DeletionWorkflowError.java @@ -3,9 +3,11 @@ import java.time.LocalDateTime; import lombok.Builder; import lombok.Data; +import lombok.ToString; @Data @Builder +@ToString public class DeletionWorkflowError { private DeletionSourceType deletionSourceType; diff --git a/src/main/java/de/caritas/cob/userservice/api/workflow/delete/service/DeleteUserAccountService.java b/src/main/java/de/caritas/cob/userservice/api/workflow/delete/service/DeleteUserAccountService.java index 580c485be..79d099e94 100644 --- a/src/main/java/de/caritas/cob/userservice/api/workflow/delete/service/DeleteUserAccountService.java +++ b/src/main/java/de/caritas/cob/userservice/api/workflow/delete/service/DeleteUserAccountService.java @@ -83,7 +83,7 @@ private List deleteConsultantsAndCollectPossibleErrors() .collect(Collectors.toList()); } - private List performConsultantDeletion(Consultant consultant) { + public List performConsultantDeletion(Consultant consultant) { var deletionWorkflowDTO = new ConsultantDeletionWorkflowDTO(consultant, new ArrayList<>()); diff --git a/src/test/java/de/caritas/cob/userservice/api/adapters/keycloak/KeycloakServiceTest.java b/src/test/java/de/caritas/cob/userservice/api/adapters/keycloak/KeycloakServiceTest.java index 2afdd7a35..748206202 100644 --- a/src/test/java/de/caritas/cob/userservice/api/adapters/keycloak/KeycloakServiceTest.java +++ b/src/test/java/de/caritas/cob/userservice/api/adapters/keycloak/KeycloakServiceTest.java @@ -271,6 +271,8 @@ public void deleteEmailAddress_Should_useServicesCorrectly() { when(keycloakClient.getUsersResource()).thenReturn(usersResource); keycloakService.deleteEmailAddress(); + + Mockito.verify(userResource, times(1)).update(any()); } @Test @@ -403,8 +405,8 @@ public void createKeycloakUser_Should_createUserWithDefaultLocale() { try { this.keycloakService.createKeycloakUser(userDTO); } catch (CustomValidationHttpStatusException e) { - assertThat(e.getCustomHttpHeader(), notNullValue()); - assertThat(e.getCustomHttpHeader().get("X-Reason").get(0), is(EMAIL_NOT_AVAILABLE.name())); + assertThat(e.getCustomHttpHeaders(), notNullValue()); + assertThat(e.getCustomHttpHeaders().get("X-Reason").get(0), is(EMAIL_NOT_AVAILABLE.name())); } } @@ -424,8 +426,9 @@ public void createKeycloakUser_Should_createUserWithDefaultLocale() { try { this.keycloakService.createKeycloakUser(userDTO); } catch (CustomValidationHttpStatusException e) { - assertThat(e.getCustomHttpHeader(), notNullValue()); - assertThat(e.getCustomHttpHeader().get("X-Reason").get(0), is(USERNAME_NOT_AVAILABLE.name())); + assertThat(e.getCustomHttpHeaders(), notNullValue()); + assertThat( + e.getCustomHttpHeaders().get("X-Reason").get(0), is(USERNAME_NOT_AVAILABLE.name())); } } @@ -445,8 +448,9 @@ public void createKeycloakUser_Should_createUserWithDefaultLocale() { try { this.keycloakService.createKeycloakUser(userDTO); } catch (CustomValidationHttpStatusException e) { - assertThat(e.getCustomHttpHeader(), notNullValue()); - assertThat(e.getCustomHttpHeader().get("X-Reason").get(0), is(USERNAME_NOT_AVAILABLE.name())); + assertThat(e.getCustomHttpHeaders(), notNullValue()); + assertThat( + e.getCustomHttpHeaders().get("X-Reason").get(0), is(USERNAME_NOT_AVAILABLE.name())); } } @@ -723,7 +727,7 @@ public void updateUserData_Should_throwCustomException_When_emailIsChangedButNot this.keycloakService.updateUserData("userId", userDTO, "firstName", "lastName"); fail("Exception was not thrown"); } catch (CustomValidationHttpStatusException e) { - assertThat(e.getCustomHttpHeader().get("X-Reason").get(0), is(EMAIL_NOT_AVAILABLE.name())); + assertThat(e.getCustomHttpHeaders().get("X-Reason").get(0), is(EMAIL_NOT_AVAILABLE.name())); } } diff --git a/src/test/java/de/caritas/cob/userservice/api/admin/service/admin/create/CreateAdminServiceIT.java b/src/test/java/de/caritas/cob/userservice/api/admin/service/admin/create/CreateAdminServiceIT.java index 098ebeb8c..a115750f2 100644 --- a/src/test/java/de/caritas/cob/userservice/api/admin/service/admin/create/CreateAdminServiceIT.java +++ b/src/test/java/de/caritas/cob/userservice/api/admin/service/admin/create/CreateAdminServiceIT.java @@ -179,8 +179,8 @@ public void createNewAdminAgency_Should_throwExpectedException_When_emailIsInval // then } catch (CustomValidationHttpStatusException e) { - assertThat(e.getCustomHttpHeader()).isNotNull(); - assertThat(e.getCustomHttpHeader().get("X-Reason").get(0)).isEqualTo(EMAIL_NOT_VALID.name()); + assertThat(e.getCustomHttpHeaders()).isNotNull(); + assertThat(e.getCustomHttpHeaders().get("X-Reason").get(0)).isEqualTo(EMAIL_NOT_VALID.name()); } } } diff --git a/src/test/java/de/caritas/cob/userservice/api/admin/service/admin/update/UpdateAdminServiceIT.java b/src/test/java/de/caritas/cob/userservice/api/admin/service/admin/update/UpdateAdminServiceIT.java index 7846ae422..8af4d9cb4 100644 --- a/src/test/java/de/caritas/cob/userservice/api/admin/service/admin/update/UpdateAdminServiceIT.java +++ b/src/test/java/de/caritas/cob/userservice/api/admin/service/admin/update/UpdateAdminServiceIT.java @@ -74,7 +74,7 @@ public void updateAgencyAdmin_Should_throwCustomResponseException_When_newEmailI // then } catch (CustomValidationHttpStatusException e) { - assertThat(e.getCustomHttpHeader().get("X-Reason").get(0), is(EMAIL_NOT_VALID.name())); + assertThat(e.getCustomHttpHeaders().get("X-Reason").get(0), is(EMAIL_NOT_VALID.name())); } } } diff --git a/src/test/java/de/caritas/cob/userservice/api/admin/service/agency/ConsultantAgencyAdminUserServiceTest.java b/src/test/java/de/caritas/cob/userservice/api/admin/service/agency/ConsultantAgencyAdminUserServiceTest.java index 36ec737ce..4d1515753 100644 --- a/src/test/java/de/caritas/cob/userservice/api/admin/service/agency/ConsultantAgencyAdminUserServiceTest.java +++ b/src/test/java/de/caritas/cob/userservice/api/admin/service/agency/ConsultantAgencyAdminUserServiceTest.java @@ -141,7 +141,7 @@ public void removeConsultantsFromTeamSessionsByAgencyId_Should_changeTeamSession fail("Exception was not thrown"); } catch (CustomValidationHttpStatusException e) { assertThat( - requireNonNull(e.getCustomHttpHeader().get("X-Reason")).iterator().next(), + requireNonNull(e.getCustomHttpHeaders().get("X-Reason")).iterator().next(), is(CONSULTANT_AGENCY_RELATION_DOES_NOT_EXIST.name())); } } diff --git a/src/test/java/de/caritas/cob/userservice/api/admin/service/agency/ConsultantAgencyDeletionValidationServiceTest.java b/src/test/java/de/caritas/cob/userservice/api/admin/service/agency/ConsultantAgencyDeletionValidationServiceTest.java index 5a2adc9a5..2b601ae55 100644 --- a/src/test/java/de/caritas/cob/userservice/api/admin/service/agency/ConsultantAgencyDeletionValidationServiceTest.java +++ b/src/test/java/de/caritas/cob/userservice/api/admin/service/agency/ConsultantAgencyDeletionValidationServiceTest.java @@ -53,7 +53,7 @@ public class ConsultantAgencyDeletionValidationServiceTest { fail("Exception was not thrown"); } catch (CustomValidationHttpStatusException e) { assertThat( - requireNonNull(e.getCustomHttpHeader().get("X-Reason")).iterator().next(), + requireNonNull(e.getCustomHttpHeaders().get("X-Reason")).iterator().next(), is(CONSULTANT_IS_THE_LAST_OF_AGENCY_AND_AGENCY_IS_STILL_ACTIVE.name())); } } @@ -75,7 +75,7 @@ public class ConsultantAgencyDeletionValidationServiceTest { fail("Exception was not thrown"); } catch (CustomValidationHttpStatusException e) { assertThat( - requireNonNull(e.getCustomHttpHeader().get("X-Reason")).iterator().next(), + requireNonNull(e.getCustomHttpHeaders().get("X-Reason")).iterator().next(), is(CONSULTANT_IS_THE_LAST_OF_AGENCY_AND_AGENCY_HAS_OPEN_ENQUIRIES.name())); } } 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/ConsultantCreatorServiceIT.java index 938b89388..1da133c48 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/ConsultantCreatorServiceIT.java @@ -193,8 +193,8 @@ public void createNewConsultant_Should_throwExpectedException_When_emailIsInvali this.consultantCreatorService.createNewConsultant(createConsultantDTO); fail("Exception should be thrown"); } catch (CustomValidationHttpStatusException e) { - assertThat(e.getCustomHttpHeader(), notNullValue()); - assertThat(e.getCustomHttpHeader().get("X-Reason").get(0), is(EMAIL_NOT_VALID.name())); + assertThat(e.getCustomHttpHeaders(), notNullValue()); + assertThat(e.getCustomHttpHeaders().get("X-Reason").get(0), is(EMAIL_NOT_VALID.name())); } } } diff --git a/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/delete/ConsultantPreDeletionServiceTest.java b/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/delete/ConsultantPreDeletionServiceTest.java index 3f492b612..cbe90a169 100644 --- a/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/delete/ConsultantPreDeletionServiceTest.java +++ b/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/delete/ConsultantPreDeletionServiceTest.java @@ -55,7 +55,7 @@ public class ConsultantPreDeletionServiceTest { fail("Exception was not thrown"); } catch (CustomValidationHttpStatusException e) { assertThat( - requireNonNull(e.getCustomHttpHeader().get("X-Reason")).iterator().next(), + requireNonNull(e.getCustomHttpHeaders().get("X-Reason")).iterator().next(), is(CONSULTANT_HAS_ACTIVE_OR_ARCHIVE_SESSIONS.name())); } } diff --git a/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/update/ConsultantUpdateServiceBase.java b/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/update/ConsultantUpdateServiceBase.java index 10307f8e6..3e691e767 100644 --- a/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/update/ConsultantUpdateServiceBase.java +++ b/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/update/ConsultantUpdateServiceBase.java @@ -61,7 +61,7 @@ public void updateConsultant_Should_throwCustomResponseException_When_absenceIsI fail("Exception should be thrown"); } catch (CustomValidationHttpStatusException e) { assertThat( - e.getCustomHttpHeader().get("X-Reason").get(0), + e.getCustomHttpHeaders().get("X-Reason").get(0), is(MISSING_ABSENCE_MESSAGE_FOR_ABSENT_USER.name())); } } @@ -74,7 +74,7 @@ public void updateConsultant_Should_throwCustomResponseException_When_newEmailIs this.consultantUpdateService.updateConsultant(getValidConsultantId(), updateConsultantDTO); fail("Exception should be thrown"); } catch (CustomValidationHttpStatusException e) { - assertThat(e.getCustomHttpHeader().get("X-Reason").get(0), is(EMAIL_NOT_VALID.name())); + assertThat(e.getCustomHttpHeaders().get("X-Reason").get(0), is(EMAIL_NOT_VALID.name())); } } diff --git a/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/validation/UserAccountInputValidatorTest.java b/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/validation/UserAccountInputValidatorTest.java index 9d197f56e..b37976cab 100644 --- a/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/validation/UserAccountInputValidatorTest.java +++ b/src/test/java/de/caritas/cob/userservice/api/admin/service/consultant/validation/UserAccountInputValidatorTest.java @@ -68,9 +68,9 @@ public void validateCreateConsultantDTO_ShouldNot_throwException_When_consultant this.userAccountInputValidator.validateAbsence(createConsultantDTO); fail("Exception should be thrown"); } catch (CustomValidationHttpStatusException e) { - assertThat(e.getCustomHttpHeader(), notNullValue()); + assertThat(e.getCustomHttpHeaders(), notNullValue()); assertThat( - e.getCustomHttpHeader().get("X-Reason").get(0), + e.getCustomHttpHeaders().get("X-Reason").get(0), is(MISSING_ABSENCE_MESSAGE_FOR_ABSENT_USER.name())); } } @@ -106,8 +106,8 @@ public void validateEmailAddressShould_throwExpectedException_When_EmailIsInvali this.userAccountInputValidator.validateEmailAddress("invalid"); fail("Exception should be thrown"); } catch (CustomValidationHttpStatusException e) { - assertThat(e.getCustomHttpHeader(), notNullValue()); - assertThat(e.getCustomHttpHeader().get("X-Reason").get(0), is(EMAIL_NOT_VALID.name())); + assertThat(e.getCustomHttpHeaders(), notNullValue()); + assertThat(e.getCustomHttpHeaders().get("X-Reason").get(0), is(EMAIL_NOT_VALID.name())); } } } diff --git a/src/test/java/de/caritas/cob/userservice/api/facade/CreateUserFacadeTest.java b/src/test/java/de/caritas/cob/userservice/api/facade/CreateUserFacadeTest.java index 8fe8ed7ca..99e844996 100644 --- a/src/test/java/de/caritas/cob/userservice/api/facade/CreateUserFacadeTest.java +++ b/src/test/java/de/caritas/cob/userservice/api/facade/CreateUserFacadeTest.java @@ -78,9 +78,9 @@ public class CreateUserFacadeTest { try { this.createUserFacade.createUserAccountWithInitializedConsultingType(USER_DTO_SUCHT); } catch (CustomValidationHttpStatusException e) { - assertThat(e.getCustomHttpHeader(), notNullValue()); + assertThat(e.getCustomHttpHeaders(), notNullValue()); assertThat( - e.getCustomHttpHeader().get("X-Reason").get(0), + e.getCustomHttpHeaders().get("X-Reason").get(0), Matchers.is(USERNAME_NOT_AVAILABLE.name())); } } @@ -106,9 +106,9 @@ public class CreateUserFacadeTest { try { this.createUserFacade.createUserAccountWithInitializedConsultingType(USER_DTO_SUCHT); } catch (CustomValidationHttpStatusException e) { - assertThat(e.getCustomHttpHeader(), notNullValue()); + assertThat(e.getCustomHttpHeaders(), notNullValue()); assertThat( - e.getCustomHttpHeader().get("X-Reason").get(0), + e.getCustomHttpHeaders().get("X-Reason").get(0), Matchers.is(USERNAME_NOT_AVAILABLE.name())); assertThat(e.getHttpStatus(), is(HttpStatus.CONFLICT)); } diff --git a/src/test/java/de/caritas/cob/userservice/api/facade/rollback/RollbackFacadeTest.java b/src/test/java/de/caritas/cob/userservice/api/facade/rollback/RollbackFacadeTest.java index a7a30cb7c..63117d8c9 100644 --- a/src/test/java/de/caritas/cob/userservice/api/facade/rollback/RollbackFacadeTest.java +++ b/src/test/java/de/caritas/cob/userservice/api/facade/rollback/RollbackFacadeTest.java @@ -5,12 +5,14 @@ import static org.mockito.Mockito.verify; import de.caritas.cob.userservice.api.adapters.keycloak.KeycloakService; +import de.caritas.cob.userservice.api.model.Consultant; import de.caritas.cob.userservice.api.model.Session; import de.caritas.cob.userservice.api.model.User; import de.caritas.cob.userservice.api.model.UserAgency; import de.caritas.cob.userservice.api.service.UserAgencyService; import de.caritas.cob.userservice.api.service.session.SessionService; import de.caritas.cob.userservice.api.service.user.UserService; +import de.caritas.cob.userservice.api.workflow.delete.service.DeleteUserAccountService; import org.jeasy.random.EasyRandom; import org.junit.Test; import org.junit.runner.RunWith; @@ -27,6 +29,19 @@ public class RollbackFacadeTest { @Mock private SessionService sessionService; @Mock private UserService userService; + @Mock DeleteUserAccountService deleteUserAccountService; + + @Test + public void rollbackConsultantAccount_Should_Call_DeleteUserAccountService() { + // given + EasyRandom easyRandom = new EasyRandom(); + Consultant consultant = easyRandom.nextObject(Consultant.class); + // when + rollbackFacade.rollbackConsultantAccount(consultant); + // then + verify(deleteUserAccountService, times(1)).performConsultantDeletion(consultant); + } + @Test public void rollBackUserAccount_Should_DeleteSessionAndMonitoring_When_SessionIsGiven() { EasyRandom easyRandom = new EasyRandom(); From 79bcf7a5f6a725e02b298c2e0af2d53f53b7b10e Mon Sep 17 00:00:00 2001 From: tkuzynow Date: Tue, 9 Jan 2024 17:08:37 +0100 Subject: [PATCH 2/7] feat: fix rollback policy for the case where consultant cannot be created in appointment service --- .../de/caritas/cob/userservice/api/service/LogService.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/de/caritas/cob/userservice/api/service/LogService.java b/src/main/java/de/caritas/cob/userservice/api/service/LogService.java index 7f6d3c6b6..24f9f7848 100644 --- a/src/main/java/de/caritas/cob/userservice/api/service/LogService.java +++ b/src/main/java/de/caritas/cob/userservice/api/service/LogService.java @@ -116,8 +116,6 @@ public static void logError(Exception exception) { * @param exception The exception */ public static void logWarn(Exception exception) { - if (LOGGER.isWarnEnabled()) { - LOGGER.warn(getStackTrace(exception)); - } + LOGGER.warn(getStackTrace(exception)); } } From 30fd01279c15fc524965b412fb6cde9bc25a587c Mon Sep 17 00:00:00 2001 From: tkuzynow Date: Wed, 10 Jan 2024 12:20:50 +0100 Subject: [PATCH 3/7] feat: fix rollback policy for the case where consultant cannot be created in appointment service --- .../api/admin/service/consultant/ConsultantAdminService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d555b073c..2babc01ea 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 @@ -88,7 +88,7 @@ public ConsultantAdminResponseDTO createNewConsultant(CreateConsultantDTO create try { this.appointmentService.createConsultant(consultantAdminResponseDTO); - } catch (RestClientException e) { + } 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(), From f675f82258182ca059c9883701ad9fbeb0cd9b74 Mon Sep 17 00:00:00 2001 From: tkuzynow Date: Fri, 19 Jan 2024 16:27:29 +0100 Subject: [PATCH 4/7] fix: format violations --- .../api/admin/service/consultant/ConsultantAdminService.java | 1 - 1 file changed, 1 deletion(-) 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 2babc01ea..5a8d1ed70 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 @@ -30,7 +30,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import org.springframework.web.client.RestClientException; /** Service class for admin operations on {@link Consultant} objects. */ @Service From 229ef228cf4425d7d6e06d00385a46e60eef84d6 Mon Sep 17 00:00:00 2001 From: tkuzynow Date: Mon, 22 Jan 2024 15:41:20 +0100 Subject: [PATCH 5/7] fix: patch consultant logic rewritten to properly handle distributed transacations and inform appointmentservice about display name changes --- api/useradminservice.yaml | 2 + services/appointmentService.yaml | 22 +++ .../cob/userservice/api/AccountManager.java | 15 +- .../userservice/api/PatchConsultantSaga.java | 97 +++++++++++ .../PatchConsultantSagaRollbackHandler.java | 40 +++++ .../userservice/api/UserServiceMapper.java | 11 +- .../service/consultant/TransactionalStep.java | 10 +- .../appointment/AppointmentService.java | 20 +++ .../api/PatchConsultantSagaTest.java | 157 ++++++++++++++++++ 9 files changed, 363 insertions(+), 11 deletions(-) create mode 100644 src/main/java/de/caritas/cob/userservice/api/PatchConsultantSaga.java create mode 100644 src/main/java/de/caritas/cob/userservice/api/PatchConsultantSagaRollbackHandler.java create mode 100644 src/test/java/de/caritas/cob/userservice/api/PatchConsultantSagaTest.java diff --git a/api/useradminservice.yaml b/api/useradminservice.yaml index 45ca56d05..dfdf77405 100644 --- a/api/useradminservice.yaml +++ b/api/useradminservice.yaml @@ -1336,6 +1336,8 @@ components: type: integer tenantName: type: string + displayName: + type: string CreateAdminDTO: type: object diff --git a/services/appointmentService.yaml b/services/appointmentService.yaml index 1c39aaad8..5384c8b41 100644 --- a/services/appointmentService.yaml +++ b/services/appointmentService.yaml @@ -35,6 +35,28 @@ paths: schema: $ref: '#/components/schemas/CalcomUser' /consultants/{consultantId}: + patch: + tags: + - consultant + summary: Patch consultant to cal.com with consultant object + operationId: patchConsultant + parameters: + - name: consultantId + in: path + description: ID of onber consultant + required: true + schema: + type: string + requestBody: + description: consultant object that needs to be patched to cal.com + content: + application/json: + schema: + $ref: './../api/useradminservice.yaml#/components/schemas/ConsultantDTO' + required: true + responses: + '200': + description: successful operation put: tags: - consultant diff --git a/src/main/java/de/caritas/cob/userservice/api/AccountManager.java b/src/main/java/de/caritas/cob/userservice/api/AccountManager.java index 776e758ba..25d4b7887 100644 --- a/src/main/java/de/caritas/cob/userservice/api/AccountManager.java +++ b/src/main/java/de/caritas/cob/userservice/api/AccountManager.java @@ -15,6 +15,7 @@ import de.caritas.cob.userservice.api.port.out.SessionRepository; import de.caritas.cob.userservice.api.port.out.UserRepository; import de.caritas.cob.userservice.api.service.agency.AgencyService; +import de.caritas.cob.userservice.api.service.appointment.AppointmentService; import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -51,6 +52,10 @@ public class AccountManager implements AccountManaging { private final SessionRepository sessionRepository; + private final AppointmentService appointmentService; + + private final PatchConsultantSaga patchConsultantSaga; + @Override public Optional> findConsultant(String id) { var userMap = new HashMap(); @@ -173,15 +178,7 @@ private Map patchAdviceSeeker(User adviceSeeker, Map patchConsultant(Consultant consultant, Map patchMap) { var patchedConsultant = userServiceMapper.consultantOf(consultant, patchMap); - var savedConsultant = consultantRepository.save(patchedConsultant); - - userServiceMapper - .displayNameOf(patchMap) - .ifPresent( - displayName -> - messageClient.updateUser(savedConsultant.getRocketChatId(), displayName)); - - return userServiceMapper.mapOf(savedConsultant, patchMap); + return patchConsultantSaga.executeTransactional(patchedConsultant, patchMap); } private Map findByDbConsultant(Consultant dbConsultant) { diff --git a/src/main/java/de/caritas/cob/userservice/api/PatchConsultantSaga.java b/src/main/java/de/caritas/cob/userservice/api/PatchConsultantSaga.java new file mode 100644 index 000000000..6bab5d3bb --- /dev/null +++ b/src/main/java/de/caritas/cob/userservice/api/PatchConsultantSaga.java @@ -0,0 +1,97 @@ +package de.caritas.cob.userservice.api; + +import com.google.common.collect.Lists; +import de.caritas.cob.userservice.api.admin.service.consultant.TransactionalStep; +import de.caritas.cob.userservice.api.exception.httpresponses.DistributedTransactionException; +import de.caritas.cob.userservice.api.exception.httpresponses.DistributedTransactionInfo; +import de.caritas.cob.userservice.api.model.Consultant; +import de.caritas.cob.userservice.api.port.out.ConsultantRepository; +import de.caritas.cob.userservice.api.port.out.MessageClient; +import de.caritas.cob.userservice.api.service.appointment.AppointmentService; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Slf4j +public class PatchConsultantSaga { + + private final ConsultantRepository consultantRepository; + + @Setter private UserServiceMapper userServiceMapper; + + private final MessageClient messageClient; + + private final AppointmentService appointmentService; + + private final PatchConsultantSagaRollbackHandler patchConsultantSagaRollbackHandler; + + @Transactional + public Map executeTransactional( + Consultant patchedConsultant, Map patchMap) { + Consultant savedConsultant = saveConsultant(patchedConsultant); + userServiceMapper + .encodedDisplayNameOf(patchMap) + .ifPresent( + encodedUserName -> updateUserInRocketChatOrRollback(savedConsultant, encodedUserName)); + + userServiceMapper + .displayNameOf(patchMap) + .ifPresent( + displayName -> + patchConsultantInAppointmentServiceOrRollback(savedConsultant, displayName)); + return userServiceMapper.mapOf(savedConsultant, patchMap); + } + + private void patchConsultantInAppointmentServiceOrRollback( + Consultant savedConsultant, String displayName) { + + try { + appointmentService.patchConsultant(savedConsultant.getId(), displayName); + } catch (Exception e) { + log.error( + "Error while patching consultant in appointment service. Will rollback patchConsultantSaga.", + e); + patchConsultantSagaRollbackHandler.rollbackUpdateUserInRocketchat(savedConsultant); + // rollback on MariaDB will be handled automatically by spring due to @Transactional + throw new DistributedTransactionException( + e, + DistributedTransactionInfo.builder() + .completedTransactionalOperations( + Lists.newArrayList( + TransactionalStep.SAVE_CONSULTANT_IN_MARIADB, + TransactionalStep.UPDATE_ROCKET_CHAT_USER_DISPLAY_NAME)) + .name("patchConsultant") + .failedStep(TransactionalStep.PATCH_APPOINTMENT_SERVICE_CONSULTANT) + .build()); + } + } + + private void updateUserInRocketChatOrRollback(Consultant savedConsultant, String displayName) { + try { + if (savedConsultant.getRocketChatId() != null) { + messageClient.updateUser(savedConsultant.getRocketChatId(), displayName); + } + } catch (Exception e) { + log.error( + "Error while updating consultant in rocketchat. Will rollback patchConsultantSaga.", e); + // rollback will be handled automatically by spring due to @Transactional + throw new DistributedTransactionException( + e, + DistributedTransactionInfo.builder() + .completedTransactionalOperations( + Lists.newArrayList(TransactionalStep.SAVE_CONSULTANT_IN_MARIADB)) + .name("patchConsultant") + .failedStep(TransactionalStep.UPDATE_ROCKET_CHAT_USER_DISPLAY_NAME) + .build()); + } + } + + private Consultant saveConsultant(Consultant patchedConsultant) { + return consultantRepository.save(patchedConsultant); + } +} diff --git a/src/main/java/de/caritas/cob/userservice/api/PatchConsultantSagaRollbackHandler.java b/src/main/java/de/caritas/cob/userservice/api/PatchConsultantSagaRollbackHandler.java new file mode 100644 index 000000000..d56d86303 --- /dev/null +++ b/src/main/java/de/caritas/cob/userservice/api/PatchConsultantSagaRollbackHandler.java @@ -0,0 +1,40 @@ +package de.caritas.cob.userservice.api; + +import com.google.common.collect.Lists; +import de.caritas.cob.userservice.api.admin.service.consultant.TransactionalStep; +import de.caritas.cob.userservice.api.exception.httpresponses.DistributedTransactionException; +import de.caritas.cob.userservice.api.exception.httpresponses.DistributedTransactionInfo; +import de.caritas.cob.userservice.api.model.Consultant; +import de.caritas.cob.userservice.api.port.out.MessageClient; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Slf4j +public class PatchConsultantSagaRollbackHandler { + + private final MessageClient messageClient; + + public void rollbackUpdateUserInRocketchat(Consultant savedConsultant) { + try { + var originalDisplayName = + messageClient + .findUser(savedConsultant.getRocketChatId()) + .get() + .get("displayName") + .toString(); + messageClient.updateUser(savedConsultant.getRocketChatId(), originalDisplayName); + } catch (Exception e) { + log.error("Error while rolling back consultant", e); + throw new DistributedTransactionException( + e, + DistributedTransactionInfo.builder() + .completedTransactionalOperations(Lists.newArrayList()) + .name("patchConsultant") + .failedStep(TransactionalStep.ROLLBACK_UPDATE_ROCKET_CHAT_USER_DISPLAY_NAME) + .build()); + } + } +} diff --git a/src/main/java/de/caritas/cob/userservice/api/UserServiceMapper.java b/src/main/java/de/caritas/cob/userservice/api/UserServiceMapper.java index d4e8d50b8..0d03e7452 100644 --- a/src/main/java/de/caritas/cob/userservice/api/UserServiceMapper.java +++ b/src/main/java/de/caritas/cob/userservice/api/UserServiceMapper.java @@ -400,7 +400,7 @@ public Consultant consultantOf(Consultant consultant, Map patchM return consultant; } - public Optional displayNameOf(Map patchMap) { + public Optional encodedDisplayNameOf(Map patchMap) { if (patchMap.containsKey("displayName")) { var displayName = (String) patchMap.get("displayName"); var encodedDisplayName = usernameTranscoder.encodeUsername(displayName); @@ -411,6 +411,15 @@ public Optional displayNameOf(Map patchMap) { return Optional.empty(); } + public Optional displayNameOf(Map patchMap) { + if (patchMap.containsKey("displayName")) { + var displayName = (String) patchMap.get("displayName"); + return Optional.of(displayName); + } + + return Optional.empty(); + } + public User adviceSeekerOf(User adviceSeeker, Map patchMap) { if (patchMap.containsKey("email")) { adviceSeeker.setEmail((String) patchMap.get("email")); 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 93cbbaa43..2d8c19b4a 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 @@ -6,5 +6,13 @@ public enum TransactionalStep { CREATE_ACCOUNT_IN_ROCKETCHAT, - CREATE_ACCOUNT_IN_CALCOM_OR_APPOINTMENTSERVICE + CREATE_ACCOUNT_IN_CALCOM_OR_APPOINTMENTSERVICE, + + SAVE_CONSULTANT_IN_MARIADB, + ROLLBACK_CONSULTANT_IN_MARIADB, + UPDATE_ROCKET_CHAT_USER_DISPLAY_NAME, + + ROLLBACK_UPDATE_ROCKET_CHAT_USER_DISPLAY_NAME, + + PATCH_APPOINTMENT_SERVICE_CONSULTANT; } diff --git a/src/main/java/de/caritas/cob/userservice/api/service/appointment/AppointmentService.java b/src/main/java/de/caritas/cob/userservice/api/service/appointment/AppointmentService.java index 6e7feabb5..77b41a8de 100644 --- a/src/main/java/de/caritas/cob/userservice/api/service/appointment/AppointmentService.java +++ b/src/main/java/de/caritas/cob/userservice/api/service/appointment/AppointmentService.java @@ -222,4 +222,24 @@ private void addDefaultHeaders( tenantHeaderSupplier.addTenantHeader(headers); headers.forEach((key, value) -> apiClient.addDefaultHeader(key, value.iterator().next())); } + + public void patchConsultant(String consultantId, String displayName) { + if (!appointmentFeatureEnabled) { + return; + } + ConsultantApi appointmentConsultantApi = + this.appointmentConsultantServiceApiControllerFactory.createControllerApi(); + + if (consultantId != null && !consultantId.isEmpty()) { + addTechnicalUserHeaders(appointmentConsultantApi.getApiClient()); + try { + appointmentConsultantApi.patchConsultant( + consultantId, + new de.caritas.cob.userservice.appointmentservice.generated.web.model.ConsultantDTO() + .displayName(displayName)); + } catch (HttpClientErrorException ex) { + acceptDeletionIfConsultantNotFoundInAppointmentService(ex, consultantId); + } + } + } } diff --git a/src/test/java/de/caritas/cob/userservice/api/PatchConsultantSagaTest.java b/src/test/java/de/caritas/cob/userservice/api/PatchConsultantSagaTest.java new file mode 100644 index 000000000..48be93d62 --- /dev/null +++ b/src/test/java/de/caritas/cob/userservice/api/PatchConsultantSagaTest.java @@ -0,0 +1,157 @@ +package de.caritas.cob.userservice.api; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.collect.Maps; +import com.neovisionaries.i18n.LanguageCode; +import de.caritas.cob.userservice.api.admin.service.consultant.TransactionalStep; +import de.caritas.cob.userservice.api.exception.httpresponses.DistributedTransactionException; +import de.caritas.cob.userservice.api.helper.UsernameTranscoder; +import de.caritas.cob.userservice.api.model.Consultant; +import de.caritas.cob.userservice.api.port.out.ConsultantRepository; +import de.caritas.cob.userservice.api.port.out.MessageClient; +import de.caritas.cob.userservice.api.service.appointment.AppointmentService; +import java.util.Map; +import org.assertj.core.api.Fail; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class PatchConsultantSagaTest { + + private static final String CHANGED_DISPLAY_NAME = "new displayName"; + private static final String CONSULTANT_ID = "consultantId"; + private static final String ROCKETCHAT_ID = "rocketChatId"; + @InjectMocks PatchConsultantSaga patchConsultantSaga; + + @Mock ConsultantRepository consultantRepository; + + UserServiceMapper userServiceMapper = new UserServiceMapper(new UsernameTranscoder()); + + @Mock MessageClient messageClient; + + @Mock AppointmentService appointmentService; + + @Mock PatchConsultantSagaRollbackHandler patchConsultantSagaRollbackHandler; + + @BeforeEach + void setup() { + patchConsultantSaga.setUserServiceMapper(userServiceMapper); + } + + @Test + void + executeTransactionalOrRollback_Should_SaveConsultantInMariaDB_And_UpdateRocketChat_And_AppointmentService() { + // given + Map patchMap = givenPatchMapWithDisplayName(); + when(messageClient.updateUser(Mockito.eq(ROCKETCHAT_ID), Mockito.anyString())).thenReturn(true); + Consultant patchedConsultant = + Consultant.builder() + .rocketChatId(ROCKETCHAT_ID) + .id(CONSULTANT_ID) + .username("username") + .firstName("firstname") + .lastName("lastname") + .email("email") + .languageCode(LanguageCode.de) + .build(); + when(consultantRepository.save(patchedConsultant)).thenReturn(patchedConsultant); + + // when + patchConsultantSaga.executeTransactional(patchedConsultant, patchMap); + + // then + verify(consultantRepository).save(patchedConsultant); + verify(messageClient).updateUser(Mockito.eq(ROCKETCHAT_ID), Mockito.anyString()); + verify(appointmentService).patchConsultant(CONSULTANT_ID, CHANGED_DISPLAY_NAME); + } + + @Test + void + executeTransactionalOrRollback_Should_RollbackUpdateRocketChat_When_AppointmentService_ThrowsException() { + // given + Map patchMap = givenPatchMapWithDisplayName(); + when(messageClient.updateUser(Mockito.eq(ROCKETCHAT_ID), Mockito.anyString())).thenReturn(true); + Consultant patchedConsultant = + Consultant.builder() + .rocketChatId(ROCKETCHAT_ID) + .id(CONSULTANT_ID) + .username("username") + .firstName("firstname") + .lastName("lastname") + .email("email") + .languageCode(LanguageCode.de) + .build(); + when(consultantRepository.save(patchedConsultant)).thenReturn(patchedConsultant); + doThrow(new RuntimeException()) + .when(appointmentService) + .patchConsultant(Mockito.anyString(), Mockito.anyString()); + + try { + // when + patchConsultantSaga.executeTransactional(patchedConsultant, patchMap); + Fail.fail("Expected DistributedTransactionException"); + } catch (DistributedTransactionException ex) { + // then + verify(consultantRepository).save(patchedConsultant); + verify(messageClient).updateUser(Mockito.eq(ROCKETCHAT_ID), Mockito.anyString()); + verify(appointmentService).patchConsultant(CONSULTANT_ID, CHANGED_DISPLAY_NAME); + verify(patchConsultantSagaRollbackHandler).rollbackUpdateUserInRocketchat(patchedConsultant); + assertThat(ex.getMessage()) + .contains(TransactionalStep.PATCH_APPOINTMENT_SERVICE_CONSULTANT.name()); + } + } + + @Test + void + executeTransactionalOrRollback_Should_Not_CallAppoitmentService_When_RocketchatService_ThrowsException() { + // given + Map patchMap = givenPatchMapWithDisplayName(); + Consultant patchedConsultant = + Consultant.builder() + .rocketChatId(ROCKETCHAT_ID) + .id(CONSULTANT_ID) + .username("username") + .firstName("firstname") + .lastName("lastname") + .email("email") + .languageCode(LanguageCode.de) + .build(); + when(consultantRepository.save(patchedConsultant)).thenReturn(patchedConsultant); + doThrow(new RuntimeException()) + .when(messageClient) + .updateUser(Mockito.anyString(), Mockito.anyString()); + + try { + // when + patchConsultantSaga.executeTransactional(patchedConsultant, patchMap); + Fail.fail("Expected DistributedTransactionException"); + } catch (DistributedTransactionException ex) { + // then + verify(consultantRepository).save(patchedConsultant); + verify(messageClient).updateUser(Mockito.eq(ROCKETCHAT_ID), Mockito.anyString()); + verify(appointmentService, Mockito.never()) + .patchConsultant(CONSULTANT_ID, CHANGED_DISPLAY_NAME); + verify(patchConsultantSagaRollbackHandler, Mockito.never()) + .rollbackUpdateUserInRocketchat(patchedConsultant); + assertThat(ex.getMessage()) + .contains(TransactionalStep.UPDATE_ROCKET_CHAT_USER_DISPLAY_NAME.name()); + } + } + + @NotNull + private static Map givenPatchMapWithDisplayName() { + Map patchMap = Maps.newHashMap(); + patchMap.put("displayName", CHANGED_DISPLAY_NAME); + return patchMap; + } +} From e063cd6cee23bdea367373c0a0854cad6169d02e Mon Sep 17 00:00:00 2001 From: tkuzynow Date: Mon, 22 Jan 2024 17:33:12 +0100 Subject: [PATCH 6/7] fix: patch consultant logic rewritten to properly handle distributed transacations and inform appointmentservice about display name changes --- .../userservice/api/PatchConsultantSaga.java | 2 +- .../api/PatchConsultantSagaTest.java | 30 +++++++++++-------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/main/java/de/caritas/cob/userservice/api/PatchConsultantSaga.java b/src/main/java/de/caritas/cob/userservice/api/PatchConsultantSaga.java index 6bab5d3bb..d96f7c25c 100644 --- a/src/main/java/de/caritas/cob/userservice/api/PatchConsultantSaga.java +++ b/src/main/java/de/caritas/cob/userservice/api/PatchConsultantSaga.java @@ -22,7 +22,7 @@ public class PatchConsultantSaga { private final ConsultantRepository consultantRepository; - @Setter private UserServiceMapper userServiceMapper; + private final UserServiceMapper userServiceMapper; private final MessageClient messageClient; diff --git a/src/test/java/de/caritas/cob/userservice/api/PatchConsultantSagaTest.java b/src/test/java/de/caritas/cob/userservice/api/PatchConsultantSagaTest.java index 48be93d62..28e8a2937 100644 --- a/src/test/java/de/caritas/cob/userservice/api/PatchConsultantSagaTest.java +++ b/src/test/java/de/caritas/cob/userservice/api/PatchConsultantSagaTest.java @@ -1,6 +1,7 @@ package de.caritas.cob.userservice.api; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Fail.fail; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -9,15 +10,12 @@ import com.neovisionaries.i18n.LanguageCode; import de.caritas.cob.userservice.api.admin.service.consultant.TransactionalStep; import de.caritas.cob.userservice.api.exception.httpresponses.DistributedTransactionException; -import de.caritas.cob.userservice.api.helper.UsernameTranscoder; import de.caritas.cob.userservice.api.model.Consultant; import de.caritas.cob.userservice.api.port.out.ConsultantRepository; import de.caritas.cob.userservice.api.port.out.MessageClient; import de.caritas.cob.userservice.api.service.appointment.AppointmentService; import java.util.Map; -import org.assertj.core.api.Fail; import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -35,7 +33,8 @@ class PatchConsultantSagaTest { @Mock ConsultantRepository consultantRepository; - UserServiceMapper userServiceMapper = new UserServiceMapper(new UsernameTranscoder()); + @Mock + UserServiceMapper userServiceMapper; @Mock MessageClient messageClient; @@ -43,16 +42,12 @@ class PatchConsultantSagaTest { @Mock PatchConsultantSagaRollbackHandler patchConsultantSagaRollbackHandler; - @BeforeEach - void setup() { - patchConsultantSaga.setUserServiceMapper(userServiceMapper); - } - @Test void executeTransactionalOrRollback_Should_SaveConsultantInMariaDB_And_UpdateRocketChat_And_AppointmentService() { // given Map patchMap = givenPatchMapWithDisplayName(); + givenUserServiceMapper(); when(messageClient.updateUser(Mockito.eq(ROCKETCHAT_ID), Mockito.anyString())).thenReturn(true); Consultant patchedConsultant = Consultant.builder() @@ -80,6 +75,8 @@ void setup() { executeTransactionalOrRollback_Should_RollbackUpdateRocketChat_When_AppointmentService_ThrowsException() { // given Map patchMap = givenPatchMapWithDisplayName(); + givenUserServiceMapper(); + when(userServiceMapper.displayNameOf(patchMap)).thenReturn(java.util.Optional.of(CHANGED_DISPLAY_NAME)); when(messageClient.updateUser(Mockito.eq(ROCKETCHAT_ID), Mockito.anyString())).thenReturn(true); Consultant patchedConsultant = Consultant.builder() @@ -99,7 +96,7 @@ void setup() { try { // when patchConsultantSaga.executeTransactional(patchedConsultant, patchMap); - Fail.fail("Expected DistributedTransactionException"); + fail("Expected DistributedTransactionException"); } catch (DistributedTransactionException ex) { // then verify(consultantRepository).save(patchedConsultant); @@ -113,9 +110,11 @@ void setup() { @Test void - executeTransactionalOrRollback_Should_Not_CallAppoitmentService_When_RocketchatService_ThrowsException() { + executeTransactionalOrRollback_Should_Not_CallAppointmentService_When_RocketchatService_ThrowsException() { // given Map patchMap = givenPatchMapWithDisplayName(); + when(userServiceMapper.encodedDisplayNameOf(Mockito.anyMap())).thenReturn(java.util.Optional.of(CHANGED_DISPLAY_NAME)); + Consultant patchedConsultant = Consultant.builder() .rocketChatId(ROCKETCHAT_ID) @@ -134,7 +133,7 @@ void setup() { try { // when patchConsultantSaga.executeTransactional(patchedConsultant, patchMap); - Fail.fail("Expected DistributedTransactionException"); + fail("Expected DistributedTransactionException"); } catch (DistributedTransactionException ex) { // then verify(consultantRepository).save(patchedConsultant); @@ -149,9 +148,14 @@ void setup() { } @NotNull - private static Map givenPatchMapWithDisplayName() { + private Map givenPatchMapWithDisplayName() { Map patchMap = Maps.newHashMap(); patchMap.put("displayName", CHANGED_DISPLAY_NAME); return patchMap; } + + private void givenUserServiceMapper() { + when(userServiceMapper.displayNameOf(Mockito.anyMap())).thenReturn(java.util.Optional.of(CHANGED_DISPLAY_NAME)); + when(userServiceMapper.encodedDisplayNameOf(Mockito.anyMap())).thenReturn(java.util.Optional.of(CHANGED_DISPLAY_NAME)); + } } From d15adc2f0bf1aecd694dbeb4abcea19e0689bc7a Mon Sep 17 00:00:00 2001 From: tkuzynow Date: Mon, 22 Jan 2024 18:07:36 +0100 Subject: [PATCH 7/7] fix: patch consultant logic rewritten to properly handle distributed transacations and inform appointmentservice about display name changes --- .../userservice/api/PatchConsultantSaga.java | 1 - .../api/PatchConsultantSagaTest.java | 17 ++++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/main/java/de/caritas/cob/userservice/api/PatchConsultantSaga.java b/src/main/java/de/caritas/cob/userservice/api/PatchConsultantSaga.java index d96f7c25c..a5abd0c49 100644 --- a/src/main/java/de/caritas/cob/userservice/api/PatchConsultantSaga.java +++ b/src/main/java/de/caritas/cob/userservice/api/PatchConsultantSaga.java @@ -10,7 +10,6 @@ import de.caritas.cob.userservice.api.service.appointment.AppointmentService; import java.util.Map; import lombok.RequiredArgsConstructor; -import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/src/test/java/de/caritas/cob/userservice/api/PatchConsultantSagaTest.java b/src/test/java/de/caritas/cob/userservice/api/PatchConsultantSagaTest.java index 28e8a2937..d77865b04 100644 --- a/src/test/java/de/caritas/cob/userservice/api/PatchConsultantSagaTest.java +++ b/src/test/java/de/caritas/cob/userservice/api/PatchConsultantSagaTest.java @@ -33,8 +33,7 @@ class PatchConsultantSagaTest { @Mock ConsultantRepository consultantRepository; - @Mock - UserServiceMapper userServiceMapper; + @Mock UserServiceMapper userServiceMapper; @Mock MessageClient messageClient; @@ -76,7 +75,8 @@ class PatchConsultantSagaTest { // given Map patchMap = givenPatchMapWithDisplayName(); givenUserServiceMapper(); - when(userServiceMapper.displayNameOf(patchMap)).thenReturn(java.util.Optional.of(CHANGED_DISPLAY_NAME)); + when(userServiceMapper.displayNameOf(patchMap)) + .thenReturn(java.util.Optional.of(CHANGED_DISPLAY_NAME)); when(messageClient.updateUser(Mockito.eq(ROCKETCHAT_ID), Mockito.anyString())).thenReturn(true); Consultant patchedConsultant = Consultant.builder() @@ -113,7 +113,8 @@ class PatchConsultantSagaTest { executeTransactionalOrRollback_Should_Not_CallAppointmentService_When_RocketchatService_ThrowsException() { // given Map patchMap = givenPatchMapWithDisplayName(); - when(userServiceMapper.encodedDisplayNameOf(Mockito.anyMap())).thenReturn(java.util.Optional.of(CHANGED_DISPLAY_NAME)); + when(userServiceMapper.encodedDisplayNameOf(Mockito.anyMap())) + .thenReturn(java.util.Optional.of(CHANGED_DISPLAY_NAME)); Consultant patchedConsultant = Consultant.builder() @@ -148,14 +149,16 @@ class PatchConsultantSagaTest { } @NotNull - private Map givenPatchMapWithDisplayName() { + private Map givenPatchMapWithDisplayName() { Map patchMap = Maps.newHashMap(); patchMap.put("displayName", CHANGED_DISPLAY_NAME); return patchMap; } private void givenUserServiceMapper() { - when(userServiceMapper.displayNameOf(Mockito.anyMap())).thenReturn(java.util.Optional.of(CHANGED_DISPLAY_NAME)); - when(userServiceMapper.encodedDisplayNameOf(Mockito.anyMap())).thenReturn(java.util.Optional.of(CHANGED_DISPLAY_NAME)); + when(userServiceMapper.displayNameOf(Mockito.anyMap())) + .thenReturn(java.util.Optional.of(CHANGED_DISPLAY_NAME)); + when(userServiceMapper.encodedDisplayNameOf(Mockito.anyMap())) + .thenReturn(java.util.Optional.of(CHANGED_DISPLAY_NAME)); } }