diff --git a/src/test/java/org/example/visacasemanagementsystem/TestcontainersConfiguration.java b/src/test/java/org/example/visacasemanagementsystem/TestcontainersConfiguration.java index 0ccc615..9f601bb 100644 --- a/src/test/java/org/example/visacasemanagementsystem/TestcontainersConfiguration.java +++ b/src/test/java/org/example/visacasemanagementsystem/TestcontainersConfiguration.java @@ -7,7 +7,7 @@ import org.testcontainers.utility.DockerImageName; @TestConfiguration(proxyBeanMethods = false) -class TestcontainersConfiguration { +public class TestcontainersConfiguration { @Bean @ServiceConnection diff --git a/src/test/java/org/example/visacasemanagementsystem/user/controller/UserViewControllerTest.java b/src/test/java/org/example/visacasemanagementsystem/user/controller/UserViewControllerTest.java new file mode 100644 index 0000000..a33ec67 --- /dev/null +++ b/src/test/java/org/example/visacasemanagementsystem/user/controller/UserViewControllerTest.java @@ -0,0 +1,413 @@ +package org.example.visacasemanagementsystem.user.controller; + +import org.example.visacasemanagementsystem.audit.service.AuditService; +import org.example.visacasemanagementsystem.config.SecurityConfig; +import org.example.visacasemanagementsystem.exception.UnauthorizedException; +import org.example.visacasemanagementsystem.user.UserAuthorization; +import org.example.visacasemanagementsystem.user.dto.UserDTO; +import org.example.visacasemanagementsystem.user.entity.User; +import org.example.visacasemanagementsystem.user.security.UserPrincipal; +import org.example.visacasemanagementsystem.user.service.UserService; +import org.example.visacasemanagementsystem.visa.service.VisaService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.List; +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +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(UserViewController.class) +@Import(SecurityConfig.class) +@DisplayName("UserViewController web-layer tests") +class UserViewControllerTest { + + @Autowired + MockMvc mockMvc; + + @MockitoBean + private UserDetailsService userDetailsService; + @MockitoBean + private UserService userService; + @MockitoBean + private VisaService visaService; + @MockitoBean + private AuditService auditService; + + // ── GET /user/signup ────────────────────────────────────────────────────── + + @Test + @DisplayName("Checking if GET /user/signup returns the signup view") + void userSignupForm_ShouldReturnSignupView() throws Exception { + // Act & Assert + mockMvc.perform(get("/user/signup")) + .andExpect(status().isOk()) + .andExpect(view().name("user/signup")); + } + + // ── POST /user/signup ───────────────────────────────────────────────────── + + @Test + @DisplayName("Checking if POST /user/signup with valid data redirects to the login page") + void createUser_WithValidData_ShouldRedirectToLogin() throws Exception { + // Arrange + when(userService.createUser(any())).thenReturn( + new UserDTO(1L, "New Applicant", "new@test.com", UserAuthorization.USER) + ); + + // Act & Assert + mockMvc.perform(post("/user/signup") + .param("fullName", "New Applicant") + .param("email", "new@test.com") + .param("password", "securePass1") + .with(csrf())) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/user/login")); + + verify(userService).createUser(any()); + } + + @Test + @DisplayName("Checking if POST /user/signup with a duplicate email re-renders the signup form with an error") + void createUser_WithDuplicateEmail_ShouldReturnSignupViewWithError() throws Exception { + // Arrange + when(userService.createUser(any())) + .thenThrow(new IllegalArgumentException("A user with this email already exists")); + + // Act & Assert + mockMvc.perform(post("/user/signup") + .param("fullName", "Duplicate User") + .param("email", "existing@test.com") + .param("password", "password123") + .with(csrf())) + .andExpect(status().isOk()) + .andExpect(view().name("user/signup")) + .andExpect(model().attributeExists("error")); + } + + @Test + @DisplayName("Checking if POST /user/signup with a short password re-renders the signup form with an error") + void createUser_WithShortPassword_ShouldReturnSignupViewWithError() throws Exception { + // Arrange + when(userService.createUser(any())) + .thenThrow(new IllegalArgumentException("Password must be at least 8 characters")); + + // Act & Assert + mockMvc.perform(post("/user/signup") + .param("fullName", "Short Pass") + .param("email", "short@test.com") + .param("password", "abc") + .with(csrf())) + .andExpect(status().isOk()) + .andExpect(view().name("user/signup")) + .andExpect(model().attributeExists("error")); + } + + // ── GET /user/login ─────────────────────────────────────────────────────── + + @Test + @DisplayName("Checking if GET /user/login returns the login view") + void userLoginForm_ShouldReturnLoginView() throws Exception { + // Act & Assert + mockMvc.perform(get("/user/login")) + .andExpect(status().isOk()) + .andExpect(view().name("user/login")); + } + + // ── GET /profile/view/{userId} ──────────────────────────────────────────── + + @Test + @DisplayName("Checking if GET /profile/view/{id} viewed by the profile owner shows 'canEdit = true'") + void viewProfile_AsOwnUser_ShouldReturnProfileViewWithCanEditTrue() throws Exception { + // Arrange + Long userId = 1L; + UserDTO userDTO = new UserDTO(userId, "Test User", "user@test.com", UserAuthorization.USER); + when(userService.findById(userId)).thenReturn(Optional.of(userDTO)); + + // Act & Assert + mockMvc.perform(get("/profile/view/" + userId) + .with(authentication(authFor(userId, "Test User", "user@test.com", UserAuthorization.USER)))) + .andExpect(status().isOk()) + .andExpect(view().name("profile/view")) + .andExpect(model().attributeExists("user")) + .andExpect(model().attribute("canEdit", true)); + } + + @Test + @DisplayName("Checking if GET /profile/view/{id} viewed by a SYSADMIN shows 'canEdit = true'") + void viewProfile_AsSysAdmin_ShouldReturnProfileViewWithCanEditTrue() throws Exception { + // Arrange + Long targetUserId = 1L; + Long sysAdminId = 99L; + UserDTO userDTO = new UserDTO(targetUserId, "Test User", "user@test.com", UserAuthorization.USER); + when(userService.findById(targetUserId)).thenReturn(Optional.of(userDTO)); + + // Act & Assert + mockMvc.perform(get("/profile/view/" + targetUserId) + .with(authentication(authFor(sysAdminId, "SysAdmin", "sysadmin@test.com", UserAuthorization.SYSADMIN)))) + .andExpect(status().isOk()) + .andExpect(view().name("profile/view")) + .andExpect(model().attribute("canEdit", true)); + } + + @Test + @DisplayName("Checking if GET /profile/view/{id} returns 403 when a regular user tries to view another user's profile") + void viewProfile_AsUnauthorizedUser_ShouldReturnForbidden() throws Exception { + // Arrange + Long targetUserId = 999L; + + doThrow(new UnauthorizedException("You do not have permission to edit this profile.")) + .when(userService).validateProfileAccess(any(UserPrincipal.class), eq(targetUserId)); + + UserDTO userDTO = new UserDTO(targetUserId, "Other User", "other@test.com", UserAuthorization.USER); + when(userService.findById(targetUserId)).thenReturn(Optional.of(userDTO)); + + // Act & Assert + mockMvc.perform(get("/profile/view/" + targetUserId) + .with(authentication(authFor(1L, "Test User", "user@test.com", UserAuthorization.USER)))) + .andExpect(status().isForbidden()); + } + + // ── GET /profile/edit/{userId} ──────────────────────────────────────────── + + @Test + @DisplayName("Checking if GET /profile/edit/{id} returns the edit form when accessed by the profile owner") + void showProfileEditForm_AsOwnUser_ShouldReturnEditView() throws Exception { + // Arrange + Long userId = 1L; + UserDTO userDTO = new UserDTO(userId, "Test User", "user@test.com", UserAuthorization.USER); + when(userService.findById(userId)).thenReturn(Optional.of(userDTO)); + + // Act & Assert + mockMvc.perform(get("/profile/edit/" + userId) + .with(authentication(authFor(userId, "Test User", "user@test.com", UserAuthorization.USER)))) + .andExpect(status().isOk()) + .andExpect(view().name("profile/edit")) + .andExpect(model().attributeExists("user")); + } + + @Test + @DisplayName("Checking if GET /profile/edit/{id} returns 403 when accessed by an unauthorized user") + void showProfileEditForm_AsUnauthorizedUser_ShouldReturnForbidden() throws Exception { + // Arrange + Long targetUserId = 999L; + + doThrow(new UnauthorizedException("You do not have permission to edit this profile.")) + .when(userService).validateProfileAccess(any(UserPrincipal.class), eq(targetUserId)); + + // Act & Assert + mockMvc.perform(get("/profile/edit/" + targetUserId) + .with(authentication(authFor(1L, "Test User", "user@test.com", UserAuthorization.USER)))) + .andExpect(status().isForbidden()); + } + + // ── POST /profile/edit/{userId} ─────────────────────────────────────────── + + @Test + @DisplayName("Checking if POST /profile/edit/{id} with valid data redirects to the profile view") + void updateProfile_WithValidData_ShouldRedirectToProfileView() throws Exception { + // Arrange + Long userId = 1L; + when(userService.updateUser(any())).thenReturn( + new UserDTO(userId, "Updated Name", "updated@test.com", UserAuthorization.USER) + ); + + // Act & Assert + mockMvc.perform(post("/profile/edit/" + userId) + .param("fullName", "Updated Name") + .param("email", "updated@test.com") + .with(authentication(authFor(userId, "Test User", "user@test.com", UserAuthorization.USER))) + .with(csrf())) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/profile/view/" + userId)); + + verify(userService).updateUser(any()); + } + + @Test + @DisplayName("Checking if POST /profile/edit/{id} with a duplicate email re-renders the edit form with an error") + void updateProfile_WithDuplicateEmail_ShouldReturnEditViewWithError() throws Exception { + // Arrange + Long userId = 1L; + when(userService.updateUser(any())) + .thenThrow(new IllegalArgumentException("A user with this email already exists")); + + // Act & Assert + mockMvc.perform(post("/profile/edit/" + userId) + .param("fullName", "Test User") + .param("email", "taken@test.com") + .with(authentication(authFor(userId, "Test User", "user@test.com", UserAuthorization.USER))) + .with(csrf())) + .andExpect(status().isOk()) + .andExpect(view().name("profile/edit")) + .andExpect(model().attributeExists("error")); + } + + @Test + @DisplayName("Checking if POST /profile/edit/{id} returns 403 when submitted by an unauthorized user") + void updateProfile_AsUnauthorizedUser_ShouldReturnForbidden() throws Exception { + // Arrange + Long targetUserId = 999L; + + doThrow(new UnauthorizedException("You do not have permission to edit this profile.")) + .when(userService).validateProfileAccess(any(UserPrincipal.class), eq(targetUserId)); + + // Act & Assert + mockMvc.perform(post("/profile/edit/" + targetUserId) + .param("fullName", "Hacked") + .param("email", "hacked@test.com") + .with(authentication(authFor(1L, "Test User", "user@test.com", UserAuthorization.USER))) + .with(csrf())) + .andExpect(status().isForbidden()); + } + + // ── GET /user/list ──────────────────────────────────────────────────────── + + @Test + @DisplayName("Checking if GET /user/list returns the user list view when accessed by a SYSADMIN") + void userListView_AsSysAdmin_ShouldReturnListViewWithUsers() throws Exception { + // Arrange + Long sysAdminId = 1L; + when(userService.findAll()).thenReturn(List.of( + new UserDTO(1L, "SysAdmin", "sysadmin@test.com", UserAuthorization.SYSADMIN), + new UserDTO(2L, "User", "user@test.com", UserAuthorization.USER) + )); + + // Act & Assert + mockMvc.perform(get("/user/list") + .with(authentication(authFor(sysAdminId, "SysAdmin", "sysadmin@test.com", UserAuthorization.SYSADMIN)))) + .andExpect(status().isOk()) + .andExpect(view().name("user/list")) + .andExpect(model().attributeExists("users")) + .andExpect(model().attributeExists("name")); + } + + @Test + @WithMockUser + @DisplayName("Checking if GET /user/list returns 403 when accessed by a non-SYSADMIN") + void userListView_AsNonSysAdmin_ShouldReturnForbidden() throws Exception { + // Act & Assert — @PreAuthorize("hasRole('SYSADMIN')") rejects USER + + mockMvc.perform(get("/user/list") + .with(authentication(authFor(1L, "Test User", "user@test.com", UserAuthorization.USER)))) + .andExpect(status().isForbidden()); + } + + // ── GET /dashboard/applicant ────────────────────────────────────────────── + + @Test + @DisplayName("Checking if GET /dashboard/applicant returns the applicant dashboard with their visa list") + void applicantDashboard_AsUser_ShouldReturnDashboardWithVisas() throws Exception { + // Arrange + Long userId = 1L; + when(visaService.findVisasByApplicantId(userId)).thenReturn(List.of()); + + // Act & Assert + mockMvc.perform(get("/dashboard/applicant") + .with(authentication(authFor(userId, "Test User", "user@test.com", UserAuthorization.USER)))) + .andExpect(status().isOk()) + .andExpect(view().name("dashboard/applicant")) + .andExpect(model().attributeExists("visas")) + .andExpect(model().attributeExists("name")); + + verify(visaService).findVisasByApplicantId(userId); + } + + // ── GET /dashboard/admin ────────────────────────────────────────────────── + + @Test + @DisplayName("Checking if GET /dashboard/admin returns assigned and unassigned cases for an ADMIN") + void adminDashboard_AsAdmin_ShouldReturnDashboardWithCases() throws Exception { + // Arrange + Long adminId = 1L; + when(visaService.findVisasByHandlerId(adminId)).thenReturn(List.of()); + when(visaService.findVisaByStatus("SUBMITTED")).thenReturn(List.of()); + + // Act & Assert + mockMvc.perform(get("/dashboard/admin") + .with(authentication(authFor(adminId, "Test Admin", "admin@test.com", UserAuthorization.ADMIN)))) + .andExpect(status().isOk()) + .andExpect(view().name("dashboard/admin")) + .andExpect(model().attributeExists("assignedCases")) + .andExpect(model().attributeExists("unassignedCases")) + .andExpect(model().attributeExists("name")); + } + + @Test + @DisplayName("Checking if GET /dashboard/admin returns 403 when accessed by a regular USER") + void adminDashboard_AsRegularUser_ShouldReturnForbidden() throws Exception { + // Act & Assert — @PreAuthorize("hasRole('ADMIN')") rejects USER + mockMvc.perform(get("/dashboard/admin") + .with(authentication(authFor(1L, "Test User", "user@test.com", UserAuthorization.USER)))) + .andExpect(status().isForbidden()); + } + + // ── GET /dashboard/sysadmin ─────────────────────────────────────────────── + + @Test + @DisplayName("Checking if GET /dashboard/sysadmin returns users and audit logs for a SYSADMIN") + void sysAdminDashboard_AsSysAdmin_ShouldReturnDashboardWithUsersAndLogs() throws Exception { + // Arrange + Long sysAdminId = 1L; + when(userService.findAll()).thenReturn(List.of()); + when(auditService.findAll()).thenReturn(List.of()); + + // Act & Assert + mockMvc.perform(get("/dashboard/sysadmin") + .with(authentication(authFor(sysAdminId, "SysAdmin", "sysadmin@test.com", UserAuthorization.SYSADMIN)))) + .andExpect(status().isOk()) + .andExpect(view().name("dashboard/sysadmin")) + .andExpect(model().attributeExists("users")) + .andExpect(model().attributeExists("auditLogs")) + .andExpect(model().attributeExists("name")); + } + + @Test + @DisplayName("Checking if GET /dashboard/sysadmin returns 403 when accessed by an ADMIN") + void sysAdminDashboard_AsAdmin_ShouldReturnForbidden() throws Exception { + // Act & Assert — @PreAuthorize("hasRole('SYSADMIN')") rejects ADMIN + mockMvc.perform(get("/dashboard/sysadmin") + .with(authentication(authFor(1L, "Test Admin", "admin@test.com", UserAuthorization.ADMIN)))) + .andExpect(status().isForbidden()); + } + + @Test + @DisplayName("Checking if GET /dashboard/sysadmin returns 403 when accessed by a regular USER") + void sysAdminDashboard_AsRegularUser_ShouldReturnForbidden() throws Exception { + // Act & Assert — @PreAuthorize("hasRole('SYSADMIN')") rejects USER + mockMvc.perform(get("/dashboard/sysadmin") + .with(authentication(authFor(1L, "Test User", "user@test.com", UserAuthorization.USER)))) + .andExpect(status().isForbidden()); + } + + // ── Helper methods ──────────────────────────────────────────────────────── + + + private Authentication authFor(Long id, String fullName, String email, UserAuthorization auth) { + User user = new User(); + user.setId(id); + user.setFullName(fullName); + user.setEmail(email); + user.setUsername(email); + user.setPassword("password"); + user.setUserAuthorization(auth); + UserPrincipal principal = new UserPrincipal(user); + return new UsernamePasswordAuthenticationToken(principal, null, principal.getAuthorities()); + } +} \ No newline at end of file diff --git a/src/test/java/org/example/visacasemanagementsystem/user/mapper/UserMapperTest.java b/src/test/java/org/example/visacasemanagementsystem/user/mapper/UserMapperTest.java new file mode 100644 index 0000000..a7494c6 --- /dev/null +++ b/src/test/java/org/example/visacasemanagementsystem/user/mapper/UserMapperTest.java @@ -0,0 +1,172 @@ +package org.example.visacasemanagementsystem.user.mapper; + +import org.example.visacasemanagementsystem.user.UserAuthorization; +import org.example.visacasemanagementsystem.user.dto.CreateUserDTO; +import org.example.visacasemanagementsystem.user.dto.UpdateUserDTO; +import org.example.visacasemanagementsystem.user.dto.UserDTO; +import org.example.visacasemanagementsystem.user.entity.User; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +@DisplayName("UserMapper unit tests") +class UserMapperTest { + + private UserMapper userMapper; + + @BeforeEach + void setUp() { + userMapper = new UserMapper(); + } + + @Test + @DisplayName("Checking if toDTO maps a User entity to a UserDTO with all fields preserved") + void shouldMapUserEntityToUserDTO() { + // Arrange + User user = new User(); + user.setFullName("Alice Wonderland"); + user.setEmail("alice@example.com"); + user.setUsername("alice@example.com"); + user.setPassword("password123"); + user.setUserAuthorization(UserAuthorization.USER); + + // Act + UserDTO dto = userMapper.toDTO(user); + + // Assert + assertThat(dto).isNotNull(); + assertThat(dto.fullName()).isEqualTo("Alice Wonderland"); + assertThat(dto.email()).isEqualTo("alice@example.com"); + assertThat(dto.userAuthorization()).isEqualTo(UserAuthorization.USER); + } + + @Test + @DisplayName("Checking if toDTO returns null when the input User is null") + void shouldReturnNullDTO_WhenUserIsNull() { + // Act & Assert + assertThat(userMapper.toDTO(null)).isNull(); + } + + @Test + @DisplayName("Checking if toEntity maps a CreateUserDTO to a User entity with correct name, email, username, and authorization") + void shouldMapCreateUserDTOToUserEntity() { + // Arrange + CreateUserDTO dto = new CreateUserDTO( + "Bob Builder", + "bob@example.com", + "password123", + UserAuthorization.ADMIN + ); + + // Act + User user = userMapper.toEntity(dto); + + // Assert + assertThat(user).isNotNull(); + assertThat(user.getFullName()).isEqualTo("Bob Builder"); + assertThat(user.getEmail()).isEqualTo("bob@example.com"); + assertThat(user.getUsername()).isEqualTo("bob@example.com"); + assertThat(user.getUserAuthorization()).isEqualTo(UserAuthorization.ADMIN); + } + + @Test + @DisplayName("Checking if toEntity does not set the password field (UserService handles passwords separately)") + void shouldNotSetPassword_WhenMappingFromCreateUserDTO() { + // Arrange + CreateUserDTO dto = new CreateUserDTO( + "No Pass", + "nopass@example.com", + "supersecret", + UserAuthorization.USER + ); + + // Act + User user = userMapper.toEntity(dto); + + // Assert + assertThat(user).isNotNull(); + assertThat(user.getPassword()).isNull(); + } + + @Test + @DisplayName("Checking if toEntity returns null when the input CreateUserDTO is null") + void shouldReturnNullEntity_WhenCreateUserDTOIsNull() { + // Act & Assert + assertThat(userMapper.toEntity(null)).isNull(); + } + + @Test + @DisplayName("Checking if updateEntityFromDTO overwrites fullName, email, and username on an existing User entity") + void shouldUpdateExistingUserEntityFromUpdateUserDTO() { + // Arrange + User user = new User(); + user.setFullName("Old Name"); + user.setEmail("old@example.com"); + user.setUsername("old@example.com"); + user.setPassword("password123"); + user.setUserAuthorization(UserAuthorization.USER); + + UpdateUserDTO dto = new UpdateUserDTO(1L, "New Name", "new@example.com"); + + // Act + userMapper.updateEntityFromDTO(dto, user); + + // Assert + assertThat(user.getFullName()).isEqualTo("New Name"); + assertThat(user.getEmail()).isEqualTo("new@example.com"); + assertThat(user.getUsername()).isEqualTo("new@example.com"); + } + + @Test + @DisplayName("Checking if updateEntityFromDTO does not modify authorization or password") + void shouldNotModifyAuthorizationOrPassword_WhenUpdatingFromDTO() { + // Arrange + User user = new User(); + user.setFullName("Original"); + user.setEmail("original@example.com"); + user.setUsername("original@example.com"); + user.setPassword("secret123"); + user.setUserAuthorization(UserAuthorization.SYSADMIN); + + UpdateUserDTO dto = new UpdateUserDTO(1L, "Changed Name", "changed@example.com"); + + // Act + userMapper.updateEntityFromDTO(dto, user); + + // Assert + assertThat(user.getUserAuthorization()).isEqualTo(UserAuthorization.SYSADMIN); + assertThat(user.getPassword()).isEqualTo("secret123"); + } + + @Test + @DisplayName("Checking if updateEntityFromDTO leaves the entity unchanged when the DTO is null") + void shouldNotUpdateEntity_WhenUpdateDTOIsNull() { + // Arrange + User user = new User(); + user.setFullName("Original Name"); + user.setEmail("original@example.com"); + user.setUsername("original@example.com"); + + // Act + userMapper.updateEntityFromDTO(null, user); + + // Assert + assertThat(user.getFullName()).isEqualTo("Original Name"); + assertThat(user.getEmail()).isEqualTo("original@example.com"); + assertThat(user.getUsername()).isEqualTo("original@example.com"); + } + + @Test + @DisplayName("Checking if updateEntityFromDTO does not throw when the User entity is null") + void shouldNotThrow_WhenUserIsNullInUpdate() { + // Arrange + UpdateUserDTO dto = new UpdateUserDTO(1L, "Some Name", "some@example.com"); + + // Act & Assert + assertThatCode(() -> userMapper.updateEntityFromDTO(dto, null)) + .doesNotThrowAnyException(); + } +} diff --git a/src/test/java/org/example/visacasemanagementsystem/user/service/UserServiceIntegrationTest.java b/src/test/java/org/example/visacasemanagementsystem/user/service/UserServiceIntegrationTest.java new file mode 100644 index 0000000..d7d5546 --- /dev/null +++ b/src/test/java/org/example/visacasemanagementsystem/user/service/UserServiceIntegrationTest.java @@ -0,0 +1,213 @@ +package org.example.visacasemanagementsystem.user.service; + +import org.example.visacasemanagementsystem.exception.UnauthorizedException; +import org.example.visacasemanagementsystem.user.UserAuthorization; +import org.example.visacasemanagementsystem.user.dto.CreateUserDTO; +import org.example.visacasemanagementsystem.user.dto.UpdateUserDTO; +import org.example.visacasemanagementsystem.user.dto.UserDTO; +import org.example.visacasemanagementsystem.user.entity.User; +import org.example.visacasemanagementsystem.user.repository.UserRepository; +import org.example.visacasemanagementsystem.user.security.UserPrincipal; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +@SpringBootTest +@Transactional +@ActiveProfiles("test") +@DisplayName("UserService integration tests (H2)") +class UserServiceIntegrationTest { + + @Autowired + private UserService userService; + + @Autowired + private UserRepository userRepository; + + @Test + @DisplayName("Checking if createUser persists and returns the new user when all input is valid") + void createUser_shouldSaveUser_WhenDataIsValid() { + // Arrange + CreateUserDTO dto = new CreateUserDTO( + "New Applicant", + "newapplicant@integration.test", + "securePass1", + UserAuthorization.USER + ); + + // Act + UserDTO result = userService.createUser(dto); + + // Assert + assertThat(result).isNotNull(); + assertThat(result.id()).isNotNull(); + assertThat(result.fullName()).isEqualTo("New Applicant"); + assertThat(result.email()).isEqualTo("newapplicant@integration.test"); + + var savedUsers = userRepository.findAll(); + assertThat(savedUsers).anyMatch(u -> u.getEmail().equals("newapplicant@integration.test")); + } + + @Test + @DisplayName("Checking if updateUser changes fullName and email in the database") + void updateUser_shouldUpdateUserFields_WhenDataIsValid() { + // Arrange + User user = createAndSaveValidUser(); + UpdateUserDTO dto = new UpdateUserDTO(user.getId(), "Updated Name", "updated@integration.test"); + + // Act + UserDTO result = userService.updateUser(dto); + + // Assert + assertThat(result.fullName()).isEqualTo("Updated Name"); + assertThat(result.email()).isEqualTo("updated@integration.test"); + + User updatedUser = userRepository.findById(user.getId()).orElseThrow(); + assertThat(updatedUser.getFullName()).isEqualTo("Updated Name"); + assertThat(updatedUser.getEmail()).isEqualTo("updated@integration.test"); + } + + @Test + @WithMockUser(roles = "SYSADMIN") + @DisplayName("Checking if updateUserAuthorization promotes a USER to ADMIN when requested by a SYSADMIN") + void updateUserAuthorization_shouldChangeAuthorization_WhenRequestedBySysAdmin() { + // Arrange + User targetUser = createAndSaveValidUser(); + + // Act + UserDTO result = userService.updateUserAuthorization( + targetUser.getId(), UserAuthorization.ADMIN + ); + + // Assert + assertThat(result.userAuthorization()).isEqualTo(UserAuthorization.ADMIN); + + User updated = userRepository.findById(targetUser.getId()).orElseThrow(); + assertThat(updated.getUserAuthorization()).isEqualTo(UserAuthorization.ADMIN); + } + + @Test + @WithMockUser(roles = "SYSADMIN") + @DisplayName("Checking if deleteUser removes the user row from the database when requested by a SYSADMIN") + void deleteUser_shouldRemoveUser_WhenRequestedBySysAdmin() { + // Arrange + User targetUser = createAndSaveValidUser(); + Long targetId = targetUser.getId(); + + // Act + userService.deleteUser(targetId); + + // Assert + assertThat(userRepository.findById(targetId)).isEmpty(); + } + + @Test + @DisplayName("Checking if findAll returns a list that includes every seeded user") + void findAll_shouldReturnAllUsers() { + // Arrange + createAndSaveValidUser(); + createAndSaveUser("Admin", UserAuthorization.ADMIN); + + // Act + var result = userService.findAll(); + + // Assert + assertThat(result).hasSizeGreaterThanOrEqualTo(2); + } + + @Test + @DisplayName("Checking if findById returns the correct user when the ID exists") + void findById_shouldReturnUser_WhenUserExists() { + // Arrange + User user = createAndSaveValidUser(); + + // Act + var result = userService.findById(user.getId()); + + // Assert + assertThat(result).isPresent(); + assertThat(result.get().fullName()).isEqualTo("Test User"); + } + + @Test + @DisplayName("Checking if findByEmail returns the correct user when the email exists") + void findByEmail_shouldReturnUser_WhenEmailExists() { + // Arrange + User user = createAndSaveValidUser(); + + // Act + var result = userService.findByEmail(user.getEmail()); + + // Assert + assertThat(result).isPresent(); + assertThat(result.get().id()).isEqualTo(user.getId()); + } + + @Test + @DisplayName("Checking if validateProfileAccess allows a user to view their own profile") + void validateProfileAccess_shouldNotThrow_WhenAccessingOwnProfile() { + // Arrange + User user = createAndSaveValidUser(); + UserPrincipal principal = new UserPrincipal(user); + + // Act & Assert — should complete without exception + userService.validateProfileAccess(principal, user.getId()); + } + + @Test + @DisplayName("Checking if validateProfileAccess allows a SYSADMIN to view any profile") + void validateProfileAccess_shouldNotThrow_WhenSysAdminAccessesOtherProfile() { + // Arrange + User sysAdmin = createAndSaveUser("SysAdmin", UserAuthorization.SYSADMIN); + User otherUser = createAndSaveValidUser(); + UserPrincipal principal = new UserPrincipal(sysAdmin); + + // Act & Assert — should complete without exception + userService.validateProfileAccess(principal, otherUser.getId()); + } + + @Test + @DisplayName("Checking if validateProfileAccess rejects a regular user viewing another user's profile") + void validateProfileAccess_shouldThrowUnauthorizedException_WhenUserAccessesOtherProfile() { + // Arrange + User user = createAndSaveValidUser(); + User otherUser = createAndSaveUser("Other", UserAuthorization.ADMIN); + UserPrincipal principal = new UserPrincipal(user); + + // Act & Assert + Long otherUserId = otherUser.getId(); + assertThatExceptionOfType(UnauthorizedException.class) + .isThrownBy(() -> userService.validateProfileAccess(principal, otherUserId)); + } + + // ── Helper methods ──────────────────────────────────────────────────────── + + private User createAndSaveValidUser() { + User user = new User(); + String uniqueEmail = java.util.UUID.randomUUID() + "@test.com"; + user.setFullName("Test User"); + user.setEmail(uniqueEmail); + user.setUsername(uniqueEmail); + user.setPassword("password123"); + user.setUserAuthorization(UserAuthorization.USER); + return userRepository.save(user); + } + + private User createAndSaveUser(String name, UserAuthorization auth) { + User user = new User(); + String uniqueEmail = java.util.UUID.randomUUID() + "@test.com"; + user.setFullName(name); + user.setEmail(uniqueEmail); + user.setUsername(uniqueEmail); + user.setPassword("password123"); + user.setUserAuthorization(auth); + return userRepository.save(user); + } +} diff --git a/src/test/java/org/example/visacasemanagementsystem/user/service/UserServiceTest.java b/src/test/java/org/example/visacasemanagementsystem/user/service/UserServiceTest.java new file mode 100644 index 0000000..10e3620 --- /dev/null +++ b/src/test/java/org/example/visacasemanagementsystem/user/service/UserServiceTest.java @@ -0,0 +1,294 @@ +package org.example.visacasemanagementsystem.user.service; + +import jakarta.persistence.EntityNotFoundException; +import org.example.visacasemanagementsystem.exception.UnauthorizedException; +import org.example.visacasemanagementsystem.user.UserAuthorization; +import org.example.visacasemanagementsystem.user.dto.CreateUserDTO; +import org.example.visacasemanagementsystem.user.dto.UpdateUserDTO; +import org.example.visacasemanagementsystem.user.dto.UserDTO; +import org.example.visacasemanagementsystem.user.entity.User; +import org.example.visacasemanagementsystem.user.mapper.UserMapper; +import org.example.visacasemanagementsystem.user.repository.UserRepository; +import org.example.visacasemanagementsystem.user.security.UserPrincipal; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("UserService unit tests") +class UserServiceTest { + + @Mock + private UserRepository userRepository; + @Mock + private UserMapper userMapper; + @Mock + private PasswordEncoder passwordEncoder; + + @InjectMocks + private UserService userService; + + // ── createUser ──────────────────────────────────────────────────────────── + + @Test + @DisplayName("Checking if createUser throws IllegalArgumentException when password is shorter than 8 characters") + void createUser_shouldThrowIllegalArgumentException_WhenPasswordIsTooShort() { + // Arrange + CreateUserDTO dto = new CreateUserDTO( + "Short Pass", "short@test.com", "abc", UserAuthorization.USER + ); + + when(userMapper.toEntity(dto)).thenReturn(new User()); + + // Act & Assert + assertThatThrownBy(() -> userService.createUser(dto)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("8 characters"); + + // Verify — repository should never be called + verifyNoInteractions(userRepository); + } + + @Test + @DisplayName("Checking if createUser throws IllegalArgumentException when email is already taken") + void createUser_shouldThrowIllegalArgumentException_WhenEmailAlreadyExists() { + // Arrange + CreateUserDTO dto = new CreateUserDTO( + "Duplicate", "existing@test.com", "password123", UserAuthorization.USER + ); + + User mappedUser = new User(); + when(userMapper.toEntity(dto)).thenReturn(mappedUser); + when(passwordEncoder.encode("password123")).thenReturn("encodedPassword"); + when(userRepository.save(any(User.class))) + .thenThrow(new DataIntegrityViolationException("unique constraint")); + + // Act & Assert + assertThatThrownBy(() -> userService.createUser(dto)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("email already exists"); + } + + @Test + @DisplayName("Checking if createUser persists and returns a UserDTO when input is valid") + void createUser_shouldSaveAndReturnUserDTO_WhenDataIsValid() { + // Arrange + CreateUserDTO dto = new CreateUserDTO( + "New User", "new@test.com", "password123", UserAuthorization.SYSADMIN + ); + + User mappedUser = new User(); + User savedUser = new User(); + savedUser.setId(1L); + UserDTO expectedDTO = new UserDTO(1L, "New User", "new@test.com", UserAuthorization.USER); + + when(userMapper.toEntity(dto)).thenReturn(mappedUser); + when(passwordEncoder.encode("password123")).thenReturn("encodedPassword"); + when(userRepository.save(mappedUser)).thenReturn(savedUser); + when(userMapper.toDTO(savedUser)).thenReturn(expectedDTO); + + // Act + UserDTO result = userService.createUser(dto); + + // Assert + assertThat(result).isEqualTo(expectedDTO); + assertThat(mappedUser.getPassword()).isEqualTo("encodedPassword"); + assertThat(mappedUser.getUserAuthorization()).isEqualTo(UserAuthorization.USER); + verify(passwordEncoder).encode("password123"); + verify(userRepository).save(mappedUser); + } + + // ── updateUser ──────────────────────────────────────────────────────────── + + @Test + @DisplayName("Checking if updateUser throws EntityNotFoundException when user ID does not exist") + void updateUser_shouldThrowEntityNotFoundException_WhenUserDoesNotExist() { + // Arrange + UpdateUserDTO dto = new UpdateUserDTO(999L, "Name", "email@test.com"); + when(userRepository.findById(999L)).thenReturn(Optional.empty()); + + // Act & Assert + assertThatThrownBy(() -> userService.updateUser(dto)) + .isInstanceOf(EntityNotFoundException.class) + .hasMessage("User not found"); + } + + @Test + @DisplayName("Checking if updateUser throws IllegalArgumentException when new email is already taken by another user") + void updateUser_shouldThrowIllegalArgumentException_WhenEmailAlreadyExists() { + // Arrange + Long userId = 1L; + UpdateUserDTO dto = new UpdateUserDTO(userId, "User", "taken@test.com"); + + User existingUser = new User(); + existingUser.setId(userId); + + when(userRepository.findById(userId)).thenReturn(Optional.of(existingUser)); + when(userRepository.saveAndFlush(any(User.class))) + .thenThrow(new DataIntegrityViolationException("unique constraint")); + + // Act & Assert + assertThatThrownBy(() -> userService.updateUser(dto)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("email already exists"); + } + + @Test + @DisplayName("Checking if updateUser updates fields and returns the updated UserDTO") + void updateUser_shouldUpdateAndReturnUser_WhenDataIsValid() { + // Arrange + Long userId = 1L; + UpdateUserDTO dto = new UpdateUserDTO(userId, "Updated Name", "updated@test.com"); + + User existingUser = new User(); + existingUser.setId(userId); + UserDTO expectedDTO = new UserDTO(userId, "Updated Name", "updated@test.com", UserAuthorization.USER); + + when(userRepository.findById(userId)).thenReturn(Optional.of(existingUser)); + when(userRepository.saveAndFlush(any(User.class))).thenReturn(existingUser); + when(userMapper.toDTO(existingUser)).thenReturn(expectedDTO); + + // Act + UserDTO result = userService.updateUser(dto); + + // Assert + assertThat(result).isEqualTo(expectedDTO); + verify(userMapper).updateEntityFromDTO(dto, existingUser); + verify(userRepository).saveAndFlush(existingUser); + } + + // ── updateUserAuthorization ─────────────────────────────────────────────── + + @Test + @DisplayName("Checking if updateUserAuthorization throws EntityNotFoundException when target user does not exist") + void updateUserAuthorization_shouldThrowEntityNotFoundException_WhenTargetUserDoesNotExist() { + // Arrange + Long nonExistingUserId = 999L; + + when(userRepository.findById(nonExistingUserId)).thenReturn(Optional.empty()); + + // Act & Assert + assertThatThrownBy(() -> userService.updateUserAuthorization(nonExistingUserId, UserAuthorization.ADMIN)) + .isInstanceOf(EntityNotFoundException.class) + .hasMessage("User not found"); + } + + @Test + @DisplayName("Checking if updateUserAuthorization changes role and returns updated UserDTO") + void updateUserAuthorization_shouldChangeAuthorization_WhenUserExists() { + // Arrange + Long userId = 1L; + + User targetUser = new User(); + targetUser.setId(userId); + targetUser.setUserAuthorization(UserAuthorization.USER); + + UserDTO expectedDTO = new UserDTO(userId, "User", "user@test.com", UserAuthorization.ADMIN); + + when(userRepository.findById(userId)).thenReturn(Optional.of(targetUser)); + when(userRepository.save(any(User.class))).thenReturn(targetUser); + when(userMapper.toDTO(targetUser)).thenReturn(expectedDTO); + + // Act + UserDTO result = userService.updateUserAuthorization(userId, UserAuthorization.ADMIN); + + // Assert + assertThat(result).isEqualTo(expectedDTO); + assertThat(targetUser.getUserAuthorization()).isEqualTo(UserAuthorization.ADMIN); + assertThat(result.userAuthorization()).isEqualTo(UserAuthorization.ADMIN); + verify(userRepository).save(targetUser); + } + + // ── deleteUser ──────────────────────────────────────────────────────────── + + @Test + @DisplayName("Checking if deleteUser throws EntityNotFoundException when target user does not exist") + void deleteUser_shouldThrowEntityNotFoundException_WhenTargetUserDoesNotExist() { + // Arrange + Long nonExistingUserId = 999L; + + when(userRepository.findById(nonExistingUserId)).thenReturn(Optional.empty()); + + // Act & Assert + assertThatThrownBy(() -> userService.deleteUser(nonExistingUserId)) + .isInstanceOf(EntityNotFoundException.class) + .hasMessage("User not found"); + } + + @Test + @DisplayName("Checking if deleteUser removes the user when the user exists") + void deleteUser_shouldDeleteUser_WhenUserExists() { + // Arrange + Long userId = 1L; + + User targetUser = new User(); + targetUser.setId(userId); + + when(userRepository.findById(userId)).thenReturn(Optional.of(targetUser)); + + // Act + userService.deleteUser(userId); + + // Assert + verify(userRepository, times(1)).delete(targetUser); + } + + // ── validateProfileAccess ───────────────────────────────────────────────── + + @Test + @DisplayName("Checking if validateProfileAccess throws UnauthorizedException when a regular user accesses another user's profile") + void validateProfileAccess_shouldThrowUnauthorizedException_WhenUserAccessesOtherProfile() { + // Arrange + User user = new User(); + user.setId(1L); + user.setFullName("Regular User"); + user.setEmail("user@test.com"); + user.setUsername("user@test.com"); + user.setPassword("password"); + user.setUserAuthorization(UserAuthorization.USER); + UserPrincipal principal = new UserPrincipal(user); + + Long otherUserId = 999L; + + // Act & Assert + assertThatThrownBy(() -> userService.validateProfileAccess(principal, otherUserId)) + .isInstanceOf(UnauthorizedException.class) + .hasMessageContaining("permission"); + } + + // ── findById ────────────────────────────────────────────────────────────── + + @Test + @DisplayName("Checking if findById returns empty Optional when user ID does not exist") + void findById_shouldReturnEmpty_WhenUserDoesNotExist() { + // Arrange + when(userRepository.findById(999L)).thenReturn(Optional.empty()); + + // Act & Assert + assertThat(userService.findById(999L)).isEmpty(); + } + + // ── findByEmail ─────────────────────────────────────────────────────────── + + @Test + @DisplayName("Checking if findByEmail returns empty Optional when email does not exist") + void findByEmail_shouldReturnEmpty_WhenEmailDoesNotExist() { + // Arrange + when(userRepository.findByEmail("nobody@test.com")).thenReturn(Optional.empty()); + + // Act & Assert + assertThat(userService.findByEmail("nobody@test.com")).isEmpty(); + } +} + \ No newline at end of file