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
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -22,8 +26,8 @@ public CommentController(CommentService commentService) {

// Create new comment
@PostMapping
public ResponseEntity<CommentDTO> createComment(@Valid @RequestBody CreateCommentDTO dto){
CommentDTO createComment = commentService.createComment(dto);
public ResponseEntity<CommentDTO> createComment(@Valid @RequestBody CreateCommentDTO dto, @AuthenticationPrincipal UserPrincipal principal){
CommentDTO createComment = commentService.createComment(dto, principal.getUserId());
return new ResponseEntity<>(createComment, HttpStatus.CREATED);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import java.time.LocalDateTime;

public record CommentDTO(
Long id,
Long visaId,
String authorName,
String text,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand All @@ -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()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -41,28 +47,33 @@ 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")
.with(csrf())
.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
@WithMockUser
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));

Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ void shouldMapCreateCommentDTOtoEntity() {
// Arrange
CreateCommentDTO dto = new CreateCommentDTO(
100L,
50L,
"This is a comment text."

);
Expand Down Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -27,7 +32,7 @@
@SpringBootTest
@Transactional
@ActiveProfiles("test")
class CommentServiceIntegrationTest {
class CommentServiceIntegrationTest{

@Autowired
private CommentService commentService;
Expand All @@ -37,6 +42,7 @@ class CommentServiceIntegrationTest {
private UserRepository userRepository;

private User testUser;
private User nonExistentUser;
private Visa testVisa;

@BeforeEach
Expand All @@ -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);
Expand All @@ -62,62 +77,67 @@ 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<CommentDTO> 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);
}

@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");
}
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -49,6 +50,11 @@ class VisaViewControllerTest {
@MockitoBean
private CommentService commentService;

@AfterEach
void tearDown() {
SecurityContextHolder.clearContext();
}

@Test
@WithMockUser
void showDashboard_AsAdmin_ShouldReturnAllVisas() throws Exception {
Expand Down Expand Up @@ -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
Expand Down