Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/main/java/org/nkcoder/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
60 changes: 60 additions & 0 deletions src/main/java/org/nkcoder/controller/AdminUserController.java
Original file line number Diff line number Diff line change
@@ -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<ApiResponse<UserResponse>> 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<ApiResponse<UserResponse>> 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<ApiResponse<Void>> 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"));
}
}
37 changes: 0 additions & 37 deletions src/main/java/org/nkcoder/controller/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -57,40 +56,4 @@ public ResponseEntity<ApiResponse<Void>> changeMyPassword(

return ResponseEntity.ok(ApiResponse.success("Password changed successfully"));
}

// Admin endpoints
@GetMapping("/{userId}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<ApiResponse<UserResponse>> 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<ApiResponse<UserResponse>> 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<ApiResponse<Void>> 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"));
}
}
3 changes: 3 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ spring:
docker:
compose:
enabled: false
threads:
virtual:
enabled: true

# JWT Configuration
jwt:
Expand Down
147 changes: 147 additions & 0 deletions src/test/java/org/nkcoder/controller/AdminUserControllerTest.java
Original file line number Diff line number Diff line change
@@ -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());
}
}
}
88 changes: 0 additions & 88 deletions src/test/java/org/nkcoder/controller/UserControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
"""
Expand Down
Loading