From b36f181fa4f1f59c676b93661026fb7284fe1814 Mon Sep 17 00:00:00 2001 From: Agit Rubar Demir <61833677+agitrubard@users.noreply.github.com> Date: Sun, 4 Aug 2024 10:41:25 +0300 Subject: [PATCH] AYS-328 | Create Password Flow Has Been Created (#358) --- .../auth/controller/AysAuthController.java | 27 +- .../request/AysPasswordCreateRequest.java | 33 +++ ...est.java => AysPasswordForgotRequest.java} | 2 +- .../auth/service/AysUserPasswordService.java | 16 +- .../impl/AysUserPasswordServiceImpl.java | 88 ++++-- .../controller/AysAuthControllerTest.java | 78 +++++- .../auth/controller/AysAuthEndToEndTest.java | 108 +++++++- .../org/ays/auth/model/AysUserBuilder.java | 18 +- .../AysForgotPasswordRequestBuilder.java | 4 +- .../AysPasswordCreateRequestBuilder.java | 27 ++ .../impl/AysUserPasswordServiceImplTest.java | 259 +++++++++++++++++- 11 files changed, 608 insertions(+), 52 deletions(-) create mode 100644 src/main/java/org/ays/auth/model/request/AysPasswordCreateRequest.java rename src/main/java/org/ays/auth/model/request/{AysForgotPasswordRequest.java => AysPasswordForgotRequest.java} (89%) create mode 100644 src/test/java/org/ays/auth/model/request/AysPasswordCreateRequestBuilder.java diff --git a/src/main/java/org/ays/auth/controller/AysAuthController.java b/src/main/java/org/ays/auth/controller/AysAuthController.java index bc3ef939c..90d1294af 100644 --- a/src/main/java/org/ays/auth/controller/AysAuthController.java +++ b/src/main/java/org/ays/auth/controller/AysAuthController.java @@ -4,8 +4,9 @@ import lombok.RequiredArgsConstructor; import org.ays.auth.model.AysToken; import org.ays.auth.model.mapper.AysTokenToResponseMapper; -import org.ays.auth.model.request.AysForgotPasswordRequest; import org.ays.auth.model.request.AysLoginRequest; +import org.ays.auth.model.request.AysPasswordCreateRequest; +import org.ays.auth.model.request.AysPasswordForgotRequest; import org.ays.auth.model.request.AysTokenInvalidateRequest; import org.ays.auth.model.request.AysTokenRefreshRequest; import org.ays.auth.model.response.AysTokenResponse; @@ -44,7 +45,7 @@ class AysAuthController { * @return An AysResponse containing an AysTokenResponse object and the HTTP status code (200 OK). */ @PostMapping("/token") - public AysResponse landingAuthenticate(@RequestBody @Valid AysLoginRequest loginRequest) { + public AysResponse authenticate(@RequestBody @Valid AysLoginRequest loginRequest) { final AysToken token = authService.authenticate(loginRequest); final AysTokenResponse tokenResponse = tokenToTokenResponseMapper.map(token); return AysResponse.successOf(tokenResponse); @@ -86,7 +87,7 @@ public AysResponse invalidateTokens(@RequestBody @Valid AysTokenInvalidate * @return An AysResponse indicating the success of the password create request. */ @PostMapping("/password/forgot") - public AysResponse forgotPassword(@RequestBody @Valid AysForgotPasswordRequest forgotPasswordRequest) { + public AysResponse forgotPassword(@RequestBody @Valid AysPasswordForgotRequest forgotPasswordRequest) { userPasswordService.forgotPassword(forgotPasswordRequest); return AysResponse.SUCCESS; } @@ -108,4 +109,24 @@ public AysResponse checkPasswordChangingValidity(@PathVariable @UUID Strin return AysResponse.SUCCESS; } + + /** + * Handles the request to create a new password for a user. + *

+ * This endpoint processes a request to set a new password for a user identified + * by the provided password ID. It validates the request and updates the user's + * password if the request meets the required criteria. + * + * @param id The unique identifier of the user for whom the password is being created. + * @param createRequest The request body containing the new password details. + * @return A response indicating the success of the operation. + */ + @PostMapping("/password/{id}") + public AysResponse forgotPassword(@PathVariable @UUID String id, + @RequestBody @Valid AysPasswordCreateRequest createRequest) { + + userPasswordService.createPassword(id, createRequest); + return AysResponse.SUCCESS; + } + } diff --git a/src/main/java/org/ays/auth/model/request/AysPasswordCreateRequest.java b/src/main/java/org/ays/auth/model/request/AysPasswordCreateRequest.java new file mode 100644 index 000000000..d97d3df38 --- /dev/null +++ b/src/main/java/org/ays/auth/model/request/AysPasswordCreateRequest.java @@ -0,0 +1,33 @@ +package org.ays.auth.model.request; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.lang3.StringUtils; + +@Getter +@Setter +public class AysPasswordCreateRequest { + + @NotBlank + private String password; + + @NotBlank + private String passwordRepeat; + + + @JsonIgnore + @AssertTrue(message = "passwords must be equal") + @SuppressWarnings("This method is unused by the application directly but Spring is using it in the background.") + private boolean isPasswordsEqual() { + + if (StringUtils.isEmpty(this.password) || StringUtils.isEmpty(this.passwordRepeat)) { + return true; + } + + return this.password.equals(this.passwordRepeat); + } + +} diff --git a/src/main/java/org/ays/auth/model/request/AysForgotPasswordRequest.java b/src/main/java/org/ays/auth/model/request/AysPasswordForgotRequest.java similarity index 89% rename from src/main/java/org/ays/auth/model/request/AysForgotPasswordRequest.java rename to src/main/java/org/ays/auth/model/request/AysPasswordForgotRequest.java index c396442e2..e687b08e0 100644 --- a/src/main/java/org/ays/auth/model/request/AysForgotPasswordRequest.java +++ b/src/main/java/org/ays/auth/model/request/AysPasswordForgotRequest.java @@ -8,7 +8,7 @@ @Getter @Setter -public class AysForgotPasswordRequest { +public class AysPasswordForgotRequest { @EmailAddress @NotBlank diff --git a/src/main/java/org/ays/auth/service/AysUserPasswordService.java b/src/main/java/org/ays/auth/service/AysUserPasswordService.java index a67fe726c..65c8f0bf7 100644 --- a/src/main/java/org/ays/auth/service/AysUserPasswordService.java +++ b/src/main/java/org/ays/auth/service/AysUserPasswordService.java @@ -1,6 +1,7 @@ package org.ays.auth.service; -import org.ays.auth.model.request.AysForgotPasswordRequest; +import org.ays.auth.model.request.AysPasswordCreateRequest; +import org.ays.auth.model.request.AysPasswordForgotRequest; import org.ays.auth.util.exception.AysEmailAddressNotValidException; import org.ays.auth.util.exception.AysUserPasswordCannotChangedException; import org.ays.auth.util.exception.AysUserPasswordDoesNotExistException; @@ -23,7 +24,7 @@ public interface AysUserPasswordService { * @param forgotPasswordRequest the request containing the user's email address. * @throws AysEmailAddressNotValidException if no user is found with the provided email address. */ - void forgotPassword(AysForgotPasswordRequest forgotPasswordRequest); + void forgotPassword(AysPasswordForgotRequest forgotPasswordRequest); /** * Checks the validity of changing the user's password. @@ -38,4 +39,15 @@ public interface AysUserPasswordService { */ void checkPasswordChangingValidity(String passwordId); + /** + * Creates a new password for the user. + *

+ * This method updates the user's password with the new one provided in the request. + * It validates the password change request before updating the password. + * + * @param passwordId The unique identifier of the password to be created. + * @param createRequest The request containing the new password details. + */ + void createPassword(String passwordId, AysPasswordCreateRequest createRequest); + } diff --git a/src/main/java/org/ays/auth/service/impl/AysUserPasswordServiceImpl.java b/src/main/java/org/ays/auth/service/impl/AysUserPasswordServiceImpl.java index c1bfc6ac2..adf91a9a3 100644 --- a/src/main/java/org/ays/auth/service/impl/AysUserPasswordServiceImpl.java +++ b/src/main/java/org/ays/auth/service/impl/AysUserPasswordServiceImpl.java @@ -2,7 +2,8 @@ import lombok.RequiredArgsConstructor; import org.ays.auth.model.AysUser; -import org.ays.auth.model.request.AysForgotPasswordRequest; +import org.ays.auth.model.request.AysPasswordCreateRequest; +import org.ays.auth.model.request.AysPasswordForgotRequest; import org.ays.auth.port.AysUserReadPort; import org.ays.auth.port.AysUserSavePort; import org.ays.auth.service.AysUserMailService; @@ -11,6 +12,7 @@ import org.ays.auth.util.exception.AysUserPasswordCannotChangedException; import org.ays.auth.util.exception.AysUserPasswordDoesNotExistException; import org.ays.common.util.AysRandomUtil; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -33,6 +35,7 @@ class AysUserPasswordServiceImpl implements AysUserPasswordService { private final AysUserReadPort userReadPort; private final AysUserSavePort userSavePort; private final AysUserMailService userMailService; + private final PasswordEncoder passwordEncoder; /** @@ -48,21 +51,17 @@ class AysUserPasswordServiceImpl implements AysUserPasswordService { * @throws AysEmailAddressNotValidException if no user is found with the provided email address. */ @Override - public void forgotPassword(final AysForgotPasswordRequest forgotPasswordRequest) { + public void forgotPassword(final AysPasswordForgotRequest forgotPasswordRequest) { final String emailAddress = forgotPasswordRequest.getEmailAddress(); final AysUser user = userReadPort.findByEmailAddress(emailAddress) .orElseThrow(() -> new AysEmailAddressNotValidException(emailAddress)); - if (user.getPassword() == null) { - final AysUser.Password password = AysUser.Password.builder() - .value(AysRandomUtil.generateUUID()) - .forgotAt(LocalDateTime.now()) - .build(); - user.setPassword(password); - } else { - user.getPassword().setForgotAt(LocalDateTime.now()); - } + final AysUser.Password password = AysUser.Password.builder() + .value(AysRandomUtil.generateUUID()) + .forgotAt(LocalDateTime.now()) + .build(); + user.setPassword(password); final AysUser savedUser = userSavePort.save(user); userMailService.sendPasswordCreateEmail(savedUser); @@ -91,11 +90,45 @@ public void checkPasswordChangingValidity(final String passwordId) { } + /** + * Creates a new password for a user identified by the given password ID. + *

+ * This method updates the user's password with the new password provided in the request. + * It first verifies if the password change request is valid based on the time elapsed since + * the password change request was initiated. If the request is valid, it updates the password + * and saves the changes. + * + * @param passwordId The unique identifier of the user whose password is being updated. + * @param createRequest The request object containing the new password details. + * @throws AysUserPasswordDoesNotExistException if no user is found with the provided password ID. + * @throws AysUserPasswordCannotChangedException if the password change request is invalid due to + * the elapsed time or other conditions that prevent the password from being changed. + */ + @Override + public void createPassword(final String passwordId, + final AysPasswordCreateRequest createRequest) { + + final AysUser user = userReadPort.findByPasswordId(passwordId) + .orElseThrow(() -> new AysUserPasswordDoesNotExistException(passwordId)); + + this.checkChangingValidity(user.getPassword()); + + AysUser.Password password = AysUser.Password.builder() + .value(passwordEncoder.encode(createRequest.getPassword())) + .build(); + user.setPassword(password); + + userSavePort.save(user); + } + + /** * Checks the validity of changing the password. *

- * This method verifies if the password change request is valid by checking if the password change request - * was initiated within the allowable time frame. It throws an exception if the password cannot be changed. + * This method verifies if the password change request is valid based on the time elapsed since the + * password change request was initiated or the password was created. It distinguishes between + * a newly created password and an existing one to determine if the change request is within + * the allowable time frame. It throws an exception if the password cannot be changed. * * @param password The AysUser.Password object representing the user's password. * @throws AysUserPasswordCannotChangedException if the password cannot be changed due to invalid conditions. @@ -105,15 +138,38 @@ private void checkChangingValidity(final AysUser.Password password) { Optional forgotAt = Optional .ofNullable(password.getForgotAt()); + boolean isFirstCreation = forgotAt.isEmpty() && password.getUpdatedAt() == null; + if (isFirstCreation) { + this.checkExpiration(password.getId(), password.getCreatedAt()); + return; + } + if (forgotAt.isEmpty()) { throw new AysUserPasswordCannotChangedException(password.getId()); } - boolean isExpired = LocalDateTime.now().minusHours(2).isBefore(forgotAt.get()); + this.checkExpiration(password.getId(), forgotAt.get()); + } + + /** + * Checks if the password change request has expired. + *

+ * This method determines if the time elapsed since the provided timestamp exceeds the allowable time frame + * for changing the password. It throws an exception if the password change request is deemed expired. + * + * @param id The unique identifier of the password being validated. + * @param date The timestamp used to check if the password change request has expired. + * @throws AysUserPasswordCannotChangedException if the time elapsed since the timestamp exceeds the allowable time frame. + */ + private void checkExpiration(final String id, + final LocalDateTime date) { + + boolean isExpired = LocalDateTime.now() + .minusHours(2) + .isBefore(date); if (!isExpired) { - throw new AysUserPasswordCannotChangedException(password.getId()); + throw new AysUserPasswordCannotChangedException(id); } - } } diff --git a/src/test/java/org/ays/auth/controller/AysAuthControllerTest.java b/src/test/java/org/ays/auth/controller/AysAuthControllerTest.java index 05c289b5e..c1d09cd6a 100644 --- a/src/test/java/org/ays/auth/controller/AysAuthControllerTest.java +++ b/src/test/java/org/ays/auth/controller/AysAuthControllerTest.java @@ -3,10 +3,12 @@ import org.ays.AysRestControllerTest; import org.ays.auth.model.enums.AysSourcePage; import org.ays.auth.model.mapper.AysTokenToResponseMapper; -import org.ays.auth.model.request.AysForgotPasswordRequest; import org.ays.auth.model.request.AysForgotPasswordRequestBuilder; import org.ays.auth.model.request.AysLoginRequest; import org.ays.auth.model.request.AysLoginRequestBuilder; +import org.ays.auth.model.request.AysPasswordCreateRequest; +import org.ays.auth.model.request.AysPasswordCreateRequestBuilder; +import org.ays.auth.model.request.AysPasswordForgotRequest; import org.ays.auth.model.request.AysTokenInvalidateRequest; import org.ays.auth.model.request.AysTokenInvalidateRequestBuilder; import org.ays.auth.model.request.AysTokenRefreshRequest; @@ -190,14 +192,14 @@ void givenValidAysTokenInvalidateRequest_whenTokensInvalidated_thenReturnSuccess @Test void givenValidForgotPasswordRequest_whenSendPasswordCreateMail_thenReturnSuccessResponse() throws Exception { // Given - AysForgotPasswordRequest mockForgotPasswordRequest = new AysForgotPasswordRequestBuilder() + AysPasswordForgotRequest mockForgotPasswordRequest = new AysForgotPasswordRequestBuilder() .withEmailAddress(AysValidTestData.User.EMAIL_ADDRESS) .build(); // When Mockito.doNothing() .when(userPasswordService) - .forgotPassword(Mockito.any(AysForgotPasswordRequest.class)); + .forgotPassword(Mockito.any(AysPasswordForgotRequest.class)); // Then String endpoint = BASE_PATH.concat("/password/forgot"); @@ -214,7 +216,7 @@ void givenValidForgotPasswordRequest_whenSendPasswordCreateMail_thenReturnSucces // Verify Mockito.verify(userPasswordService, Mockito.times(1)) - .forgotPassword(Mockito.any(AysForgotPasswordRequest.class)); + .forgotPassword(Mockito.any(AysPasswordForgotRequest.class)); } @ParameterizedTest @@ -228,7 +230,7 @@ void givenValidForgotPasswordRequest_whenSendPasswordCreateMail_thenReturnSucces void givenForgotPasswordRequestWithInvalidEmailAddress_whenEmailDoesNotValid_thenReturnValidationError(String mockEmailAddress) throws Exception { // Given - AysForgotPasswordRequest mockForgotPasswordRequest = new AysForgotPasswordRequestBuilder() + AysPasswordForgotRequest mockForgotPasswordRequest = new AysForgotPasswordRequestBuilder() .withEmailAddress(mockEmailAddress) .build(); @@ -247,14 +249,14 @@ void givenForgotPasswordRequestWithInvalidEmailAddress_whenEmailDoesNotValid_the // Verify Mockito.verify(userPasswordService, Mockito.never()) - .forgotPassword(Mockito.any(AysForgotPasswordRequest.class)); + .forgotPassword(Mockito.any(AysPasswordForgotRequest.class)); } @Test void givenForgotPasswordRequestWithoutEmailAddress_whenEmailIsNull_thenReturnValidationError() throws Exception { // Given - AysForgotPasswordRequest mockForgotPasswordRequest = new AysForgotPasswordRequestBuilder() + AysPasswordForgotRequest mockForgotPasswordRequest = new AysForgotPasswordRequestBuilder() .withEmailAddress(null) .build(); @@ -273,7 +275,7 @@ void givenForgotPasswordRequestWithoutEmailAddress_whenEmailIsNull_thenReturnVal // Verify Mockito.verify(userPasswordService, Mockito.never()) - .forgotPassword(Mockito.any(AysForgotPasswordRequest.class)); + .forgotPassword(Mockito.any(AysPasswordForgotRequest.class)); } @@ -330,4 +332,64 @@ void givenId_whenIdDoesNotValid_thenReturnValidationError(String invalidId) thro .checkPasswordChangingValidity(Mockito.anyString()); } + + @Test + void givenValidPasswordCreateRequest_whenPasswordCreated_thenReturnSuccessResponse() throws Exception { + // Given + String mockId = "1fa43c75-7a7a-4041-8cef-03be8429dd30"; + AysPasswordCreateRequest mockPasswordCreateRequest = new AysPasswordCreateRequestBuilder() + .withValidValues() + .build(); + + // When + Mockito.doNothing() + .when(userPasswordService) + .createPassword(Mockito.anyString(), Mockito.any(AysPasswordCreateRequest.class)); + + // Then + String endpoint = BASE_PATH.concat("/password/").concat(mockId); + MockHttpServletRequestBuilder mockHttpServletRequestBuilder = AysMockMvcRequestBuilders + .post(endpoint, mockPasswordCreateRequest); + + AysResponse mockResponse = AysResponseBuilder.SUCCESS; + + aysMockMvc.perform(mockHttpServletRequestBuilder, mockResponse) + .andExpect(AysMockResultMatchersBuilders.status() + .isOk()) + .andExpect(AysMockResultMatchersBuilders.response() + .doesNotExist()); + + // Verify + Mockito.verify(userPasswordService, Mockito.times(1)) + .createPassword(Mockito.anyString(), Mockito.any(AysPasswordCreateRequest.class)); + } + + @Test + void givenPasswordCreateRequest_whenPasswordsNotEqual_thenReturnValidationError() throws Exception { + + // Given + String mockId = "1fa43c75-7a7a-4041-8cef-03be8429dd30"; + AysPasswordCreateRequest mockPasswordCreateRequest = new AysPasswordCreateRequestBuilder() + .withPassword("password") + .withPasswordRepeat("password1") + .build(); + + // Then + String endpoint = BASE_PATH.concat("/password/").concat(mockId); + MockHttpServletRequestBuilder mockHttpServletRequestBuilder = AysMockMvcRequestBuilders + .post(endpoint, mockPasswordCreateRequest); + + AysErrorResponse mockErrorResponse = AysErrorBuilder.VALIDATION_ERROR; + + aysMockMvc.perform(mockHttpServletRequestBuilder, mockErrorResponse) + .andExpect(AysMockResultMatchersBuilders.status() + .isBadRequest()) + .andExpect(AysMockResultMatchersBuilders.subErrors() + .isNotEmpty()); + + // Verify + Mockito.verify(userPasswordService, Mockito.never()) + .createPassword(Mockito.anyString(), Mockito.any(AysPasswordCreateRequest.class)); + } + } diff --git a/src/test/java/org/ays/auth/controller/AysAuthEndToEndTest.java b/src/test/java/org/ays/auth/controller/AysAuthEndToEndTest.java index d4774faa5..af6251025 100644 --- a/src/test/java/org/ays/auth/controller/AysAuthEndToEndTest.java +++ b/src/test/java/org/ays/auth/controller/AysAuthEndToEndTest.java @@ -5,10 +5,12 @@ import org.ays.auth.model.AysUser; import org.ays.auth.model.AysUserBuilder; import org.ays.auth.model.enums.AysSourcePage; -import org.ays.auth.model.request.AysForgotPasswordRequest; import org.ays.auth.model.request.AysForgotPasswordRequestBuilder; import org.ays.auth.model.request.AysLoginRequest; import org.ays.auth.model.request.AysLoginRequestBuilder; +import org.ays.auth.model.request.AysPasswordCreateRequest; +import org.ays.auth.model.request.AysPasswordCreateRequestBuilder; +import org.ays.auth.model.request.AysPasswordForgotRequest; import org.ays.auth.model.request.AysTokenInvalidateRequest; import org.ays.auth.model.request.AysTokenRefreshRequest; import org.ays.auth.model.response.AysTokenResponse; @@ -130,10 +132,30 @@ void givenValidAysTokenInvalidateRequest_whenTokensInvalidated_thenReturnSuccess @Test - void givenValidForgotPasswordRequest_whenSendPasswordCreateMail_thenReturnSuccessResponse() throws Exception { + void givenValidForgotPasswordRequest_whenNewPasswordCreatedAndPasswordCreateMailSent_thenReturnSuccessResponse() throws Exception { + + // Initialize + Institution institution = new InstitutionBuilder() + .withId(AysValidTestData.Admin.INSTITUTION_ID) + .build(); + + AysRole role = roleReadPort.findAllActivesByInstitutionId(institution.getId()) + .stream() + .findFirst() + .orElseThrow(); + + AysUser user = userSavePort.save( + new AysUserBuilder() + .withValidValues() + .withoutId() + .withRoles(List.of(role)) + .withInstitution(institution) + .build() + ); + // Given - AysForgotPasswordRequest mockForgotPasswordRequest = new AysForgotPasswordRequestBuilder() - .withEmailAddress(AysValidTestData.User.EMAIL_ADDRESS) + AysPasswordForgotRequest mockForgotPasswordRequest = new AysForgotPasswordRequestBuilder() + .withEmailAddress(user.getEmailAddress()) .build(); // Then @@ -150,12 +172,18 @@ void givenValidForgotPasswordRequest_whenSendPasswordCreateMail_thenReturnSucces .doesNotExist()); // Verify - AysUser user = userReadPort.findByEmailAddress(AysValidTestData.User.EMAIL_ADDRESS) + AysUser userFromDatabase = userReadPort.findById(user.getId()) .orElseThrow(); - Assertions.assertNotNull(user.getPassword()); - Assertions.assertNotNull(user.getPassword().getForgotAt()); - Assertions.assertTrue(user.getPassword().getForgotAt().isAfter(LocalDateTime.now().minusMinutes(1))); + AysUser.Password passwordFromDatabase = userFromDatabase.getPassword(); + Assertions.assertNotNull(passwordFromDatabase); + Assertions.assertNotNull(passwordFromDatabase.getValue()); + Assertions.assertNotNull(passwordFromDatabase.getForgotAt()); + Assertions.assertTrue(passwordFromDatabase.getForgotAt().isAfter(LocalDateTime.now().minusMinutes(1))); + Assertions.assertNotNull(passwordFromDatabase.getCreatedUser()); + Assertions.assertNotNull(passwordFromDatabase.getCreatedAt()); + Assertions.assertNull(passwordFromDatabase.getUpdatedUser()); + Assertions.assertNull(passwordFromDatabase.getUpdatedAt()); } @@ -204,4 +232,68 @@ void givenValidId_whenCheckPasswordIdSuccessfully_thenReturnSuccessResponse() th .doesNotExist()); } + + @Test + void givenValidPasswordCreateRequest_whenPasswordCreated_thenReturnSuccessResponse() throws Exception { + + // Initialize + Institution institution = new InstitutionBuilder() + .withId(AysValidTestData.Admin.INSTITUTION_ID) + .build(); + + AysRole role = roleReadPort.findAllActivesByInstitutionId(institution.getId()) + .stream() + .findFirst() + .orElseThrow(); + + AysUser.Password password = new AysUserBuilder.PasswordBuilder() + .withoutId() + .withForgotAt(LocalDateTime.now().minusMinutes(15)) + .build(); + + AysUser user = userSavePort.save( + new AysUserBuilder() + .withValidValues() + .withoutId() + .withRoles(List.of(role)) + .withInstitution(institution) + .withPassword(password) + .build() + ); + + // Given + String mockId = user.getPassword().getId(); + + AysPasswordCreateRequest mockPasswordCreateRequest = new AysPasswordCreateRequestBuilder() + .withValidValues() + .build(); + + // Then + String endpoint = BASE_PATH.concat("/password/").concat(mockId); + MockHttpServletRequestBuilder mockHttpServletRequestBuilder = AysMockMvcRequestBuilders + .post(endpoint, mockPasswordCreateRequest); + + AysResponse mockResponse = AysResponseBuilder.SUCCESS; + + aysMockMvc.perform(mockHttpServletRequestBuilder, mockResponse) + .andExpect(AysMockResultMatchersBuilders.status() + .isOk()) + .andExpect(AysMockResultMatchersBuilders.response() + .doesNotExist()); + + // Verify + AysUser userFromDatabase = userReadPort.findById(user.getId()) + .orElseThrow(); + + AysUser.Password passwordFromDatabase = userFromDatabase.getPassword(); + Assertions.assertNotNull(passwordFromDatabase); + Assertions.assertNotEquals(mockId, passwordFromDatabase.getId()); + Assertions.assertNotNull(passwordFromDatabase.getValue()); + Assertions.assertNull(passwordFromDatabase.getForgotAt()); + Assertions.assertNotNull(passwordFromDatabase.getCreatedUser()); + Assertions.assertNotNull(passwordFromDatabase.getCreatedAt()); + Assertions.assertNull(passwordFromDatabase.getUpdatedUser()); + Assertions.assertNull(passwordFromDatabase.getUpdatedAt()); + } + } diff --git a/src/test/java/org/ays/auth/model/AysUserBuilder.java b/src/test/java/org/ays/auth/model/AysUserBuilder.java index 349e69aa8..b0dafd46e 100644 --- a/src/test/java/org/ays/auth/model/AysUserBuilder.java +++ b/src/test/java/org/ays/auth/model/AysUserBuilder.java @@ -109,7 +109,8 @@ public PasswordBuilder() { public PasswordBuilder withValidValues() { return this .withId("31c479b8-625f-49b2-8fbc-c0a085a79883") - .withValue(AysValidTestData.PASSWORD_ENCRYPTED); + .withValue(AysValidTestData.PASSWORD_ENCRYPTED) + .withoutForgotAt(); } public PasswordBuilder withId(String id) { @@ -132,6 +133,21 @@ public PasswordBuilder withForgotAt(LocalDateTime forgotAt) { return this; } + public PasswordBuilder withoutForgotAt() { + data.setForgotAt(null); + return this; + } + + public PasswordBuilder withCreatedAt(LocalDateTime createdAt) { + data.setCreatedAt(createdAt); + return this; + } + + public PasswordBuilder withUpdatedAt(LocalDateTime updatedAt) { + data.setUpdatedAt(updatedAt); + return this; + } + } diff --git a/src/test/java/org/ays/auth/model/request/AysForgotPasswordRequestBuilder.java b/src/test/java/org/ays/auth/model/request/AysForgotPasswordRequestBuilder.java index fc7084ee9..25a1b69bd 100644 --- a/src/test/java/org/ays/auth/model/request/AysForgotPasswordRequestBuilder.java +++ b/src/test/java/org/ays/auth/model/request/AysForgotPasswordRequestBuilder.java @@ -2,10 +2,10 @@ import org.ays.common.model.TestDataBuilder; -public class AysForgotPasswordRequestBuilder extends TestDataBuilder { +public class AysForgotPasswordRequestBuilder extends TestDataBuilder { public AysForgotPasswordRequestBuilder() { - super(AysForgotPasswordRequest.class); + super(AysPasswordForgotRequest.class); } public AysForgotPasswordRequestBuilder withValidValues() { diff --git a/src/test/java/org/ays/auth/model/request/AysPasswordCreateRequestBuilder.java b/src/test/java/org/ays/auth/model/request/AysPasswordCreateRequestBuilder.java new file mode 100644 index 000000000..cf26c05e5 --- /dev/null +++ b/src/test/java/org/ays/auth/model/request/AysPasswordCreateRequestBuilder.java @@ -0,0 +1,27 @@ +package org.ays.auth.model.request; + +import org.ays.common.model.TestDataBuilder; + +public class AysPasswordCreateRequestBuilder extends TestDataBuilder { + + public AysPasswordCreateRequestBuilder() { + super(AysPasswordCreateRequest.class); + } + + public AysPasswordCreateRequestBuilder withValidValues() { + return this + .withPassword("testpass") + .withPasswordRepeat("testpass"); + } + + public AysPasswordCreateRequestBuilder withPassword(String password) { + data.setPassword(password); + return this; + } + + public AysPasswordCreateRequestBuilder withPasswordRepeat(String passwordRepeat) { + data.setPasswordRepeat(passwordRepeat); + return this; + } + +} diff --git a/src/test/java/org/ays/auth/service/impl/AysUserPasswordServiceImplTest.java b/src/test/java/org/ays/auth/service/impl/AysUserPasswordServiceImplTest.java index a45ad3bb1..eebfedc73 100644 --- a/src/test/java/org/ays/auth/service/impl/AysUserPasswordServiceImplTest.java +++ b/src/test/java/org/ays/auth/service/impl/AysUserPasswordServiceImplTest.java @@ -3,8 +3,10 @@ import org.ays.AysUnitTest; import org.ays.auth.model.AysUser; import org.ays.auth.model.AysUserBuilder; -import org.ays.auth.model.request.AysForgotPasswordRequest; import org.ays.auth.model.request.AysForgotPasswordRequestBuilder; +import org.ays.auth.model.request.AysPasswordCreateRequest; +import org.ays.auth.model.request.AysPasswordCreateRequestBuilder; +import org.ays.auth.model.request.AysPasswordForgotRequest; import org.ays.auth.port.AysUserReadPort; import org.ays.auth.port.AysUserSavePort; import org.ays.auth.service.AysUserMailService; @@ -16,6 +18,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; +import org.springframework.security.crypto.password.PasswordEncoder; import java.time.LocalDateTime; import java.util.Optional; @@ -34,11 +37,14 @@ class AysUserPasswordServiceImplTest extends AysUnitTest { @Mock private AysUserMailService userMailService; + @Mock + private PasswordEncoder passwordEncoder; + @Test void givenValidForgotPasswordRequest_whenUserExistWithPassword_thenSetPasswordForgotAtAndSendPasswordCreateEmail() { // Given - AysForgotPasswordRequest mockForgotPasswordRequest = new AysForgotPasswordRequestBuilder() + AysPasswordForgotRequest mockForgotPasswordRequest = new AysForgotPasswordRequestBuilder() .withValidValues() .build(); @@ -51,14 +57,18 @@ void givenValidForgotPasswordRequest_whenUserExistWithPassword_thenSetPasswordFo Mockito.when(userReadPort.findByEmailAddress(Mockito.anyString())) .thenReturn(Optional.of(mockUser)); + AysUser.Password mockPassword = new AysUserBuilder.PasswordBuilder() + .withValidValues() + .withValue("16b6a38b-a84d-4545-b05b-68758f998cb4") + .withForgotAt(LocalDateTime.now()) + .build(); AysUser mockSavedUser = new AysUserBuilder() .withValidValues() .withId(mockUser.getId()) .withEmailAddress(mockUser.getEmailAddress()) .withPhoneNumber(mockUser.getPhoneNumber()) - .withPassword(mockUser.getPassword()) + .withPassword(mockPassword) .build(); - mockSavedUser.getPassword().setForgotAt(LocalDateTime.now()); Mockito.when(userSavePort.save(Mockito.any(AysUser.class))) .thenReturn(mockSavedUser); @@ -83,7 +93,7 @@ void givenValidForgotPasswordRequest_whenUserExistWithPassword_thenSetPasswordFo @Test void givenValidForgotPasswordRequest_whenUserExistWithoutPassword_thenCreateTempPasswordAndSendPasswordCreateEmail() { // Given - AysForgotPasswordRequest mockForgotPasswordRequest = new AysForgotPasswordRequestBuilder() + AysPasswordForgotRequest mockForgotPasswordRequest = new AysForgotPasswordRequestBuilder() .withValidValues() .build(); @@ -95,12 +105,17 @@ void givenValidForgotPasswordRequest_whenUserExistWithoutPassword_thenCreateTemp Mockito.when(userReadPort.findByEmailAddress(Mockito.anyString())) .thenReturn(Optional.of(mockUser)); + AysUser.Password mockPassword = new AysUserBuilder.PasswordBuilder() + .withValidValues() + .withValue("b78a9229-9ca6-4a2b-9c14-aac160473d13") + .withForgotAt(LocalDateTime.now()) + .build(); AysUser mockSavedUser = new AysUserBuilder() .withValidValues() .withId(mockUser.getId()) .withEmailAddress(mockUser.getEmailAddress()) .withPhoneNumber(mockUser.getPhoneNumber()) - .withPassword(new AysUserBuilder.PasswordBuilder().withValidValues().build()) + .withPassword(mockPassword) .build(); Mockito.when(userSavePort.save(Mockito.any(AysUser.class))) .thenReturn(mockSavedUser); @@ -126,7 +141,7 @@ void givenValidForgotPasswordRequest_whenUserExistWithoutPassword_thenCreateTemp @Test void givenValidForgotPasswordRequest_whenEmailDoesNotExist_thenThrowAysEmailAddressNotValidException() { // Given - AysForgotPasswordRequest mockForgotPasswordRequest = new AysForgotPasswordRequestBuilder() + AysPasswordForgotRequest mockForgotPasswordRequest = new AysForgotPasswordRequestBuilder() .withValidValues() .build(); @@ -155,13 +170,15 @@ void givenValidForgotPasswordRequest_whenEmailDoesNotExist_thenThrowAysEmailAddr @Test void givenValidId_whenPasswordExistAndForgotInTwoHours_thenDoNothing() { // Given - String mockId = "40fb7a46-40bd-46cb-b44f-1f47162133b1"; + String mockId = "67134f24-49b4-4abe-946e-550d7cf3abd3"; // When AysUser.Password mockPassword = new AysUserBuilder.PasswordBuilder() .withValidValues() .withId(mockId) .withForgotAt(LocalDateTime.now().minusMinutes(5)) + .withCreatedAt(LocalDateTime.now().minusDays(1)) + .withUpdatedAt(LocalDateTime.now().minusHours(3)) .build(); AysUser mockUser = new AysUserBuilder() .withValidValues() @@ -181,7 +198,7 @@ void givenValidId_whenPasswordExistAndForgotInTwoHours_thenDoNothing() { @Test void givenId_whenPasswordDoesExist_thenThrowUserPasswordDoesNotExistException() { // Given - String mockId = "40fb7a46-40bd-46cb-b44f-1f47162133b1"; + String mockId = "6b4e213e-b2e6-468c-acec-6768c636be7e"; // When Mockito.when(userReadPort.findByPasswordId(Mockito.anyString())) @@ -201,7 +218,7 @@ void givenId_whenPasswordDoesExist_thenThrowUserPasswordDoesNotExistException() @Test void givenValidId_whenPasswordExistAndForgotAtDoesNotExist_thenThrowUserPasswordCannotChangedException() { // Given - String mockId = "40fb7a46-40bd-46cb-b44f-1f47162133b1"; + String mockId = "c009ddf2-a74a-4ef6-b530-d70f49ea4f0e"; // When AysUser.Password mockPassword = new AysUserBuilder.PasswordBuilder() @@ -209,6 +226,8 @@ void givenValidId_whenPasswordExistAndForgotAtDoesNotExist_thenThrowUserPassword .withId(mockId) .withValue("608a15a8-5e82-4fd8-ac74-308068393e53") .withForgotAt(null) + .withCreatedAt(LocalDateTime.now().minusDays(1)) + .withUpdatedAt(LocalDateTime.now().minusHours(3)) .build(); AysUser mockUser = new AysUserBuilder() .withValidValues() @@ -231,7 +250,7 @@ void givenValidId_whenPasswordExistAndForgotAtDoesNotExist_thenThrowUserPassword @Test void givenValidId_whenPasswordExistAndForgotInThreeHours_thenThrowUserPasswordCannotChangedException() { // Given - String mockId = "40fb7a46-40bd-46cb-b44f-1f47162133b1"; + String mockId = "d6c36815-0e33-4224-b9ce-b4acf431b891"; // When AysUser.Password mockPassword = new AysUserBuilder.PasswordBuilder() @@ -239,6 +258,8 @@ void givenValidId_whenPasswordExistAndForgotInThreeHours_thenThrowUserPasswordCa .withId(mockId) .withValue("608a15a8-5e82-4fd8-ac74-308068393e53") .withForgotAt(LocalDateTime.now().minusHours(3)) + .withCreatedAt(LocalDateTime.now().minusDays(1)) + .withUpdatedAt(LocalDateTime.now().minusHours(3)) .build(); AysUser mockUser = new AysUserBuilder() .withValidValues() @@ -258,4 +279,220 @@ void givenValidId_whenPasswordExistAndForgotInThreeHours_thenThrowUserPasswordCa .findByPasswordId(Mockito.anyString()); } + + @Test + void givenValidIdAndCreateRequest_whenPasswordExistAndForgotInTwoHours_thenEncryptPasswordAndSetAndForgotAtToNullAndSaveUser() { + // Given + String mockId = "1cf3234f-240d-48c4-b557-a4110f5f0391"; + AysPasswordCreateRequest mockCreateRequest = new AysPasswordCreateRequestBuilder() + .withValidValues() + .build(); + + // When + AysUser.Password mockPassword = new AysUserBuilder.PasswordBuilder() + .withValidValues() + .withId(mockId) + .withForgotAt(LocalDateTime.now().minusMinutes(5)) + .withCreatedAt(LocalDateTime.now().minusDays(1)) + .build(); + AysUser mockUser = new AysUserBuilder() + .withValidValues() + .withPassword(mockPassword) + .build(); + Mockito.when(userReadPort.findByPasswordId(Mockito.anyString())) + .thenReturn(Optional.of(mockUser)); + + String mockEncodedPassword = "encodedPassword"; + Mockito.when(passwordEncoder.encode(Mockito.anyString())) + .thenReturn(mockEncodedPassword); + + AysUser.Password mockSavedPassword = new AysUserBuilder.PasswordBuilder() + .withValidValues() + .withValue(mockEncodedPassword) + .build(); + AysUser mockSavedUser = new AysUserBuilder() + .withValidValues() + .withId(mockUser.getId()) + .withEmailAddress(mockUser.getEmailAddress()) + .withPhoneNumber(mockUser.getPhoneNumber()) + .withPassword(mockSavedPassword) + .build(); + Mockito.when(userSavePort.save(Mockito.any(AysUser.class))) + .thenReturn(mockSavedUser); + + // Then + userPasswordService.createPassword(mockId, mockCreateRequest); + + // Verify + Mockito.verify(userReadPort, Mockito.times(1)) + .findByPasswordId(Mockito.anyString()); + + Mockito.verify(passwordEncoder, Mockito.times(1)) + .encode(Mockito.anyString()); + + Mockito.verify(userSavePort, Mockito.times(1)) + .save(Mockito.any(AysUser.class)); + } + + @Test + void givenValidIdAndCreateRequest_whenPasswordExistAndForgotAtAndUpdatedAtDoesNotExist_thenEncryptPasswordAndSetAndForgotAtToNullAndSaveUser() { + // Given + String mockId = "918481b0-6bcc-4b74-9062-27e5e6708c6c"; + AysPasswordCreateRequest mockCreateRequest = new AysPasswordCreateRequestBuilder() + .withValidValues() + .build(); + + // When + AysUser.Password mockPassword = new AysUserBuilder.PasswordBuilder() + .withValidValues() + .withId(mockId) + .withoutForgotAt() + .withCreatedAt(LocalDateTime.now().minusHours(1)) + .build(); + AysUser mockUser = new AysUserBuilder() + .withValidValues() + .withPassword(mockPassword) + .build(); + Mockito.when(userReadPort.findByPasswordId(Mockito.anyString())) + .thenReturn(Optional.of(mockUser)); + + String mockEncodedPassword = "encodedPassword"; + Mockito.when(passwordEncoder.encode(Mockito.anyString())) + .thenReturn(mockEncodedPassword); + + AysUser.Password mockSavedPassword = new AysUserBuilder.PasswordBuilder() + .withValidValues() + .withValue(mockEncodedPassword) + .build(); + AysUser mockSavedUser = new AysUserBuilder() + .withValidValues() + .withId(mockUser.getId()) + .withEmailAddress(mockUser.getEmailAddress()) + .withPhoneNumber(mockUser.getPhoneNumber()) + .withPassword(mockSavedPassword) + .build(); + Mockito.when(userSavePort.save(Mockito.any(AysUser.class))) + .thenReturn(mockSavedUser); + + // Then + userPasswordService.createPassword(mockId, mockCreateRequest); + + // Verify + Mockito.verify(userReadPort, Mockito.times(1)) + .findByPasswordId(Mockito.anyString()); + + Mockito.verify(passwordEncoder, Mockito.times(1)) + .encode(Mockito.anyString()); + + Mockito.verify(userSavePort, Mockito.times(1)) + .save(Mockito.any(AysUser.class)); + } + + @Test + void givenIdAndCreateRequest_whenPasswordDoesExist_thenThrowUserPasswordDoesNotExistException() { + // Given + String mockId = "5075b296-6099-4cd5-84e7-cf744aaf2360"; + AysPasswordCreateRequest mockCreateRequest = new AysPasswordCreateRequestBuilder() + .withValidValues() + .build(); + + // When + Mockito.when(userReadPort.findByPasswordId(Mockito.anyString())) + .thenReturn(Optional.empty()); + + // Then + Assertions.assertThrows( + AysUserPasswordDoesNotExistException.class, + () -> userPasswordService.createPassword(mockId, mockCreateRequest) + ); + + // Verify + Mockito.verify(userReadPort, Mockito.times(1)) + .findByPasswordId(Mockito.anyString()); + + Mockito.verify(passwordEncoder, Mockito.never()) + .encode(Mockito.anyString()); + + Mockito.verify(userSavePort, Mockito.never()) + .save(Mockito.any(AysUser.class)); + } + + @Test + void givenValidIdAndCreateRequest_whenPasswordExistWithUpdatedAtAndForgotAtDoesNotExist_thenThrowUserPasswordCannotChangedException() { + // Given + String mockId = "548581fd-0f2b-482e-bd99-ef73a07b3e44"; + AysPasswordCreateRequest mockCreateRequest = new AysPasswordCreateRequestBuilder() + .withValidValues() + .build(); + + // When + AysUser.Password mockPassword = new AysUserBuilder.PasswordBuilder() + .withValidValues() + .withId(mockId) + .withForgotAt(null) + .withCreatedAt(LocalDateTime.now().minusDays(1)) + .withUpdatedAt(LocalDateTime.now().minusHours(3)) + .build(); + AysUser mockUser = new AysUserBuilder() + .withValidValues() + .withPassword(mockPassword) + .build(); + Mockito.when(userReadPort.findByPasswordId(Mockito.anyString())) + .thenReturn(Optional.of(mockUser)); + + // Then + Assertions.assertThrows( + AysUserPasswordCannotChangedException.class, + () -> userPasswordService.createPassword(mockId, mockCreateRequest) + ); + + // Verify + Mockito.verify(userReadPort, Mockito.times(1)) + .findByPasswordId(Mockito.anyString()); + + Mockito.verify(passwordEncoder, Mockito.never()) + .encode(Mockito.anyString()); + + Mockito.verify(userSavePort, Mockito.never()) + .save(Mockito.any(AysUser.class)); + } + + @Test + void givenValidIdAndCreateRequest_whenPasswordExistAndForgotInThreeHours_thenThrowUserPasswordCannotChangedException() { + // Given + String mockId = "498ef088-92b0-41d8-a39a-8d9bae87ce8d"; + AysPasswordCreateRequest mockCreateRequest = new AysPasswordCreateRequestBuilder() + .withValidValues() + .build(); + + // When + AysUser.Password mockPassword = new AysUserBuilder.PasswordBuilder() + .withValidValues() + .withId(mockId) + .withForgotAt(LocalDateTime.now().minusHours(3)) + .build(); + AysUser mockUser = new AysUserBuilder() + .withValidValues() + .withPassword(mockPassword) + .build(); + Mockito.when(userReadPort.findByPasswordId(Mockito.anyString())) + .thenReturn(Optional.of(mockUser)); + + // Then + Assertions.assertThrows( + AysUserPasswordCannotChangedException.class, + () -> userPasswordService.createPassword(mockId, mockCreateRequest) + ); + + // Verify + Mockito.verify(userReadPort, Mockito.times(1)) + .findByPasswordId(Mockito.anyString()); + + Mockito.verify(passwordEncoder, Mockito.never()) + .encode(Mockito.anyString()); + + Mockito.verify(userSavePort, Mockito.never()) + .save(Mockito.any(AysUser.class)); + } + }