diff --git a/src/main/java/org/example/visacasemanagementsystem/comment/controller/CommentController.java b/src/main/java/org/example/visacasemanagementsystem/comment/controller/CommentController.java index 79de96e..7c17fc2 100644 --- a/src/main/java/org/example/visacasemanagementsystem/comment/controller/CommentController.java +++ b/src/main/java/org/example/visacasemanagementsystem/comment/controller/CommentController.java @@ -4,12 +4,16 @@ import org.example.visacasemanagementsystem.comment.dto.CommentDTO; import org.example.visacasemanagementsystem.comment.dto.CreateCommentDTO; import org.example.visacasemanagementsystem.comment.service.CommentService; +import org.example.visacasemanagementsystem.user.security.UserPrincipal; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import java.util.List; +@PreAuthorize("isAuthenticated()") @RestController @RequestMapping("/api/comments") public class CommentController { @@ -22,8 +26,8 @@ public CommentController(CommentService commentService) { // Create new comment @PostMapping - public ResponseEntity createComment(@Valid @RequestBody CreateCommentDTO dto){ - CommentDTO createComment = commentService.createComment(dto); + public ResponseEntity createComment(@Valid @RequestBody CreateCommentDTO dto, @AuthenticationPrincipal UserPrincipal principal){ + CommentDTO createComment = commentService.createComment(dto, principal.getUserId()); return new ResponseEntity<>(createComment, HttpStatus.CREATED); } diff --git a/src/main/java/org/example/visacasemanagementsystem/comment/dto/CommentDTO.java b/src/main/java/org/example/visacasemanagementsystem/comment/dto/CommentDTO.java index 60a2126..d314324 100644 --- a/src/main/java/org/example/visacasemanagementsystem/comment/dto/CommentDTO.java +++ b/src/main/java/org/example/visacasemanagementsystem/comment/dto/CommentDTO.java @@ -3,7 +3,6 @@ import java.time.LocalDateTime; public record CommentDTO( - Long id, Long visaId, String authorName, String text, diff --git a/src/main/java/org/example/visacasemanagementsystem/comment/dto/CreateCommentDTO.java b/src/main/java/org/example/visacasemanagementsystem/comment/dto/CreateCommentDTO.java index 50fde2c..2f93ea4 100644 --- a/src/main/java/org/example/visacasemanagementsystem/comment/dto/CreateCommentDTO.java +++ b/src/main/java/org/example/visacasemanagementsystem/comment/dto/CreateCommentDTO.java @@ -5,7 +5,6 @@ public record CreateCommentDTO( @NotNull(message = "Visa ID is required") Long visaId, - @NotNull(message = "Author ID is required") Long authorId, // Todo: Remove this and let Spring Security handle it @NotBlank(message = "Comment text cannot be empty") String text ) { } diff --git a/src/main/java/org/example/visacasemanagementsystem/comment/mapper/CommentMapper.java b/src/main/java/org/example/visacasemanagementsystem/comment/mapper/CommentMapper.java index 4782f90..4f0b6e4 100644 --- a/src/main/java/org/example/visacasemanagementsystem/comment/mapper/CommentMapper.java +++ b/src/main/java/org/example/visacasemanagementsystem/comment/mapper/CommentMapper.java @@ -13,7 +13,6 @@ public CommentDTO toDTO(Comment comment) { if (comment == null) return null; return new CommentDTO( - comment.getId(), comment.getVisa() != null ? comment.getVisa().getId() : null, comment.getAuthor() != null ? comment.getAuthor().getFullName() : "System", // autofallback comment.getText(), diff --git a/src/main/java/org/example/visacasemanagementsystem/comment/service/CommentService.java b/src/main/java/org/example/visacasemanagementsystem/comment/service/CommentService.java index b75e8dc..1fdda9c 100644 --- a/src/main/java/org/example/visacasemanagementsystem/comment/service/CommentService.java +++ b/src/main/java/org/example/visacasemanagementsystem/comment/service/CommentService.java @@ -36,10 +36,8 @@ public CommentService(CommentRepository commentRepository, CommentMapper comment // Create Comment - // TODO: Security Risk - Replace authorId from DTO with authenticated user from - // SecurityContext once Spring Security is integrated to prevent IDOR vulnerabilities. @Transactional - public CommentDTO createComment(CreateCommentDTO dto) { + public CommentDTO createComment(CreateCommentDTO dto, Long userId) { if (dto == null) { throw new IllegalArgumentException("Comment payload cannot be null"); } @@ -49,8 +47,8 @@ public CommentDTO createComment(CreateCommentDTO dto) { } // Get User and Visa from database - User author = userRepository.findById(dto.authorId()) - .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + dto.authorId())); + User author = userRepository.findById(userId) + .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + userId)); Visa visa = visaRepository.findById(dto.visaId()) .orElseThrow(() -> new ResourceNotFoundException("Visa case not found with id: " + dto.visaId())); diff --git a/src/test/java/org/example/visacasemanagementsystem/comment/CommentControllerTest.java b/src/test/java/org/example/visacasemanagementsystem/comment/CommentControllerTest.java index 07ef7c4..cd12a9c 100644 --- a/src/test/java/org/example/visacasemanagementsystem/comment/CommentControllerTest.java +++ b/src/test/java/org/example/visacasemanagementsystem/comment/CommentControllerTest.java @@ -4,10 +4,16 @@ import org.example.visacasemanagementsystem.comment.dto.CommentDTO; import org.example.visacasemanagementsystem.comment.dto.CreateCommentDTO; import org.example.visacasemanagementsystem.comment.service.CommentService; +import org.example.visacasemanagementsystem.user.UserAuthorization; +import org.example.visacasemanagementsystem.user.entity.User; +import org.example.visacasemanagementsystem.user.security.UserPrincipal; import org.springframework.http.MediaType; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; @@ -41,10 +47,12 @@ class CommentControllerTest { @WithMockUser void createComment_shouldReturnCreated() throws Exception { // Arrange - CreateCommentDTO createDto= new CreateCommentDTO(1L, 1L, "Test message"); - CommentDTO responseDto = new CommentDTO(100L, 1L,"Test User", "Test message", LocalDateTime.now()); + CreateCommentDTO createDto= new CreateCommentDTO(1L, "Test message"); + CommentDTO responseDto = new CommentDTO(1L,"Test User", "Test message", LocalDateTime.now()); - when(commentService.createComment(any(CreateCommentDTO.class))).thenReturn(responseDto); + authenticateTestUser(); + + when(commentService.createComment(any(CreateCommentDTO.class), any(Long.class))).thenReturn(responseDto); // Act & Assert mockMvc.perform(post("/api/comments") @@ -52,9 +60,12 @@ void createComment_shouldReturnCreated() throws Exception { .contentType((MediaType.APPLICATION_JSON)) .content(objectMapper.writeValueAsString(createDto))) .andExpect(status().isCreated()) - .andExpect(jsonPath("$.id").value(100L)) + .andExpect(jsonPath("$.visaId").value(1L)) .andExpect(jsonPath("$.text").value("Test message")) .andExpect(jsonPath("$.authorName").value("Test User")); + + // Cleanup + SecurityContextHolder.clearContext(); } @Test @@ -62,7 +73,7 @@ void createComment_shouldReturnCreated() throws Exception { void getCommentsByVisa_ShouldReturnList() throws Exception { // Arrange String expectedText = "Hello World"; - CommentDTO comment = new CommentDTO(1L, 2L, "Admin", expectedText, LocalDateTime.now()); + CommentDTO comment = new CommentDTO(1L, "Admin", expectedText, LocalDateTime.now()); when(commentService.getCommentsByVisaId(1L)).thenReturn(List.of(comment)); @@ -72,4 +83,17 @@ void getCommentsByVisa_ShouldReturnList() throws Exception { .andExpect(jsonPath("$.length()").value(1)) .andExpect(jsonPath("$[0].text").value(expectedText)); } + + // Helper method + private static void authenticateTestUser() { + User testUser = new User(); + testUser.setId(100L); + testUser.setUsername("test@test.com"); + testUser.setEmail("test@test.com"); + testUser.setPassword("password123"); + testUser.setUserAuthorization(UserAuthorization.USER); + UserPrincipal principal = new UserPrincipal(testUser); + Authentication authentication = new TestingAuthenticationToken(principal, "password123", principal.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authentication); + } } diff --git a/src/test/java/org/example/visacasemanagementsystem/comment/CommentMapperTest.java b/src/test/java/org/example/visacasemanagementsystem/comment/CommentMapperTest.java index 1af124f..04ff787 100644 --- a/src/test/java/org/example/visacasemanagementsystem/comment/CommentMapperTest.java +++ b/src/test/java/org/example/visacasemanagementsystem/comment/CommentMapperTest.java @@ -27,7 +27,6 @@ void shouldMapCreateCommentDTOtoEntity() { // Arrange CreateCommentDTO dto = new CreateCommentDTO( 100L, - 50L, "This is a comment text." ); @@ -79,7 +78,6 @@ void shouldMapCommentEntityToCommentDTO() { CommentDTO result = commentMapper.toDTO(comment); // Assert - assertThat(result.id()).isEqualTo(1L); assertThat(result.text()).isEqualTo("This is a comment text."); assertThat(result.visaId()).isEqualTo(100L); assertThat(result.authorName()).isEqualTo("Test User"); diff --git a/src/test/java/org/example/visacasemanagementsystem/comment/CommentServiceIntegrationTest.java b/src/test/java/org/example/visacasemanagementsystem/comment/CommentServiceIntegrationTest.java index 8bc7662..8c2fbce 100644 --- a/src/test/java/org/example/visacasemanagementsystem/comment/CommentServiceIntegrationTest.java +++ b/src/test/java/org/example/visacasemanagementsystem/comment/CommentServiceIntegrationTest.java @@ -7,14 +7,19 @@ import org.example.visacasemanagementsystem.user.UserAuthorization; import org.example.visacasemanagementsystem.user.entity.User; import org.example.visacasemanagementsystem.user.repository.UserRepository; +import org.example.visacasemanagementsystem.user.security.UserPrincipal; import org.example.visacasemanagementsystem.visa.VisaStatus; import org.example.visacasemanagementsystem.visa.VisaType; import org.example.visacasemanagementsystem.visa.entity.Visa; import org.example.visacasemanagementsystem.visa.repository.VisaRepository; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; @@ -27,7 +32,7 @@ @SpringBootTest @Transactional @ActiveProfiles("test") -class CommentServiceIntegrationTest { +class CommentServiceIntegrationTest{ @Autowired private CommentService commentService; @@ -37,6 +42,7 @@ class CommentServiceIntegrationTest { private UserRepository userRepository; private User testUser; + private User nonExistentUser; private Visa testVisa; @BeforeEach @@ -50,6 +56,15 @@ void setUp() { testUser.setUserAuthorization(UserAuthorization.USER); testUser = userRepository.save(testUser); + // Create nonexistent user + nonExistentUser = new User(); + nonExistentUser.setId(Long.MAX_VALUE); + nonExistentUser.setFullName("NonUser"); + nonExistentUser.setEmail("non@example.com"); + nonExistentUser.setUsername("non@example.com"); + nonExistentUser.setPassword("password123"); + nonExistentUser.setUserAuthorization(UserAuthorization.USER); + // Create test visa testVisa = new Visa(); testVisa.setApplicant(testUser); @@ -62,51 +77,56 @@ void setUp() { } + @AfterEach + void tearDown() { + SecurityContextHolder.clearContext(); + } + @Test void createComment_shouldSaveAndRetrieveComment() { // Arrange CreateCommentDTO dto = new CreateCommentDTO( testVisa.getId(), - testUser.getId(), "This is a test comment" ); // Act - CommentDTO savedComment = commentService.createComment(dto); + authenticateTestUser(); + CommentDTO savedComment = commentService.createComment(dto, testUser.getId()); // Assert assertThat(savedComment).isNotNull(); - assertThat(savedComment.id()).isNotNull(); assertThat(savedComment.text()).isEqualTo("This is a test comment"); List comments = commentService.getCommentsByVisaId(testVisa.getId()); assertThat(comments).isNotEmpty(); assertThat(comments).hasSize(1); - assertThat(comments.get(0).text()).isEqualTo("This is a test comment"); + assertThat(comments.getFirst().text()).isEqualTo("This is a test comment"); } @Test void createComment_shouldThrowException_WhenUserDoesNotExist() { // Arrange - Long nonExistentUserId = 999L; - CreateCommentDTO dto = new CreateCommentDTO(testVisa.getId(), nonExistentUserId, "Valid comment text"); + CreateCommentDTO dto = new CreateCommentDTO(testVisa.getId(), "Valid comment text"); // Act & Assert - assertThatThrownBy(() -> commentService.createComment(dto)) + authenticateNonExistentUser(); + assertThatThrownBy(() -> commentService.createComment(dto, nonExistentUser.getId())) .isInstanceOf(ResourceNotFoundException.class) - .hasMessageContaining("User not found with id: " + nonExistentUserId); + .hasMessageContaining("User not found with id: " + nonExistentUser.getId()); } @Test void createComment_shouldThrowException_WhenVisaDoesNotExist() { // Arrange - Long nonExistentVisaId = 999L; - CreateCommentDTO dto = new CreateCommentDTO(nonExistentVisaId, testUser.getId(), "Some text"); + long nonExistentVisaId = 999L; + CreateCommentDTO dto = new CreateCommentDTO(nonExistentVisaId, "Some text"); // Act & Assert - assertThatThrownBy(() -> commentService.createComment(dto)) + authenticateTestUser(); + assertThatThrownBy(() -> commentService.createComment(dto, testUser.getId())) .isInstanceOf(ResourceNotFoundException.class) .hasMessageContaining("Visa case not found with id: " + nonExistentVisaId); } @@ -114,10 +134,10 @@ void createComment_shouldThrowException_WhenVisaDoesNotExist() { @Test void createComment_shouldThrowException_WhenTextIsBlank() { // Arrange - CreateCommentDTO dto = new CreateCommentDTO(testVisa.getId(), testUser.getId(), " "); + CreateCommentDTO dto = new CreateCommentDTO(testVisa.getId(), " "); // Act & Assert - assertThatThrownBy(() -> commentService.createComment(dto)) + assertThatThrownBy(() -> commentService.createComment(dto, testUser.getId())) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("Comment text cannot be empty"); } @@ -147,4 +167,17 @@ void getCommentsByVisaID_ShouldReturnEmptyList_WhenVisaExistsButHasNoComments() assertThat(comments).isEmpty(); } + + // Helper method + private void authenticateTestUser() { + UserPrincipal principal = new UserPrincipal(testUser); + Authentication authentication = new TestingAuthenticationToken(principal, "password123", principal.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + private void authenticateNonExistentUser() { + UserPrincipal principal = new UserPrincipal(nonExistentUser); + Authentication authentication = new TestingAuthenticationToken(principal, "password123", principal.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authentication); + } } diff --git a/src/test/java/org/example/visacasemanagementsystem/visa/VisaViewControllerTest.java b/src/test/java/org/example/visacasemanagementsystem/visa/VisaViewControllerTest.java index 94c89bb..a6228d3 100644 --- a/src/test/java/org/example/visacasemanagementsystem/visa/VisaViewControllerTest.java +++ b/src/test/java/org/example/visacasemanagementsystem/visa/VisaViewControllerTest.java @@ -13,6 +13,7 @@ import org.example.visacasemanagementsystem.visa.dto.UpdateVisaDTO; import org.example.visacasemanagementsystem.visa.dto.VisaDTO; import org.example.visacasemanagementsystem.visa.service.VisaService; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; @@ -49,6 +50,11 @@ class VisaViewControllerTest { @MockitoBean private CommentService commentService; + @AfterEach + void tearDown() { + SecurityContextHolder.clearContext(); + } + @Test @WithMockUser void showDashboard_AsAdmin_ShouldReturnAllVisas() throws Exception { @@ -412,7 +418,7 @@ void viewDetails_AsCurrentUser_ShouldReturnDetailViewWithComments() throws Excep when(visaService.findVisaDtoById(visaId)).thenReturn(mockVisa); - var mockComments = List.of(new CommentDTO(1L,100L,"Admin", "Looks good!", LocalDateTime.now())); + var mockComments = List.of(new CommentDTO(100L,"Admin", "Looks good!", LocalDateTime.now())); when(commentService.getCommentsByVisaId(visaId)).thenReturn(mockComments); // Act