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 65e4139..4782f90 100644 --- a/src/main/java/org/example/visacasemanagementsystem/comment/mapper/CommentMapper.java +++ b/src/main/java/org/example/visacasemanagementsystem/comment/mapper/CommentMapper.java @@ -21,7 +21,6 @@ public CommentDTO toDTO(Comment comment) { ); } - // Todo: Service layer must set visa and author before persisting public Comment toEntity(CreateCommentDTO dto) { if (dto == null) return null; 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 616799a..b75e8dc 100644 --- a/src/main/java/org/example/visacasemanagementsystem/comment/service/CommentService.java +++ b/src/main/java/org/example/visacasemanagementsystem/comment/service/CommentService.java @@ -1,5 +1,7 @@ package org.example.visacasemanagementsystem.comment.service; +import org.example.visacasemanagementsystem.audit.AuditEventType; +import org.example.visacasemanagementsystem.audit.service.AuditService; import org.example.visacasemanagementsystem.comment.dto.CommentDTO; import org.example.visacasemanagementsystem.comment.dto.CreateCommentDTO; import org.example.visacasemanagementsystem.comment.entity.Comment; @@ -22,12 +24,14 @@ public class CommentService { private final CommentMapper commentMapper; private final UserRepository userRepository; private final VisaRepository visaRepository; + private final AuditService auditService; - public CommentService(CommentRepository commentRepository, CommentMapper commentMapper , UserRepository userRepository, VisaRepository visaRepository) { + public CommentService(CommentRepository commentRepository, CommentMapper commentMapper , UserRepository userRepository, VisaRepository visaRepository, AuditService auditService) { this.commentRepository = commentRepository; this.commentMapper = commentMapper; this.userRepository = userRepository; this.visaRepository = visaRepository; + this.auditService = auditService; } @@ -60,6 +64,16 @@ public CommentDTO createComment(CreateCommentDTO dto) { // Save and return Comment savedComment = commentRepository.save(comment); + + // Create log in database + auditService.createAuditLog( + author.getId (), + visa.getId (), + AuditEventType.UPDATED, + "Comment added by " + author.getFullName() + + ); + return commentMapper.toDTO(savedComment); } diff --git a/src/main/java/org/example/visacasemanagementsystem/config/SecurityConfig.java b/src/main/java/org/example/visacasemanagementsystem/config/SecurityConfig.java index 0e18789..0ed23c5 100644 --- a/src/main/java/org/example/visacasemanagementsystem/config/SecurityConfig.java +++ b/src/main/java/org/example/visacasemanagementsystem/config/SecurityConfig.java @@ -14,7 +14,6 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .authorizeHttpRequests(auth -> auth .anyRequest().permitAll() ) - .csrf(csrf -> csrf.disable()) .headers(headers -> headers.frameOptions(frame -> frame.disable())); return http.build(); } diff --git a/src/main/resources/templates/visa/details.html b/src/main/resources/templates/visa/details.html index 3479dd2..ea250f5 100644 --- a/src/main/resources/templates/visa/details.html +++ b/src/main/resources/templates/visa/details.html @@ -27,8 +27,8 @@ } .back-link:hover { color: #fff; } - header { border-bottom: 1px solid #222; padding-bottom: 20px; margin-bottom: 30px; } - h1 { font-size: 28px; font-weight: 500; margin: 0; color: #fff; } + header { border-bottom: 1px solid #222; padding-bottom: 20px; margin-bottom: 30px;} + h1 { font-size: 38px; font-weight: 500; margin: 0; color: #fff; } .user-info { color: #666; font-size: 14px; margin-top: 5px; } /* Grid för info */ @@ -39,14 +39,14 @@ border-radius: 8px; background: #111; } - .label { font-size: 11px; text-transform: uppercase; color: #666; display: block; margin-bottom: 5px; letter-spacing: 0.5px; } - .value { font-size: 16px; font-weight: 500; color: #ccc; } + .label { font-size: 12px; text-transform: uppercase; color: #666; display: block; margin-bottom: 5px; letter-spacing: 0.5px; } + .value { font-size: 18px; font-weight: 500; color: #ccc; } /* Admin Actions & Buttons */ .admin-actions { background: #111; padding: 20px; - border-radius: 12px; + border-radius: 8px; border: 1px solid #222; margin-bottom: 40px; } @@ -55,9 +55,12 @@ color: #fff; border: 1px solid #333; padding: 10px 20px; - border-radius: 6px; + border-radius: 8px; cursor: pointer; transition: 0.2s; + font-weight: 600; + font-size: 12px; + } .btn-assign:hover, .btn-approve:hover, button:hover { background: #fff; @@ -70,11 +73,18 @@ background: #181818; color: #fff; border: 1px solid #333 !important; + width: 100%; + box-sizing: border-box; + display: block; + border-radius: 8px; + font-size: 12px; } #infoReason:focus { outline: none; border-color: #666 !important; } + .action-group{ display: flex; gap: 10px; } + /* Kommentar-sektion */ - .comments-section { margin-top: 50px; border-top: 1px solid #222; padding-top: 30px; } + .comments-section { border-top: 1px solid #222; padding-top: 10px; font-size: 20px; font-weight: 600; } .comment-card { padding: 15px 0; border-bottom: 1px solid #111; } .comment-meta { font-size: 12px; color: #666; margin-bottom: 5px; } .comment-text { font-size: 14px; color: #ccc; } @@ -86,6 +96,7 @@ padding: 20px; border-radius: 8px; border: 1px solid #222; + } textarea { width: 100%; @@ -93,14 +104,15 @@ color: #fff; border: 1px solid #333; padding: 10px; - border-radius: 4px; + border-radius: 8px; resize: vertical; min-height: 80px; margin-bottom: 10px; box-sizing: border-box; + font-size: 12px; } textarea:focus { outline: none; border-color: #666; } - .btn-submit { background: #fff; color: #000; font-weight: 600; border: none; } + .btn-submit { background: #fff; color: #000; font-weight: 600; border: none; transition: 0.2s ease; } .btn-submit:hover { background: #ccc; } @@ -127,9 +139,10 @@

