diff --git a/src/main/java/org/ays/auth/controller/AysUserController.java b/src/main/java/org/ays/auth/controller/AysUserController.java index 04aeb7d35..14acc2a76 100644 --- a/src/main/java/org/ays/auth/controller/AysUserController.java +++ b/src/main/java/org/ays/auth/controller/AysUserController.java @@ -20,6 +20,7 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; @@ -131,4 +132,22 @@ public AysResponse update(@PathVariable @UUID final String id, return AysResponse.SUCCESS; } + + /** + * PATCH /user/{id}/activate : Activates a user account with the given ID. + *

+ * This endpoint is protected and requires the caller to have the authority + * 'user:update'. The user ID must be a valid UUID. + *

+ * + * @param id The UUID of the user to be activated. + * @return An {@link AysResponse} indicating the success of the operation. + */ + @PatchMapping("user/{id}/activate") + @PreAuthorize("hasAnyAuthority('user:update')") + public AysResponse activate(@PathVariable @UUID final String id) { + userUpdateService.activate(id); + return AysResponse.SUCCESS; + } + } diff --git a/src/main/java/org/ays/auth/service/AysUserUpdateService.java b/src/main/java/org/ays/auth/service/AysUserUpdateService.java index c6ef63755..b04eb2c64 100644 --- a/src/main/java/org/ays/auth/service/AysUserUpdateService.java +++ b/src/main/java/org/ays/auth/service/AysUserUpdateService.java @@ -1,6 +1,8 @@ package org.ays.auth.service; import org.ays.auth.model.request.AysUserUpdateRequest; +import org.ays.auth.util.exception.AysUserNotExistByIdException; +import org.ays.auth.util.exception.AysUserNotPassiveException; /** * Service interface for updating users. @@ -14,4 +16,13 @@ public interface AysUserUpdateService { */ void update(String id, AysUserUpdateRequest updateRequest); + /** + * Activates a user by ID if the user is currently passive. + * + * @param id The unique identifier of the user to be activated. + * @throws AysUserNotExistByIdException if a user with the given ID does not exist. + * @throws AysUserNotPassiveException if the user is not in a passive state. + */ + void activate(String id); + } diff --git a/src/main/java/org/ays/auth/service/impl/AysUserUpdateServiceImpl.java b/src/main/java/org/ays/auth/service/impl/AysUserUpdateServiceImpl.java index 0f41ef572..a3c89db6f 100644 --- a/src/main/java/org/ays/auth/service/impl/AysUserUpdateServiceImpl.java +++ b/src/main/java/org/ays/auth/service/impl/AysUserUpdateServiceImpl.java @@ -4,6 +4,7 @@ import org.ays.auth.model.AysIdentity; import org.ays.auth.model.AysRole; import org.ays.auth.model.AysUser; +import org.ays.auth.model.enums.AysUserStatus; import org.ays.auth.model.request.AysUserUpdateRequest; import org.ays.auth.port.AysRoleReadPort; import org.ays.auth.port.AysUserReadPort; @@ -14,6 +15,7 @@ import org.ays.auth.util.exception.AysUserAlreadyExistsByPhoneNumberException; import org.ays.auth.util.exception.AysUserIsNotActiveOrPassiveException; import org.ays.auth.util.exception.AysUserNotExistByIdException; +import org.ays.auth.util.exception.AysUserNotPassiveException; import org.ays.common.model.AysPhoneNumber; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -76,6 +78,30 @@ public void update(final String id, } + /** + * Activates a user by ID if the user is currently passive. + * This method retrieves the user by the provided ID and activates the user + * + * @param id The unique identifier of the user to be activated. + * @throws AysUserNotExistByIdException if a user with the given ID does not exist. + * @throws AysUserNotPassiveException if the user is not in a passive state and cannot be activated. + */ + @Override + public void activate(String id) { + + final AysUser user = userReadPort.findById(id) + .filter(userFromDatabase -> identity.getInstitutionId().equals(userFromDatabase.getInstitution().getId())) + .orElseThrow(() -> new AysUserNotExistByIdException(id)); + + if (!user.isPassive()) { + throw new AysUserNotPassiveException(AysUserStatus.PASSIVE); + } + + user.activate(); + userSavePort.save(user); + } + + /** * Validates the uniqueness of the provided phone number. * Checks if there is any existing user with the same phone number. diff --git a/src/main/java/org/ays/auth/util/exception/AysUserNotPassiveException.java b/src/main/java/org/ays/auth/util/exception/AysUserNotPassiveException.java new file mode 100644 index 000000000..db32a09d9 --- /dev/null +++ b/src/main/java/org/ays/auth/util/exception/AysUserNotPassiveException.java @@ -0,0 +1,28 @@ +package org.ays.auth.util.exception; + +import org.ays.auth.model.enums.AysUserStatus; +import org.ays.common.util.exception.AysNotExistException; + +import java.io.Serial; + +/** + Exception thrown when a user does not in a passive state. + * This exception extends {@link AysNotExistException}. + */ +public final class AysUserNotPassiveException extends AysNotExistException { + + /** + * Unique serial version ID. + */ + @Serial + private static final long serialVersionUID = 3508025652421021710L; + + /** + * Constructs a new {@link AysNotExistException} with the specified detail message. + * + * @param status the detail message. + */ + public AysUserNotPassiveException(AysUserStatus status) { + super("user status is not with " + status.toString().toLowerCase() + "!"); + } +} diff --git a/src/test/java/org/ays/auth/controller/AysUserControllerTest.java b/src/test/java/org/ays/auth/controller/AysUserControllerTest.java index ba2146f50..0b28a9b38 100644 --- a/src/test/java/org/ays/auth/controller/AysUserControllerTest.java +++ b/src/test/java/org/ays/auth/controller/AysUserControllerTest.java @@ -638,4 +638,83 @@ void givenValidIdAndInvalidUserUpdateRequest_whenRoleIdIsNotValid_thenReturnVali .update(Mockito.anyString(), Mockito.any(AysUserUpdateRequest.class)); } + + @Test + void givenValidId_whenActivateUser_thenReturnSuccess() throws Exception { + + // Given + String mockId = "793fcc5d-31cc-4704-9f0a-627ac7da517d"; + + // When + Mockito.doNothing() + .when(userUpdateService) + .activate(Mockito.anyString()); + + // Then + String endpoint = BASE_PATH.concat("/user/").concat(mockId).concat("/activate"); + MockHttpServletRequestBuilder mockHttpServletRequestBuilder = AysMockMvcRequestBuilders + .patch(endpoint, mockAdminToken.getAccessToken()); + + AysResponse mockResponse = AysResponseBuilder.SUCCESS; + + aysMockMvc.perform(mockHttpServletRequestBuilder, mockResponse) + .andExpect(AysMockResultMatchersBuilders.status() + .isOk()) + .andExpect(AysMockResultMatchersBuilders.response() + .doesNotExist()); + + // Verify + Mockito.verify(userUpdateService, Mockito.times(1)) + .activate(Mockito.anyString()); + } + + @Test + void givenValidId_whenUserUnauthorized_thenReturnAccessDeniedException() throws Exception { + + // Given + String mockId = "201aec72-ecd8-49fc-86f5-2b5458871edb"; + + // Then + String endpoint = BASE_PATH.concat("/user/").concat(mockId).concat("/activate"); + MockHttpServletRequestBuilder mockHttpServletRequestBuilder = AysMockMvcRequestBuilders + .patch(endpoint, mockUserToken.getAccessToken()); + + AysErrorResponse mockErrorResponse = AysErrorBuilder.FORBIDDEN; + + aysMockMvc.perform(mockHttpServletRequestBuilder, mockErrorResponse) + .andExpect(AysMockResultMatchersBuilders.status() + .isForbidden()) + .andExpect(AysMockResultMatchersBuilders.response() + .doesNotExist()); + + // Verify + Mockito.verify(userUpdateService, Mockito.never()) + .activate(Mockito.anyString()); + } + + @ParameterizedTest + @ValueSource(strings = { + "A", + "493268349068342" + }) + void givenInvalidUserId_whenIdNotValid_thenReturnValidationError(String invalidId) throws Exception { + + // Then + String endpoint = BASE_PATH.concat("/user/").concat(invalidId).concat("/activate"); + MockHttpServletRequestBuilder mockHttpServletRequestBuilder = AysMockMvcRequestBuilders + .patch(endpoint, mockAdminToken.getAccessToken()); + + AysErrorResponse mockErrorResponse = AysErrorBuilder.VALIDATION_ERROR; + + aysMockMvc.perform(mockHttpServletRequestBuilder, mockErrorResponse) + .andExpect(AysMockResultMatchersBuilders.status() + .isBadRequest()) + .andExpect(AysMockResultMatchersBuilders.subErrors() + .isNotEmpty()); + + // Verify + Mockito.verify(userUpdateService, Mockito.never()) + .activate(Mockito.anyString()); + } + } diff --git a/src/test/java/org/ays/auth/controller/AysUserEndToEndTest.java b/src/test/java/org/ays/auth/controller/AysUserEndToEndTest.java index 51545d0da..72eb14373 100644 --- a/src/test/java/org/ays/auth/controller/AysUserEndToEndTest.java +++ b/src/test/java/org/ays/auth/controller/AysUserEndToEndTest.java @@ -20,6 +20,7 @@ import org.ays.common.model.response.AysPageResponse; import org.ays.common.model.response.AysResponse; import org.ays.common.model.response.AysResponseBuilder; +import org.ays.common.util.AysRandomUtil; import org.ays.institution.model.Institution; import org.ays.institution.model.InstitutionBuilder; import org.ays.util.AysMockMvcRequestBuilders; @@ -372,4 +373,48 @@ void givenValidIdAndUserUpdateRequest_whenUserUpdated_thenReturnSuccess() throws Assertions.assertNotNull(userFromDatabase.get().getUpdatedAt()); } + @Test + void givenValidId_whenActivateUser_thenReturnSuccess() throws Exception { + + // Initialize + Institution institution = new InstitutionBuilder() + .withId(AysValidTestData.Admin.INSTITUTION_ID) + .build(); + + List roles = roleReadPort.findAllActivesByInstitutionId(institution.getId()); + + AysUser user = userSavePort.save( + new AysUserBuilder() + .withId(AysRandomUtil.generateUUID()) + .withValidValues() + .withRoles(roles) + .withInstitution(institution) + .withStatus(AysUserStatus.PASSIVE) + .build() + ); + + // Given + String id = user.getId(); + + // Then + String endpoint = BASE_PATH.concat("/user/").concat(id).concat("/activate"); + MockHttpServletRequestBuilder mockHttpServletRequestBuilder = AysMockMvcRequestBuilders + .patch(endpoint, adminToken.getAccessToken()); + + AysResponse mockResponse = AysResponseBuilder.SUCCESS; + + aysMockMvc.perform(mockHttpServletRequestBuilder, mockResponse) + .andExpect(AysMockResultMatchersBuilders.status() + .isOk()) + .andExpect(AysMockResultMatchersBuilders.response() + .doesNotExist()); + + // Verify + Optional userFromDatabase = userReadPort.findById(user.getId()); + + Assertions.assertTrue(userFromDatabase.isPresent()); + Assertions.assertEquals(userFromDatabase.get().getId(), user.getId()); + Assertions.assertEquals(AysUserStatus.ACTIVE, userFromDatabase.get().getStatus()); + } + } diff --git a/src/test/java/org/ays/auth/service/impl/AysUserUpdateServiceImplTest.java b/src/test/java/org/ays/auth/service/impl/AysUserUpdateServiceImplTest.java index ad3153feb..265f62cc6 100644 --- a/src/test/java/org/ays/auth/service/impl/AysUserUpdateServiceImplTest.java +++ b/src/test/java/org/ays/auth/service/impl/AysUserUpdateServiceImplTest.java @@ -17,6 +17,7 @@ import org.ays.auth.util.exception.AysUserAlreadyExistsByPhoneNumberException; import org.ays.auth.util.exception.AysUserIsNotActiveOrPassiveException; import org.ays.auth.util.exception.AysUserNotExistByIdException; +import org.ays.auth.util.exception.AysUserNotPassiveException; import org.ays.common.model.AysPhoneNumber; import org.ays.common.model.AysPhoneNumberBuilder; import org.ays.common.util.AysRandomUtil; @@ -521,4 +522,156 @@ void givenValidIdAndUserUpdateRequest_whenRolesNotExist_thenThrowAysRolesNotExis .save(Mockito.any(AysUser.class)); } + + @Test + void givenValidId_whenUserIsPassive_thenActivateUser() { + + // Given + String mockId = "21a0ab5a-c0e9-4789-9704-a6b5c02e2325"; + + // When + Institution mockInstitution = new InstitutionBuilder() + .withValidValues() + .build(); + Mockito.when(identity.getInstitutionId()) + .thenReturn(mockInstitution.getId()); + + AysUser mockUser = new AysUserBuilder() + .withValidValues() + .withId(mockId) + .withInstitution(mockInstitution) + .withStatus(AysUserStatus.PASSIVE) + .build(); + + Mockito.when(userReadPort.findById(Mockito.anyString())) + .thenReturn(Optional.of(mockUser)); + + Mockito.when(userSavePort.save(Mockito.any(AysUser.class))) + .thenReturn(Mockito.mock(AysUser.class)); + + // Then + userUpdateService.activate(mockId); + + // Verify + Mockito.verify(identity, Mockito.times(1)) + .getInstitutionId(); + + Mockito.verify(userReadPort, Mockito.times(1)) + .findById(Mockito.anyString()); + + Mockito.verify(userSavePort, Mockito.times(1)) + .save(Mockito.any(AysUser.class)); + } + + @Test + void givenValidId_whenUserNotFound_thenThrowAysUserNotExistByIdException(){ + + // Given + String mockId = "9f1eb072-7830-4c43-9a32-d77b62ccddd3"; + + // When + Mockito.when(userReadPort.findById(Mockito.anyString())) + .thenReturn(Optional.empty()); + + // Then + Assertions.assertThrows( + AysUserNotExistByIdException.class, + () -> userUpdateService.activate(mockId) + ); + + // Verify + Mockito.verify(userReadPort, Mockito.times(1)) + .findById(Mockito.anyString()); + + Mockito.verify(identity, Mockito.never()) + .getInstitutionId(); + + Mockito.verify(userSavePort, Mockito.never()) + .save(Mockito.any(AysUser.class)); + } + + @Test + void givenValidId_whenUserIsNotPassive_thenThrowAysUserIsNotPassiveException(){ + + // Given + String mockId = "bf7cc8d4-eab7-487d-8564-19be0f439b4a"; + + // When + Institution mockInstitution = new InstitutionBuilder() + .withValidValues() + .build(); + Mockito.when(identity.getInstitutionId()) + .thenReturn(mockInstitution.getId()); + + AysUser mockUser = new AysUserBuilder() + .withValidValues() + .withId(mockId) + .withInstitution(mockInstitution) + .withStatus(AysUserStatus.ACTIVE) + .build(); + + Mockito.when(userReadPort.findById(Mockito.anyString())) + .thenReturn(Optional.of(mockUser)); + + // Then + Assertions.assertThrows( + AysUserNotPassiveException.class, + () -> userUpdateService.activate(mockId) + ); + + // Verify + Mockito.verify(identity, Mockito.times(1)) + .getInstitutionId(); + + Mockito.verify(userReadPort, Mockito.times(1)) + .findById(Mockito.anyString()); + + Mockito.verify(userSavePort, Mockito.never()) + .save(Mockito.any(AysUser.class)); + } + + @Test + void givenValidId_whenInstitutionIdDoesNotMatch_thenThrowAysUserNotExistByIdException() { + + // Given + String mockId = "a785c6a2-229f-4a73-8e3a-3ff49bd16a07"; + + // When + Institution mockInstitution = new InstitutionBuilder() + .withValidValues() + .build(); + + Institution differentInstitution = new InstitutionBuilder() + .withValidValues() + .build(); + + AysUser mockUser = new AysUserBuilder() + .withValidValues() + .withId(mockId) + .withInstitution(differentInstitution) + .withStatus(AysUserStatus.PASSIVE) + .build(); + + Mockito.when(identity.getInstitutionId()) + .thenReturn(mockInstitution.getId()); + + Mockito.when(userReadPort.findById(mockId)) + .thenReturn(Optional.of(mockUser)); + + // Then + Assertions.assertThrows( + AysUserNotExistByIdException.class, + () -> userUpdateService.activate(mockId) + ); + + // Verify + Mockito.verify(identity, Mockito.times(1)) + .getInstitutionId(); + + Mockito.verify(userReadPort, Mockito.times(1)) + .findById(mockId); + + Mockito.verify(userSavePort, Mockito.never()) + .save(mockUser); + } }