diff --git a/src/main/java/org/nkcoder/config/SecurityConfig.java b/src/main/java/org/nkcoder/config/SecurityConfig.java index e9b6d7a..90e6b0e 100644 --- a/src/main/java/org/nkcoder/config/SecurityConfig.java +++ b/src/main/java/org/nkcoder/config/SecurityConfig.java @@ -82,9 +82,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // Protected endpoints .requestMatchers("/api/users/me", "/api/users/me/**") .authenticated() - .requestMatchers("/api/users/{userId}") - .hasRole("ADMIN") - .requestMatchers("/api/users/{userId}/**") + + // Admin endpoints + .requestMatchers("/api/admin/**") .hasRole("ADMIN") // All other requests require authentication diff --git a/src/main/java/org/nkcoder/controller/AdminUserController.java b/src/main/java/org/nkcoder/controller/AdminUserController.java new file mode 100644 index 0000000..25f810f --- /dev/null +++ b/src/main/java/org/nkcoder/controller/AdminUserController.java @@ -0,0 +1,60 @@ +package org.nkcoder.controller; + +import jakarta.validation.Valid; +import java.util.UUID; +import org.nkcoder.dto.common.ApiResponse; +import org.nkcoder.dto.user.ChangePasswordRequest; +import org.nkcoder.dto.user.UpdateProfileRequest; +import org.nkcoder.dto.user.UserResponse; +import org.nkcoder.service.UserService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/admin/users") +@PreAuthorize("hasRole('ADMIN')") +public class AdminUserController { + + private static final Logger logger = LoggerFactory.getLogger(AdminUserController.class); + + private final UserService userService; + + public AdminUserController(UserService userService) { + this.userService = userService; + } + + @GetMapping("/{userId}") + public ResponseEntity> getUser(@PathVariable UUID userId) { + logger.info("Admin get user request for userId: {}", userId); + + UserResponse userResponse = userService.findById(userId); + + return ResponseEntity.ok(ApiResponse.success("User retrieved successfully", userResponse)); + } + + @PatchMapping("/{userId}") + public ResponseEntity> updateUser( + @PathVariable UUID userId, @Valid @RequestBody UpdateProfileRequest request) { + + logger.info("Admin update user request for userId: {}", userId); + + UserResponse userResponse = userService.updateProfile(userId, request); + + return ResponseEntity.ok(ApiResponse.success("User updated successfully", userResponse)); + } + + @PatchMapping("/{userId}/password") + public ResponseEntity> changeUserPassword( + @PathVariable UUID userId, @Valid @RequestBody ChangePasswordRequest request) { + + logger.info("Admin change password request for userId: {}", userId); + + // For admin, we only use the newPassword field + userService.changeUserPassword(userId, request.newPassword()); + + return ResponseEntity.ok(ApiResponse.success("Password changed successfully")); + } +} diff --git a/src/main/java/org/nkcoder/controller/UserController.java b/src/main/java/org/nkcoder/controller/UserController.java index 96a2a6e..9f35848 100644 --- a/src/main/java/org/nkcoder/controller/UserController.java +++ b/src/main/java/org/nkcoder/controller/UserController.java @@ -11,7 +11,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; @RestController @@ -57,40 +56,4 @@ public ResponseEntity> changeMyPassword( return ResponseEntity.ok(ApiResponse.success("Password changed successfully")); } - - // Admin endpoints - @GetMapping("/{userId}") - @PreAuthorize("hasRole('ADMIN')") - public ResponseEntity> getUser(@PathVariable UUID userId) { - logger.info("Admin get user request for userId: {}", userId); - - UserResponse userResponse = userService.findById(userId); - - return ResponseEntity.ok(ApiResponse.success("User retrieved successfully", userResponse)); - } - - @PatchMapping("/{userId}") - @PreAuthorize("hasRole('ADMIN')") - public ResponseEntity> updateUser( - @PathVariable UUID userId, @Valid @RequestBody UpdateProfileRequest request) { - - logger.info("Admin update user request for userId: {}", userId); - - UserResponse userResponse = userService.updateProfile(userId, request); - - return ResponseEntity.ok(ApiResponse.success("User updated successfully", userResponse)); - } - - @PatchMapping("/{userId}/password") - @PreAuthorize("hasRole('ADMIN')") - public ResponseEntity> changeUserPassword( - @PathVariable UUID userId, @Valid @RequestBody ChangePasswordRequest request) { - - logger.info("Admin change password request for userId: {}", userId); - - // For admin, we only use the newPassword field - userService.changeUserPassword(userId, request.newPassword()); - - return ResponseEntity.ok(ApiResponse.success("Password changed successfully")); - } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 3c252f2..6a87094 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -46,6 +46,9 @@ spring: docker: compose: enabled: false + threads: + virtual: + enabled: true # JWT Configuration jwt: diff --git a/src/test/java/org/nkcoder/controller/AdminUserControllerTest.java b/src/test/java/org/nkcoder/controller/AdminUserControllerTest.java new file mode 100644 index 0000000..e84aa87 --- /dev/null +++ b/src/test/java/org/nkcoder/controller/AdminUserControllerTest.java @@ -0,0 +1,147 @@ +package org.nkcoder.controller; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.time.LocalDateTime; +import java.util.UUID; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.nkcoder.dto.user.ChangePasswordRequest; +import org.nkcoder.dto.user.UpdateProfileRequest; +import org.nkcoder.dto.user.UserResponse; +import org.nkcoder.enums.Role; +import org.nkcoder.security.JwtAuthenticationFilter; +import org.nkcoder.service.UserService; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +@DisplayName("AdminUserController tests") +@WebMvcTest( + controllers = AdminUserController.class, + excludeAutoConfiguration = {SecurityAutoConfiguration.class}, + excludeFilters = { + @ComponentScan.Filter( + type = FilterType.ASSIGNABLE_TYPE, + classes = {JwtAuthenticationFilter.class}) + }) +class AdminUserControllerTest extends BaseControllerTest { + + @MockitoBean private UserService userService; + + private final UUID testUserId = UUID.randomUUID(); + private final String testEmail = "test@example.com"; + + private UserResponse createTestUserResponse(UUID userId, String email, String name) { + return new UserResponse( + userId, + email, + name, + Role.MEMBER, + true, + LocalDateTime.now(), + LocalDateTime.now(), + LocalDateTime.now()); + } + + @Nested + @DisplayName("Admin User Management Tests") + @WithMockUser(roles = "ADMIN") + class AdminUserManagementTests { + + @Test + @DisplayName("Should get user by ID as admin") + void shouldGetUserByIdAsAdmin() throws Exception { + // Given + UserResponse userResponse = createTestUserResponse(testUserId, testEmail, "Admin User"); + given(userService.findById(testUserId)).willReturn(userResponse); + + // When & Then + mockMvc + .perform(get("/api/admin/users/{userId}", testUserId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("User retrieved successfully")) + .andExpect(jsonPath("$.data.id").value(testUserId.toString())) + .andExpect(jsonPath("$.data.email").value(testEmail)); + + verify(userService).findById(testUserId); + } + + @Test + @DisplayName("Should update user as admin") + void shouldUpdateUserAsAdmin() throws Exception { + // Given + UpdateProfileRequest request = new UpdateProfileRequest("admin@example.com", "Admin Updated"); + UserResponse updatedResponse = + createTestUserResponse(testUserId, "admin@example.com", "Admin Updated"); + + given(userService.updateProfile(eq(testUserId), any(UpdateProfileRequest.class))) + .willReturn(updatedResponse); + + // When & Then + mockMvc + .perform( + patch("/api/admin/users/{userId}", testUserId) + .contentType(MediaType.APPLICATION_JSON) + .content(toJson(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("User updated successfully")) + .andExpect(jsonPath("$.data.email").value("admin@example.com")) + .andExpect(jsonPath("$.data.name").value("Admin Updated")); + + verify(userService).updateProfile(eq(testUserId), any(UpdateProfileRequest.class)); + } + + @Test + @DisplayName("Should change user password as admin") + void shouldChangeUserPasswordAsAdmin() throws Exception { + // Given + ChangePasswordRequest request = + new ChangePasswordRequest( + "OldPassword123!", + "NewAdminPassword123!", + "NewAdminPassword123!"); // Only newPassword is used for admin + doNothing().when(userService).changeUserPassword(testUserId, "NewAdminPassword123!"); + + // When & Then + mockMvc + .perform( + patch("/api/admin/users/{userId}/password", testUserId) + .contentType(MediaType.APPLICATION_JSON) + .content(toJson(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("Password changed successfully")); + + verify(userService).changeUserPassword(testUserId, "NewAdminPassword123!"); + } + + @Test + @DisplayName("Should return 400 when admin update request is invalid") + void shouldReturnBadRequestWhenAdminUpdateRequestIsInvalid() throws Exception { + // Given + UpdateProfileRequest request = + new UpdateProfileRequest("invalid-email", ""); // Invalid email and empty name + + // When & Then + mockMvc + .perform( + patch("/api/admin/users/{userId}", testUserId) + .contentType(MediaType.APPLICATION_JSON) + .content(toJson(request))) + .andExpect(status().isBadRequest()); + } + } +} diff --git a/src/test/java/org/nkcoder/controller/UserControllerTest.java b/src/test/java/org/nkcoder/controller/UserControllerTest.java index 7b9c130..cd7a617 100644 --- a/src/test/java/org/nkcoder/controller/UserControllerTest.java +++ b/src/test/java/org/nkcoder/controller/UserControllerTest.java @@ -174,92 +174,4 @@ void shouldReturnBadRequestWhenChangePasswordRequestIsInvalid() throws Exception .andExpect(status().isBadRequest()); } } - - @Nested - @DisplayName("Admin User Management Tests") - @WithMockUser(roles = "ADMIN") - class AdminUserManagementTests { - - @Test - @DisplayName("Should get user by ID as admin") - void shouldGetUserByIdAsAdmin() throws Exception { - // Given - UserResponse userResponse = createTestUserResponse(testUserId, testEmail, "Admin User"); - given(userService.findById(testUserId)).willReturn(userResponse); - - // When & Then - mockMvc - .perform(get("/api/users/{userId}", testUserId)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value("User retrieved successfully")) - .andExpect(jsonPath("$.data.id").value(testUserId.toString())) - .andExpect(jsonPath("$.data.email").value(testEmail)); - - verify(userService).findById(testUserId); - } - - @Test - @DisplayName("Should update user as admin") - void shouldUpdateUserAsAdmin() throws Exception { - // Given - UpdateProfileRequest request = new UpdateProfileRequest("admin@example.com", "Admin Updated"); - UserResponse updatedResponse = - createTestUserResponse(testUserId, "admin@example.com", "Admin Updated"); - - given(userService.updateProfile(eq(testUserId), any(UpdateProfileRequest.class))) - .willReturn(updatedResponse); - - // When & Then - mockMvc - .perform( - patch("/api/users/{userId}", testUserId) - .contentType(MediaType.APPLICATION_JSON) - .content(toJson(request))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value("User updated successfully")) - .andExpect(jsonPath("$.data.email").value("admin@example.com")) - .andExpect(jsonPath("$.data.name").value("Admin Updated")); - - verify(userService).updateProfile(eq(testUserId), any(UpdateProfileRequest.class)); - } - - @Test - @DisplayName("Should change user password as admin") - void shouldChangeUserPasswordAsAdmin() throws Exception { - // Given - ChangePasswordRequest request = - new ChangePasswordRequest( - "OldPassword123!", - "NewAdminPassword123!", - "NewAdminPassword123!"); // Only newPassword is used for admin - doNothing().when(userService).changeUserPassword(testUserId, "NewAdminPassword123!"); - - // When & Then - mockMvc - .perform( - patch("/api/users/{userId}/password", testUserId) - .contentType(MediaType.APPLICATION_JSON) - .content(toJson(request))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value("Password changed successfully")); - - verify(userService).changeUserPassword(testUserId, "NewAdminPassword123!"); - } - - @Test - @DisplayName("Should return 400 when admin update request is invalid") - void shouldReturnBadRequestWhenAdminUpdateRequestIsInvalid() throws Exception { - // Given - UpdateProfileRequest request = - new UpdateProfileRequest("invalid-email", ""); // Invalid email and empty name - - // When & Then - mockMvc - .perform( - patch("/api/users/{userId}", testUserId) - .contentType(MediaType.APPLICATION_JSON) - .content(toJson(request))) - .andExpect(status().isBadRequest()); - } - } } diff --git a/src/test/java/org/nkcoder/integration/AuthControllerIntegrationTest.java b/src/test/java/org/nkcoder/integration/AuthControllerIntegrationTest.java index f56ec22..78dec5a 100644 --- a/src/test/java/org/nkcoder/integration/AuthControllerIntegrationTest.java +++ b/src/test/java/org/nkcoder/integration/AuthControllerIntegrationTest.java @@ -157,19 +157,19 @@ void changePasswordRequiresAuth() throws Exception { class AdminEndpoints { @Test - @DisplayName("GET /api/users/{id} returns 401 without token") + @DisplayName("GET /api/admin/users/{id} returns 401 without token") void getUserByIdRequiresAuth() throws Exception { mockMvc - .perform(get("/api/users/{userId}", UUID.randomUUID())) + .perform(get("/api/admin/users/{userId}", UUID.randomUUID())) .andExpect(status().isUnauthorized()); } @Test - @DisplayName("PATCH /api/users/{id} returns 401 without token") + @DisplayName("PATCH /api/admin/users/{id} returns 401 without token") void updateUserRequiresAuth() throws Exception { mockMvc .perform( - patch("/api/users/{userId}", UUID.randomUUID()) + patch("/api/admin/users/{userId}", UUID.randomUUID()) .contentType(MediaType.APPLICATION_JSON) .content( """