Case #101

SUBMITTED -
- Last Updated - 2023-10-28 09:30 + +
+ Nationality + -
@@ -151,9 +164,9 @@

Case #101

2023-10-27
-
- Nationality - - +
+ Last Updated + 2023-10-28 09:30
-

Case Management

+

Case Management

Case Management
-
+
+ placeholder="Write reason for rejection or what information is missing..." + style="width: 100%; padding: 15px; margin-bottom: 20px; border: 1px solid #333; border-radius: 8px; box-sizing: border-box;" + required> -
+
@@ -216,34 +230,98 @@

Case Management

-

Comments

-
-
- Admin • - Oct 27, 14:00 +
+
+
+ Admin • + Oct 27, 14:00 +
+
This is a comment
-
This is a comment
-
+
No comments yet.
-
- - - + + + + + + + +
+ + diff --git a/src/test/java/org/example/visacasemanagementsystem/comment/CommentControllerTest.java b/src/test/java/org/example/visacasemanagementsystem/comment/CommentControllerTest.java new file mode 100644 index 0000000..07ef7c4 --- /dev/null +++ b/src/test/java/org/example/visacasemanagementsystem/comment/CommentControllerTest.java @@ -0,0 +1,75 @@ +package org.example.visacasemanagementsystem.comment; + +import org.example.visacasemanagementsystem.comment.controller.CommentController; +import org.example.visacasemanagementsystem.comment.dto.CommentDTO; +import org.example.visacasemanagementsystem.comment.dto.CreateCommentDTO; +import org.example.visacasemanagementsystem.comment.service.CommentService; +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.test.context.support.WithMockUser; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import tools.jackson.databind.ObjectMapper; + +import java.time.LocalDateTime; +import java.util.List; + + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; + + + +@WebMvcTest(CommentController.class) +class CommentControllerTest { + + @Autowired + private MockMvc mockMvc; + @MockitoBean + private CommentService commentService; + @Autowired + private ObjectMapper objectMapper; + + @Test + @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()); + + when(commentService.createComment(any(CreateCommentDTO.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("$.text").value("Test message")) + .andExpect(jsonPath("$.authorName").value("Test User")); + } + + @Test + @WithMockUser + void getCommentsByVisa_ShouldReturnList() throws Exception { + // Arrange + String expectedText = "Hello World"; + CommentDTO comment = new CommentDTO(1L, 2L, "Admin", expectedText, LocalDateTime.now()); + + when(commentService.getCommentsByVisaId(1L)).thenReturn(List.of(comment)); + + // Act & Assert + mockMvc.perform(get("/api/comments/visa/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.length()").value(1)) + .andExpect(jsonPath("$[0].text").value(expectedText)); + } +} diff --git a/src/test/java/org/example/visacasemanagementsystem/comment/CommentMapperTest.java b/src/test/java/org/example/visacasemanagementsystem/comment/CommentMapperTest.java new file mode 100644 index 0000000..1af124f --- /dev/null +++ b/src/test/java/org/example/visacasemanagementsystem/comment/CommentMapperTest.java @@ -0,0 +1,89 @@ +package org.example.visacasemanagementsystem.comment; + +import org.example.visacasemanagementsystem.comment.dto.CommentDTO; +import org.example.visacasemanagementsystem.comment.dto.CreateCommentDTO; +import org.example.visacasemanagementsystem.comment.entity.Comment; +import org.example.visacasemanagementsystem.comment.mapper.CommentMapper; +import org.example.visacasemanagementsystem.user.entity.User; +import org.example.visacasemanagementsystem.visa.entity.Visa; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +class CommentMapperTest { + + private CommentMapper commentMapper; + + @BeforeEach + void setUp() { + commentMapper = new CommentMapper(); + } + + @Test + void shouldMapCreateCommentDTOtoEntity() { + // Arrange + CreateCommentDTO dto = new CreateCommentDTO( + 100L, + 50L, + "This is a comment text." + + ); + + // Act + Comment result = commentMapper.toEntity(dto); + + // Assert + assertThat(result).isNotNull(); + assertThat(result.getText()).isEqualTo("This is a comment text."); + assertThat(result.getId()).isNull(); + } + + @Test + void shouldMapToDTOWithNullRelations() { + // Arrange + Comment comment= new Comment(); + comment.setId(1L); + comment.setText("No relations"); + comment.setAuthor(null); + comment.setVisa(null); + + // Act + CommentDTO result = commentMapper.toDTO(comment); + + // Assert + assertThat(result.authorName()).isEqualTo("System"); + assertThat(result.visaId()).isNull(); + } + + + @Test + void shouldMapCommentEntityToCommentDTO() { + // Arrange + User author = new User(); + author.setFullName("Test User"); + + Visa visa = new Visa(); + visa.setId(100L); + + Comment comment = new Comment(); + comment.setId(1L); + comment.setText("This is a comment text."); + comment.setAuthor(author); + comment.setVisa(visa); + comment.setCreatedAt(LocalDateTime.now()); + + // Act + 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"); + assertThat(result.createdAt()).isNotNull(); + + } +} diff --git a/src/test/java/org/example/visacasemanagementsystem/comment/CommentServiceIntegrationTest.java b/src/test/java/org/example/visacasemanagementsystem/comment/CommentServiceIntegrationTest.java new file mode 100644 index 0000000..bb0e272 --- /dev/null +++ b/src/test/java/org/example/visacasemanagementsystem/comment/CommentServiceIntegrationTest.java @@ -0,0 +1,149 @@ +package org.example.visacasemanagementsystem.comment; + +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.exception.ResourceNotFoundException; +import org.example.visacasemanagementsystem.user.UserAuthorization; +import org.example.visacasemanagementsystem.user.entity.User; +import org.example.visacasemanagementsystem.user.repository.UserRepository; +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.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SpringBootTest +@Transactional +@ActiveProfiles("test") +class CommentServiceIntegrationTest { + + @Autowired + private CommentService commentService; + @Autowired + private VisaRepository visaRepository; + @Autowired + private UserRepository userRepository; + + private User testUser; + private Visa testVisa; + + @BeforeEach + void setUp() { + // Create test user + testUser = new User(); + testUser.setFullName("TestUser"); + testUser.setEmail("test@example.com"); + testUser.setPassword("password123"); + testUser.setUserAuthorization(UserAuthorization.USER); + testUser = userRepository.save(testUser); + + // Create test visa + testVisa = new Visa(); + testVisa.setApplicant(testUser); + testVisa.setPassportNumber("AB123456"); + testVisa.setNationality("Swedish"); + testVisa.setVisaType(VisaType.TOURIST); + testVisa.setVisaStatus(VisaStatus.SUBMITTED); + testVisa.setTravelDate(LocalDate.now().plusMonths(1)); + testVisa = visaRepository.save(testVisa); + + } + + @Test + void createComment_shouldSaveAndRetrieveComment() { + // Arrange + CreateCommentDTO dto = new CreateCommentDTO( + testVisa.getId(), + testUser.getId(), + "This is a test comment" + ); + + // Act + CommentDTO savedComment = commentService.createComment(dto); + + // 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"); + + } + + @Test + void createComment_shouldThrowException_WhenUserDoesNotExist() { + // Arrange + Long nonExistentUserId = 999L; + CreateCommentDTO dto = new CreateCommentDTO(testVisa.getId(), nonExistentUserId, "Valid comment text"); + + // Act & Assert + assertThatThrownBy(() -> commentService.createComment(dto)) + .isInstanceOf(ResourceNotFoundException.class) + .hasMessageContaining("User not found with id: " + nonExistentUserId); + } + + @Test + void createComment_shouldThrowException_WhenVisaDoesNotExist() { + // Arrange + Long nonExistentVisaId = 999L; + CreateCommentDTO dto = new CreateCommentDTO(nonExistentVisaId, testUser.getId(), "Some text"); + + // Act & Assert + assertThatThrownBy(() -> commentService.createComment(dto)) + .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(), " "); + + // Act & Assert + assertThatThrownBy(() -> commentService.createComment(dto)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Comment text cannot be empty"); + } + + @Test + void getCommentsByVisaId_ShouldThrowException_WhenIdIsInvalid() { + // Act & Assert + assertThatThrownBy(() -> commentService.getCommentsByVisaId(-1L)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Visa ID must be a positive number"); + } + + @Test + void getCommentsByVisaID_ShouldThrowException_WhenVisaDoesNotExistAndNoComments() { + // Act & Assert + assertThatThrownBy(() -> commentService.getCommentsByVisaId(999L)) + .isInstanceOf(ResourceNotFoundException.class) + .hasMessageContaining("Visa case not found with id: 999"); + } + + @Test + void getCommentsByVisaID_ShouldReturnEmptyList_WhenVisaExistsButHasNoComments() { + // Act + List comments = commentService.getCommentsByVisaId(testVisa.getId()); + + // Assert + assertThat(comments).isEmpty(); + + } +}