From be3230ac204f98cb14d09a801548b0c9ba64fcc2 Mon Sep 17 00:00:00 2001 From: Egehan Asal Date: Sun, 7 Jul 2024 14:56:22 +0300 Subject: [PATCH] AYS-242 | Role Activation Flow Has Been Created (#336) --- .../auth/controller/AysRoleController.java | 18 ++++ src/main/java/org/ays/auth/model/AysRole.java | 8 ++ .../auth/service/AysRoleUpdateService.java | 7 ++ .../impl/AysRoleUpdateServiceImpl.java | 30 ++++++ .../AysInvalidRoleStatusException.java | 28 ++++++ .../controller/AysRoleControllerTest.java | 56 +++++++++++ .../auth/controller/AysRoleEndToEndTest.java | 45 +++++++++ .../impl/AysRoleUpdateServiceImplTest.java | 93 +++++++++++++++++++ .../ays/util/AysMockMvcRequestBuilders.java | 6 ++ 9 files changed, 291 insertions(+) create mode 100644 src/main/java/org/ays/auth/util/exception/AysInvalidRoleStatusException.java diff --git a/src/main/java/org/ays/auth/controller/AysRoleController.java b/src/main/java/org/ays/auth/controller/AysRoleController.java index c28b9cac5..f6f2335f2 100644 --- a/src/main/java/org/ays/auth/controller/AysRoleController.java +++ b/src/main/java/org/ays/auth/controller/AysRoleController.java @@ -26,6 +26,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -159,6 +160,23 @@ public AysResponse update(@PathVariable @UUID final String id, return AysResponse.SUCCESS; } + /** + * PATCH /role/{id}/activate : Activate an existing role. + *

+ * This endpoint handles the activation of a role based on its ID. The user must have the + * 'role:update' authority to access this endpoint. + *

+ * + * @param id The ID of the role to activate. + * @return An {@link AysResponse} indicating the success of the operation. + */ + @PatchMapping("/role/{id}/activate") + @PreAuthorize("hasAnyAuthority('role:update')") + public AysResponse activate(@PathVariable @UUID final String id) { + roleUpdateService.activate(id); + return AysResponse.SUCCESS; + } + /** * Delete an existing role by its ID. diff --git a/src/main/java/org/ays/auth/model/AysRole.java b/src/main/java/org/ays/auth/model/AysRole.java index 9217c48df..f1fe8c42e 100644 --- a/src/main/java/org/ays/auth/model/AysRole.java +++ b/src/main/java/org/ays/auth/model/AysRole.java @@ -48,6 +48,14 @@ public boolean isPassive() { return AysRoleStatus.PASSIVE.equals(this.status); } + /** + * Activates the role by setting its status to {@link AysRoleStatus#ACTIVE}. + * This method should be called when the role needs to be marked as active in the system. + */ + public void activate() { + this.setStatus(AysRoleStatus.ACTIVE); + } + /** * Checks if the role's status is deleted. * diff --git a/src/main/java/org/ays/auth/service/AysRoleUpdateService.java b/src/main/java/org/ays/auth/service/AysRoleUpdateService.java index 9a6b626b7..99d40e237 100644 --- a/src/main/java/org/ays/auth/service/AysRoleUpdateService.java +++ b/src/main/java/org/ays/auth/service/AysRoleUpdateService.java @@ -16,6 +16,13 @@ public interface AysRoleUpdateService { */ void update(String id, AysRoleUpdateRequest updateRequest); + /** + * Service interface for activating roles. + * Implementations of this interface should provide functionality to activate an existing role + * based on the provided id. + */ + void activate(String id); + /** * Deletes a role by its unique identifier. * diff --git a/src/main/java/org/ays/auth/service/impl/AysRoleUpdateServiceImpl.java b/src/main/java/org/ays/auth/service/impl/AysRoleUpdateServiceImpl.java index d570ac674..ce836b2df 100644 --- a/src/main/java/org/ays/auth/service/impl/AysRoleUpdateServiceImpl.java +++ b/src/main/java/org/ays/auth/service/impl/AysRoleUpdateServiceImpl.java @@ -4,6 +4,7 @@ import org.ays.auth.model.AysIdentity; import org.ays.auth.model.AysPermission; import org.ays.auth.model.AysRole; +import org.ays.auth.model.enums.AysRoleStatus; import org.ays.auth.model.request.AysRoleUpdateRequest; import org.ays.auth.port.AysPermissionReadPort; import org.ays.auth.port.AysRoleReadPort; @@ -15,6 +16,7 @@ import org.ays.auth.util.exception.AysRoleAssignedToUserException; import org.ays.auth.util.exception.AysRoleNotExistByIdException; import org.ays.auth.util.exception.AysUserNotSuperAdminException; +import org.ays.auth.util.exception.AysInvalidRoleStatusException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -65,6 +67,33 @@ public void update(final String id, roleSavePort.save(role); } + + /** + * Activates an existing role. + *

+ * This method sets the status of the role identified by its ID to active. If the role does not exist, + * an exception is thrown. Additionally, if the role's status is not {@link AysRoleStatus#PASSIVE}, + * an exception is thrown. + *

+ * + * @param id The ID of the role to activate. + * @throws AysRoleNotExistByIdException if a role with the given ID does not exist. + * @throws AysInvalidRoleStatusException if the role's current status is not {@link AysRoleStatus#PASSIVE}. + */ + @Override + public void activate(String id) { + final AysRole role = roleReadPort.findById(id) + .orElseThrow(() -> new AysRoleNotExistByIdException(id)); + + if (!role.isPassive()) { + throw new AysInvalidRoleStatusException(AysRoleStatus.PASSIVE); + } + + role.activate(); + roleSavePort.save(role); + } + + /** * Deletes an existing role identified by its ID. * @@ -91,6 +120,7 @@ public void delete(final String id) { roleSavePort.save(role); } + /** * Checks the existence of another role with the same name, excluding the current role ID. * diff --git a/src/main/java/org/ays/auth/util/exception/AysInvalidRoleStatusException.java b/src/main/java/org/ays/auth/util/exception/AysInvalidRoleStatusException.java new file mode 100644 index 000000000..c5b3c46ff --- /dev/null +++ b/src/main/java/org/ays/auth/util/exception/AysInvalidRoleStatusException.java @@ -0,0 +1,28 @@ +package org.ays.auth.util.exception; + +import org.ays.auth.model.enums.AysRoleStatus; +import org.ays.common.util.exception.AysNotExistException; + +import java.io.Serial; + +/** + * Exception to be thrown when a role's status is invalid for the requested operation. + */ +public final class AysInvalidRoleStatusException extends AysNotExistException { + + /** + * Unique identifier for serialization. + */ + @Serial + private static final long serialVersionUID = -933312733826008379L; + + /** + * Constructs a new {@link AysInvalidRoleStatusException} with the specified role status. + * + * @param status the invalid status of the role. + */ + public AysInvalidRoleStatusException(AysRoleStatus status) { + super("role status is not " + status.toString().toLowerCase() + "!"); + } + +} diff --git a/src/test/java/org/ays/auth/controller/AysRoleControllerTest.java b/src/test/java/org/ays/auth/controller/AysRoleControllerTest.java index 92df6d4f5..cf5a9bf86 100644 --- a/src/test/java/org/ays/auth/controller/AysRoleControllerTest.java +++ b/src/test/java/org/ays/auth/controller/AysRoleControllerTest.java @@ -452,6 +452,7 @@ void givenInvalidRoleCreateRequest_whenPermissionIdIsNotValid_thenReturnValidati .create(Mockito.any(AysRoleCreateRequest.class)); } + @Test void givenValidIdAndRoleUpdateRequest_whenRoleUpdated_thenReturnSuccess() throws Exception { // Given @@ -663,6 +664,61 @@ void givenValidIdAndInvalidRoleUpdateRequest_whenPermissionIdIsNotValid_thenRetu } + @Test + void givenValidId_whenRoleActivated_thenReturnSuccess() throws Exception { + + // Given + String mockId = AysRandomUtil.generateUUID(); + + // When + Mockito.doNothing() + .when(roleUpdateService) + .activate(Mockito.any()); + + // Then + String endpoint = BASE_PATH.concat("/role/".concat(mockId).concat("/activate")); + MockHttpServletRequestBuilder mockHttpServletRequestBuilder = AysMockMvcRequestBuilders + .patch(endpoint, mockSuperAdminToken.getAccessToken()); + + AysResponse mockResponse = AysResponseBuilder.SUCCESS; + + aysMockMvc.perform(mockHttpServletRequestBuilder, mockResponse) + .andExpect(AysMockResultMatchersBuilders.status() + .isOk()) + .andExpect(AysMockResultMatchersBuilders.response() + .doesNotExist()); + + // Verify + Mockito.verify(roleUpdateService, Mockito.times(1)) + .activate(Mockito.anyString()); + } + + @ParameterizedTest + @ValueSource(strings = { + "A", + "493268349068342" + }) + void givenId_whenIdIsNotValid_thenReturnValidationError(String invalidId) throws Exception { + + // Then + String endpoint = BASE_PATH.concat("/role/".concat(invalidId).concat("/activate")); + MockHttpServletRequestBuilder mockHttpServletRequestBuilder = AysMockMvcRequestBuilders + .patch(endpoint, mockSuperAdminToken.getAccessToken()); + + AysErrorResponse mockErrorResponse = AysErrorBuilder.VALIDATION_ERROR; + + aysMockMvc.perform(mockHttpServletRequestBuilder, mockErrorResponse) + .andExpect(AysMockResultMatchersBuilders.status() + .isBadRequest()) + .andExpect(AysMockResultMatchersBuilders.subErrors() + .isNotEmpty()); + + // Verify + Mockito.verify(roleUpdateService, Mockito.never()) + .activate(Mockito.anyString()); + } + + @Test void givenValidId_whenRoleDeleted_thenReturnSuccess() throws Exception { // Given diff --git a/src/test/java/org/ays/auth/controller/AysRoleEndToEndTest.java b/src/test/java/org/ays/auth/controller/AysRoleEndToEndTest.java index 21d3cb331..4ad692a7d 100644 --- a/src/test/java/org/ays/auth/controller/AysRoleEndToEndTest.java +++ b/src/test/java/org/ays/auth/controller/AysRoleEndToEndTest.java @@ -411,6 +411,51 @@ void givenValidRoleUpdateRequest_whenRoleUpdated_thenReturnSuccess() throws Exce )); } + @Test + void givenId_whenRoleActivated_thenReturnSuccess() throws Exception { + + // Initialize + Institution institution = institutionSavePort.save( + new InstitutionBuilder() + .withValidValues() + .withoutId() + .build() + ); + List permissions = permissionReadPort.findAll(); + AysRole role = roleSavePort.save( + new AysRoleBuilder() + .withValidValues() + .withoutId() + .withStatus(AysRoleStatus.PASSIVE) + .withInstitution(institution) + .withPermissions(permissions) + .build() + ); + + // Given + String id = role.getId(); + + // Then + String endpoint = BASE_PATH.concat("/role/".concat(id).concat("/activate")); + MockHttpServletRequestBuilder mockHttpServletRequestBuilder = AysMockMvcRequestBuilders + .patch(endpoint, superAdminToken.getAccessToken()); + + AysResponse mockResponse = AysResponseBuilder.SUCCESS; + + aysMockMvc.perform(mockHttpServletRequestBuilder, mockResponse) + .andExpect(AysMockResultMatchersBuilders.status() + .isOk()) + .andExpect(AysMockResultMatchersBuilders.response() + .doesNotExist()); + + // Verify + Optional roleFromDatabase = roleReadPort.findById(id); + + Assertions.assertTrue(roleFromDatabase.isPresent()); + Assertions.assertEquals(roleFromDatabase.get().getId(), id); + Assertions.assertEquals(AysRoleStatus.ACTIVE, roleFromDatabase.get().getStatus()); + } + @Test void givenValidId_whenRoleAlreadyDeleted_thenReturnSuccess() throws Exception { diff --git a/src/test/java/org/ays/auth/service/impl/AysRoleUpdateServiceImplTest.java b/src/test/java/org/ays/auth/service/impl/AysRoleUpdateServiceImplTest.java index 89e46dd81..51c44e968 100644 --- a/src/test/java/org/ays/auth/service/impl/AysRoleUpdateServiceImplTest.java +++ b/src/test/java/org/ays/auth/service/impl/AysRoleUpdateServiceImplTest.java @@ -18,9 +18,12 @@ import org.ays.auth.util.exception.AysRoleAssignedToUserException; import org.ays.auth.util.exception.AysRoleNotExistByIdException; import org.ays.auth.util.exception.AysUserNotSuperAdminException; +import org.ays.auth.util.exception.AysInvalidRoleStatusException; import org.ays.common.util.AysRandomUtil; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; @@ -364,6 +367,96 @@ void givenValidIdAndRoleUpdateRequest_whenPermissionsNotExists_thenThrowAysPermi .save(Mockito.any(AysRole.class)); } + @Test + void givenValidId_whenRoleIsPassive_thenActivateRole() { + // Given + String mockId = AysRandomUtil.generateUUID(); + + // When + AysRole mockRole = new AysRoleBuilder() + .withValidValues() + .withId(mockId) + .withStatus(AysRoleStatus.PASSIVE) + .build(); + + Mockito.when(roleReadPort.findById(Mockito.anyString())) + .thenReturn(Optional.of(mockRole)); + + Mockito.when(roleSavePort.save(Mockito.any(AysRole.class))) + .thenReturn(Mockito.mock(AysRole.class)); + + // Then + roleUpdateService.activate(mockId); + + // Verify + Mockito.verify(roleReadPort, Mockito.times(1)) + .findById(Mockito.anyString()); + + Mockito.verify(roleSavePort, Mockito.times(1)) + .save(Mockito.any(AysRole.class)); + + } + + @ParameterizedTest + @ValueSource(strings = { + "ACTIVE", + "DELETED" + }) + void givenValidId_whenRoleIsNotPassive_thenThrowAysInvalidRoleStatusException(String roleStatus) { + // Initialize + AysRoleStatus status = AysRoleStatus.valueOf(roleStatus); + + // Given + String mockId = AysRandomUtil.generateUUID(); + + // When + AysRole mockRole = new AysRoleBuilder() + .withValidValues() + .withId(mockId) + .withStatus(status) + .build(); + + Mockito.when(roleReadPort.findById(Mockito.anyString())) + .thenReturn(Optional.of(mockRole)); + + // Then + Assertions.assertThrows( + AysInvalidRoleStatusException.class, + () -> roleUpdateService.activate(mockId) + ); + + // Verify + Mockito.verify(roleReadPort, Mockito.times(1)) + .findById(Mockito.anyString()); + + Mockito.verify(roleSavePort, Mockito.never()) + .save(Mockito.any(AysRole.class)); + + } + + @Test + void givenValidId_whenRoleNotFound_thenThrowAysRoleNotExistByIdException() { + // Given + String mockId = AysRandomUtil.generateUUID(); + + // When + Mockito.when(roleReadPort.findById(Mockito.anyString())) + .thenReturn(Optional.empty()); + + // Then + Assertions.assertThrows( + AysRoleNotExistByIdException.class, + () -> roleUpdateService.activate(mockId) + ); + + // Verify + Mockito.verify(roleReadPort, Mockito.times(1)) + .findById(Mockito.anyString()); + + Mockito.verify(roleSavePort, Mockito.never()) + .save(Mockito.any(AysRole.class)); + } + @Test void givenValidId_whenRoleFound_thenDeleteRole() { diff --git a/src/test/java/org/ays/util/AysMockMvcRequestBuilders.java b/src/test/java/org/ays/util/AysMockMvcRequestBuilders.java index 1c31cdd43..a3d1e8622 100644 --- a/src/test/java/org/ays/util/AysMockMvcRequestBuilders.java +++ b/src/test/java/org/ays/util/AysMockMvcRequestBuilders.java @@ -54,6 +54,12 @@ public MockHttpServletRequestBuilder put(String endpoint, String token, Object r .contentType(MediaType.APPLICATION_JSON); } + public MockHttpServletRequestBuilder patch(String endpoint, String token) { + return MockMvcRequestBuilders.patch(endpoint) + .header(HttpHeaders.AUTHORIZATION, getTokenWithBearerPrefix(token)) + .contentType(MediaType.APPLICATION_JSON); + } + public MockHttpServletRequestBuilder delete(String endpoint, String token) { return MockMvcRequestBuilders.delete(endpoint) .header(HttpHeaders.AUTHORIZATION, getTokenWithBearerPrefix(token));