|
|
@@ -0,0 +1,258 @@
|
|
|
+package com.danielbohry.authservice.api;
|
|
|
+
|
|
|
+import com.danielbohry.authservice.api.dto.AuthenticationResponse;
|
|
|
+import com.danielbohry.authservice.api.dto.PasswordChangeRequest;
|
|
|
+import com.danielbohry.authservice.domain.ApplicationUser;
|
|
|
+import com.danielbohry.authservice.domain.Role;
|
|
|
+import com.danielbohry.authservice.service.auth.AuthService;
|
|
|
+import tools.jackson.databind.ObjectMapper;
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
|
|
+import org.springframework.boot.test.mock.mockito.MockBean;
|
|
|
+import org.springframework.http.MediaType;
|
|
|
+import org.springframework.test.web.servlet.MockMvc;
|
|
|
+
|
|
|
+import java.time.Instant;
|
|
|
+import java.util.List;
|
|
|
+
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
+import static org.mockito.ArgumentMatchers.anyString;
|
|
|
+import static org.mockito.Mockito.verify;
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
|
|
|
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
|
|
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
|
|
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
|
|
+
|
|
|
+@WebMvcTest(UserController.class)
|
|
|
+class UserControllerTest {
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private MockMvc mockMvc;
|
|
|
+
|
|
|
+ @MockBean
|
|
|
+ private AuthService authService;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private ObjectMapper objectMapper;
|
|
|
+
|
|
|
+ private ApplicationUser testUser;
|
|
|
+ private ApplicationUser adminUser;
|
|
|
+ private PasswordChangeRequest passwordChangeRequest;
|
|
|
+ private AuthenticationResponse authResponse;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ void setUp() {
|
|
|
+ testUser = ApplicationUser.builder()
|
|
|
+ .id("user-id-123")
|
|
|
+ .username("testuser")
|
|
|
+ .password("encodedPassword")
|
|
|
+ .email("[email protected]")
|
|
|
+ .roles(List.of(Role.USER))
|
|
|
+ .active(true)
|
|
|
+ .build();
|
|
|
+
|
|
|
+ adminUser = ApplicationUser.builder()
|
|
|
+ .id("admin-id-123")
|
|
|
+ .username("admin")
|
|
|
+ .password("encodedPassword")
|
|
|
+ .email("[email protected]")
|
|
|
+ .roles(List.of(Role.ADMIN, Role.USER))
|
|
|
+ .active(true)
|
|
|
+ .build();
|
|
|
+
|
|
|
+ passwordChangeRequest = new PasswordChangeRequest("oldPassword", "newPassword");
|
|
|
+
|
|
|
+ authResponse = AuthenticationResponse.builder()
|
|
|
+ .id("user-id-123")
|
|
|
+ .username("testuser")
|
|
|
+ .token("new-jwt-token-123")
|
|
|
+ .expirationDate(Instant.now().plusSeconds(3600))
|
|
|
+ .roles(List.of("ROLE_USER"))
|
|
|
+ .build();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void shouldReturnCurrentUserInfo() throws Exception {
|
|
|
+ // when/then
|
|
|
+ mockMvc.perform(get("/api/users/current")
|
|
|
+ .with(user(testUser)))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
|
|
+ .andExpect(jsonPath("$.id").value("user-id-123"))
|
|
|
+ .andExpect(jsonPath("$.username").value("testuser"))
|
|
|
+ .andExpect(jsonPath("$.roles[0]").value("USER"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void shouldReturnCurrentUserInfoForAdminWithMultipleRoles() throws Exception {
|
|
|
+ // when/then
|
|
|
+ mockMvc.perform(get("/api/users/current")
|
|
|
+ .with(user(adminUser)))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
|
|
+ .andExpect(jsonPath("$.id").value("admin-id-123"))
|
|
|
+ .andExpect(jsonPath("$.username").value("admin"))
|
|
|
+ .andExpect(jsonPath("$.roles").isArray())
|
|
|
+ .andExpect(jsonPath("$.roles.length()").value(2));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void shouldReturnForbiddenWhenNotAuthenticated() throws Exception {
|
|
|
+ // when/then - no authenticated user
|
|
|
+ mockMvc.perform(get("/api/users/current"))
|
|
|
+ .andExpect(status().isUnauthorized());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void shouldChangePasswordSuccessfully() throws Exception {
|
|
|
+ // given
|
|
|
+ when(authService.changePassword(anyString(), anyString(), anyString()))
|
|
|
+ .thenReturn(authResponse);
|
|
|
+
|
|
|
+ // when/then
|
|
|
+ mockMvc.perform(post("/api/users/change-password")
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
+ .content(objectMapper.writeValueAsString(passwordChangeRequest))
|
|
|
+ .with(user(testUser)))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
|
|
+ .andExpect(jsonPath("$.id").value("user-id-123"))
|
|
|
+ .andExpect(jsonPath("$.username").value("testuser"))
|
|
|
+ .andExpect(jsonPath("$.token").value("new-jwt-token-123"));
|
|
|
+
|
|
|
+ verify(authService).changePassword("user-id-123", "oldPassword", "newPassword");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void shouldRejectPasswordChangeWhenNotAuthenticated() throws Exception {
|
|
|
+ // when/then - no authenticated user
|
|
|
+ mockMvc.perform(post("/api/users/change-password")
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
+ .content(objectMapper.writeValueAsString(passwordChangeRequest)))
|
|
|
+ .andExpect(status().isUnauthorized());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void shouldReturnUnauthorizedForPasswordChangeWhenPrincipalNotApplicationUser() throws Exception {
|
|
|
+ // when/then - mock principal that's not an ApplicationUser instance
|
|
|
+ mockMvc.perform(post("/api/users/change-password")
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
+ .content(objectMapper.writeValueAsString(passwordChangeRequest))
|
|
|
+ .with(user("plainstring"))) // String principal instead of ApplicationUser
|
|
|
+ .andExpect(status().isUnauthorized());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void shouldHandlePasswordChangeServiceError() throws Exception {
|
|
|
+ // given
|
|
|
+ when(authService.changePassword(anyString(), anyString(), anyString()))
|
|
|
+ .thenThrow(new RuntimeException("Service error"));
|
|
|
+
|
|
|
+ // when/then
|
|
|
+ mockMvc.perform(post("/api/users/change-password")
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
+ .content(objectMapper.writeValueAsString(passwordChangeRequest))
|
|
|
+ .with(user(testUser)))
|
|
|
+ .andExpect(status().isInternalServerError());
|
|
|
+
|
|
|
+ verify(authService).changePassword("user-id-123", "oldPassword", "newPassword");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void shouldLogPasswordChangeAttempts() throws Exception {
|
|
|
+ // given
|
|
|
+ when(authService.changePassword(anyString(), anyString(), anyString()))
|
|
|
+ .thenReturn(authResponse);
|
|
|
+
|
|
|
+ // when/then - This tests that the controller method executes (logging happens inside)
|
|
|
+ mockMvc.perform(post("/api/users/change-password")
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
+ .content(objectMapper.writeValueAsString(passwordChangeRequest))
|
|
|
+ .with(user(testUser)))
|
|
|
+ .andExpect(status().isOk());
|
|
|
+
|
|
|
+ verify(authService).changePassword("user-id-123", "oldPassword", "newPassword");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void shouldReturnForbiddenForCurrentUserWhenPrincipalNotApplicationUser() throws Exception {
|
|
|
+ // when/then - mock principal that's not an ApplicationUser instance
|
|
|
+ mockMvc.perform(get("/api/users/current")
|
|
|
+ .with(user("plainstring"))) // String principal instead of ApplicationUser
|
|
|
+ .andExpect(status().isForbidden());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void shouldHandleCorsRequests() throws Exception {
|
|
|
+ // when/then - Test that CORS is enabled
|
|
|
+ mockMvc.perform(get("/api/users/current")
|
|
|
+ .header("Origin", "http://localhost:3000")
|
|
|
+ .with(user(testUser)))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(header().string("Access-Control-Allow-Origin", "*"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void shouldReturnCorrectContentTypeForUserResponse() throws Exception {
|
|
|
+ // when/then
|
|
|
+ mockMvc.perform(get("/api/users/current")
|
|
|
+ .with(user(testUser)))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
|
|
+ .andExpect(jsonPath("$.id").exists())
|
|
|
+ .andExpect(jsonPath("$.username").exists())
|
|
|
+ .andExpect(jsonPath("$.roles").exists());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void shouldIncludeAllUserRolesInResponse() throws Exception {
|
|
|
+ // given
|
|
|
+ ApplicationUser multiRoleUser = ApplicationUser.builder()
|
|
|
+ .id("user-id-123")
|
|
|
+ .username("multirole")
|
|
|
+ .password("password")
|
|
|
+ .roles(List.of(Role.ADMIN, Role.SERVICE, Role.VPN, Role.USER))
|
|
|
+ .active(true)
|
|
|
+ .build();
|
|
|
+
|
|
|
+ // when/then
|
|
|
+ mockMvc.perform(get("/api/users/current")
|
|
|
+ .with(user(multiRoleUser)))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.roles").isArray())
|
|
|
+ .andExpect(jsonPath("$.roles.length()").value(4))
|
|
|
+ .andExpect(jsonPath("$.roles[?(@=='ADMIN')]").exists())
|
|
|
+ .andExpect(jsonPath("$.roles[?(@=='SERVICE')]").exists())
|
|
|
+ .andExpect(jsonPath("$.roles[?(@=='VPN')]").exists())
|
|
|
+ .andExpect(jsonPath("$.roles[?(@=='USER')]").exists());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void shouldReturnNewTokenAfterPasswordChange() throws Exception {
|
|
|
+ // given - response with new token and updated expiration
|
|
|
+ AuthenticationResponse newTokenResponse = AuthenticationResponse.builder()
|
|
|
+ .id("user-id-123")
|
|
|
+ .username("testuser")
|
|
|
+ .token("brand-new-jwt-token")
|
|
|
+ .expirationDate(Instant.now().plusSeconds(7200)) // Different expiration
|
|
|
+ .roles(List.of("ROLE_USER"))
|
|
|
+ .build();
|
|
|
+
|
|
|
+ when(authService.changePassword(anyString(), anyString(), anyString()))
|
|
|
+ .thenReturn(newTokenResponse);
|
|
|
+
|
|
|
+ // when/then
|
|
|
+ mockMvc.perform(post("/api/users/change-password")
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
+ .content(objectMapper.writeValueAsString(passwordChangeRequest))
|
|
|
+ .with(user(testUser)))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.token").value("brand-new-jwt-token"))
|
|
|
+ .andExpect(jsonPath("$.expirationDate").exists());
|
|
|
+
|
|
|
+ verify(authService).changePassword("user-id-123", "oldPassword", "newPassword");
|
|
|
+ }
|
|
|
+}
|