diff --git a/backend/src/test/java/com/thughari/jobtrackerpro/controller/WebhookControllerTest.java b/backend/src/test/java/com/thughari/jobtrackerpro/controller/WebhookControllerTest.java new file mode 100644 index 0000000..05633d9 --- /dev/null +++ b/backend/src/test/java/com/thughari/jobtrackerpro/controller/WebhookControllerTest.java @@ -0,0 +1,115 @@ +package com.thughari.jobtrackerpro.controller; + +import com.thughari.jobtrackerpro.dto.JobDTO; +import com.thughari.jobtrackerpro.entity.User; +import com.thughari.jobtrackerpro.interfaces.GeminiService; +import com.thughari.jobtrackerpro.repo.UserRepository; +import com.thughari.jobtrackerpro.service.EmailService; +import com.thughari.jobtrackerpro.service.JobService; +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 java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class WebhookControllerTest { + + @Mock private JobService jobService; + @Mock private UserRepository userRepository; + @Mock private GeminiService geminiService; + @Mock private EmailService emailService; + + @InjectMocks + private WebhookController webhookController; + + @Test + void returnsInvalidPayloadWhenHeadersMissing() { + var response = webhookController.handleInboundEmail(Map.of("plain", "x")); + assertEquals("Invalid Payload", response.getBody()); + } + + @Test + void handlesGoogleForwardingVerification() { + Map headers = new HashMap<>(); + headers.put("from", "mailer-daemon@google.com"); + headers.put("subject", "Forwarding Confirmation"); + + String plain = "user@example.com has requested\nConfirmation code: 123456"; + + Map payload = new HashMap<>(); + payload.put("headers", headers); + payload.put("plain", plain); + + var response = webhookController.handleInboundEmail(payload); + + assertEquals("Verification Forwarded", response.getBody()); + verify(emailService).sendForwardingHelper("user@example.com", "123456", null); + } + + @Test + void ignoresSystemEmails() { + Map headers = new HashMap<>(); + headers.put("from", "foo@bar.com"); + headers.put("subject", "Please verify your address"); + + Map payload = new HashMap<>(); + payload.put("headers", headers); + payload.put("plain", "any body"); + + var response = webhookController.handleInboundEmail(payload); + + assertEquals("Ignored System Email", response.getBody()); + verifyNoInteractions(geminiService, jobService); + } + + @Test + void processesKnownUserAndCreatesJob() { + Map headers = new HashMap<>(); + headers.put("from", "Recruiter "); + headers.put("subject", "Application update"); + + Map payload = new HashMap<>(); + payload.put("headers", headers); + payload.put("plain", "hello"); + + User user = new User(); + user.setEmail("Candidate@Example.com"); + when(userRepository.findByEmail("hr@example.com")).thenReturn(Optional.of(user)); + + JobDTO jobDTO = new JobDTO(); + jobDTO.setCompany("Acme"); + when(geminiService.extractJobFromEmail(anyString(), anyString(), anyString())).thenReturn(jobDTO); + + var response = webhookController.handleInboundEmail(payload); + + assertEquals("Processed", response.getBody()); + verify(jobService).createOrUpdateJob(jobDTO, "candidate@example.com"); + } + + @Test + void returnsSkippedWhenGeminiReturnsNull() { + Map headers = new HashMap<>(); + headers.put("from", "hr@example.com"); + headers.put("subject", "Update"); + Map payload = new HashMap<>(); + payload.put("headers", headers); + payload.put("plain", "content"); + + User user = new User(); + user.setEmail("u@example.com"); + when(userRepository.findByEmail("hr@example.com")).thenReturn(Optional.of(user)); + when(geminiService.extractJobFromEmail(anyString(), anyString(), anyString())).thenReturn(null); + + var response = webhookController.handleInboundEmail(payload); + assertEquals("Skipped", response.getBody()); + } +} diff --git a/backend/src/test/java/com/thughari/jobtrackerpro/exception/GlobalExceptionHandlerTest.java b/backend/src/test/java/com/thughari/jobtrackerpro/exception/GlobalExceptionHandlerTest.java new file mode 100644 index 0000000..898e887 --- /dev/null +++ b/backend/src/test/java/com/thughari/jobtrackerpro/exception/GlobalExceptionHandlerTest.java @@ -0,0 +1,40 @@ +package com.thughari.jobtrackerpro.exception; + +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.web.multipart.MaxUploadSizeExceededException; + +import static org.junit.jupiter.api.Assertions.*; + +class GlobalExceptionHandlerTest { + + private final GlobalExceptionHandler handler = new GlobalExceptionHandler(); + + @Test + void handlesBadRequestExceptions() { + var response = handler.handleBadRequest(new IllegalArgumentException("bad")); + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + assertEquals("bad", response.getBody().getMessage()); + } + + @Test + void handlesNotFound() { + var response = handler.handleNotFound(new ResourceNotFoundException("missing")); + assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); + assertEquals("missing", response.getBody().getMessage()); + } + + @Test + void handlesPayloadTooLarge() { + var response = handler.handleMaxSizeException(new MaxUploadSizeExceededException(10)); + assertEquals(HttpStatus.PAYLOAD_TOO_LARGE, response.getStatusCode()); + assertTrue(response.getBody().getMessage().contains("File size exceeds")); + } + + @Test + void handlesFallbackException() { + var response = handler.handleGeneralException(new RuntimeException("oops")); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); + assertEquals("An unexpected error occurred.", response.getBody().getMessage()); + } +} diff --git a/backend/src/test/java/com/thughari/jobtrackerpro/scheduler/JobSchedulerTest.java b/backend/src/test/java/com/thughari/jobtrackerpro/scheduler/JobSchedulerTest.java new file mode 100644 index 0000000..c70fbcc --- /dev/null +++ b/backend/src/test/java/com/thughari/jobtrackerpro/scheduler/JobSchedulerTest.java @@ -0,0 +1,30 @@ +package com.thughari.jobtrackerpro.scheduler; + +import com.thughari.jobtrackerpro.service.JobService; +import org.junit.jupiter.api.Test; + +import static org.mockito.Mockito.*; + +class JobSchedulerTest { + + @Test + void runStaleJobCleanupInvokesService() { + JobService jobService = mock(JobService.class); + JobScheduler scheduler = new JobScheduler(jobService); + + scheduler.runStaleJobCleanup(); + + verify(jobService).cleanupStaleApplications(); + } + + @Test + void runStaleJobCleanupHandlesServiceException() { + JobService jobService = mock(JobService.class); + doThrow(new RuntimeException("boom")).when(jobService).cleanupStaleApplications(); + JobScheduler scheduler = new JobScheduler(jobService); + + scheduler.runStaleJobCleanup(); + + verify(jobService).cleanupStaleApplications(); + } +} diff --git a/backend/src/test/java/com/thughari/jobtrackerpro/security/JwtAuthenticationFilterTest.java b/backend/src/test/java/com/thughari/jobtrackerpro/security/JwtAuthenticationFilterTest.java new file mode 100644 index 0000000..fdd0a91 --- /dev/null +++ b/backend/src/test/java/com/thughari/jobtrackerpro/security/JwtAuthenticationFilterTest.java @@ -0,0 +1,58 @@ +package com.thughari.jobtrackerpro.security; + +import jakarta.servlet.FilterChain; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.core.context.SecurityContextHolder; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class JwtAuthenticationFilterTest { + + @Mock + private JwtUtils jwtUtils; + + @Mock + private FilterChain filterChain; + + @AfterEach + void tearDown() { + SecurityContextHolder.clearContext(); + } + + @Test + void skipsWhenNoBearerHeader() throws Exception { + JwtAuthenticationFilter filter = new JwtAuthenticationFilter(jwtUtils); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + filter.doFilter(request, response, filterChain); + + assertNull(SecurityContextHolder.getContext().getAuthentication()); + verify(filterChain).doFilter(request, response); + } + + @Test + void setsAuthenticationWhenTokenValid() throws Exception { + JwtAuthenticationFilter filter = new JwtAuthenticationFilter(jwtUtils); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Bearer token123"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + when(jwtUtils.validateToken("token123")).thenReturn(true); + when(jwtUtils.getEmailFromToken("token123")).thenReturn("user@example.com"); + + filter.doFilter(request, response, filterChain); + + assertNotNull(SecurityContextHolder.getContext().getAuthentication()); + assertEquals("user@example.com", SecurityContextHolder.getContext().getAuthentication().getPrincipal()); + verify(filterChain).doFilter(request, response); + } +} diff --git a/backend/src/test/java/com/thughari/jobtrackerpro/security/JwtUtilsTest.java b/backend/src/test/java/com/thughari/jobtrackerpro/security/JwtUtilsTest.java new file mode 100644 index 0000000..7b4f05c --- /dev/null +++ b/backend/src/test/java/com/thughari/jobtrackerpro/security/JwtUtilsTest.java @@ -0,0 +1,49 @@ +package com.thughari.jobtrackerpro.security; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.junit.jupiter.api.Assertions.*; + +class JwtUtilsTest { + + private JwtUtils jwtUtils; + + @BeforeEach + void setUp() { + jwtUtils = new JwtUtils(); + ReflectionTestUtils.setField(jwtUtils, "jwtSecret", "01234567890123456789012345678901"); + ReflectionTestUtils.setField(jwtUtils, "refreshJwtSecret", "abcdefghijklmnopqrstuvwxyz123456"); + ReflectionTestUtils.setField(jwtUtils, "jwtExpirationMs", 60_000); + ReflectionTestUtils.setField(jwtUtils, "refreshJwtExpirationMs", 120_000L); + ReflectionTestUtils.setField(jwtUtils, "activeProfile", "test"); + } + + @Test + void accessTokenRoundTrip() { + String token = jwtUtils.generateAccessToken("user@example.com"); + assertTrue(jwtUtils.validateAccessToken(token)); + assertEquals("user@example.com", jwtUtils.getEmailFromToken(token)); + } + + @Test + void refreshTokenRoundTrip() { + String refreshToken = jwtUtils.generateRefreshToken("user@example.com"); + assertTrue(jwtUtils.validateRefreshToken(refreshToken)); + assertEquals("user@example.com", jwtUtils.getEmailFromRefreshToken(refreshToken)); + } + + @Test + void rejectsInvalidToken() { + assertFalse(jwtUtils.validateAccessToken("not-a-token")); + assertFalse(jwtUtils.validateRefreshToken("not-a-token")); + } + + @Test + void generateTokenDelegatesToAccessToken() { + String token = jwtUtils.generateToken("mail@example.com"); + assertTrue(jwtUtils.validateToken(token)); + assertEquals("mail@example.com", jwtUtils.getEmailFromToken(token)); + } +} diff --git a/backend/src/test/java/com/thughari/jobtrackerpro/service/CareerResourceServiceTest.java b/backend/src/test/java/com/thughari/jobtrackerpro/service/CareerResourceServiceTest.java new file mode 100644 index 0000000..674bb77 --- /dev/null +++ b/backend/src/test/java/com/thughari/jobtrackerpro/service/CareerResourceServiceTest.java @@ -0,0 +1,115 @@ +package com.thughari.jobtrackerpro.service; + +import com.thughari.jobtrackerpro.dto.CreateCareerResourceRequest; +import com.thughari.jobtrackerpro.dto.UpdateCareerResourceRequest; +import com.thughari.jobtrackerpro.entity.CareerResource; +import com.thughari.jobtrackerpro.entity.User; +import com.thughari.jobtrackerpro.interfaces.StorageService; +import com.thughari.jobtrackerpro.repo.CareerResourceRepository; +import com.thughari.jobtrackerpro.repo.UserRepository; +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.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class CareerResourceServiceTest { + + @Mock private CareerResourceRepository resourceRepository; + @Mock private UserRepository userRepository; + @Mock private StorageService storageService; + @Mock private MultipartFile multipartFile; + + @InjectMocks + private CareerResourceService service; + + @Test + void getResourcePageSanitizesInput() { + when(resourceRepository.findAll(any(org.springframework.data.jpa.domain.Specification.class), any(Pageable.class))) + .thenAnswer(invocation -> { + Pageable pageable = invocation.getArgument(1); + return new PageImpl<>(List.of(), pageable, 0); + }); + + var response = service.getResourcePage(-1, 1000, " all ", " all ", "bad", null); + + assertEquals(0, response.getPage()); + assertEquals(50, response.getSize()); + } + + @Test + void createResourceRejectsDuplicateUrl() { + CreateCareerResourceRequest req = new CreateCareerResourceRequest(); + req.setTitle("A"); + req.setCategory("B"); + req.setUrl("https://example.com"); + + when(resourceRepository.existsByUrl("https://example.com")).thenReturn(true); + + assertThrows(IllegalArgumentException.class, () -> service.createResource("user@example.com", req)); + } + + @Test + void createResourceFromFileUploadsAndStoresMetadata() { + User user = new User(); + user.setId(UUID.randomUUID()); + user.setEmail("u@example.com"); + user.setName("User"); + + when(userRepository.findByEmail("u@example.com")).thenReturn(Optional.of(user)); + when(multipartFile.isEmpty()).thenReturn(false); + when(multipartFile.getOriginalFilename()).thenReturn("guide.pdf"); + when(multipartFile.getSize()).thenReturn(123L); + when(storageService.uploadResourceFile(multipartFile, user.getId().toString())).thenReturn("https://cdn/file.pdf"); + when(resourceRepository.save(any(CareerResource.class))).thenAnswer(inv -> inv.getArgument(0)); + + var dto = service.createResourceFromFile("u@example.com", " Title ", " Prep ", " Desc ", multipartFile); + + assertEquals("FILE", dto.getResourceType()); + assertEquals("guide.pdf", dto.getOriginalFileName()); + assertTrue(dto.isOwnedByCurrentUser()); + } + + @Test + void updateResourceRejectsUnauthorized() { + UUID id = UUID.randomUUID(); + CareerResource resource = new CareerResource(); + resource.setSubmittedByEmail("owner@example.com"); + + when(resourceRepository.findById(id)).thenReturn(Optional.of(resource)); + + UpdateCareerResourceRequest req = new UpdateCareerResourceRequest(); + req.setTitle("Title"); + req.setCategory("Category"); + + assertThrows(IllegalArgumentException.class, () -> service.updateResource("other@example.com", id, req)); + } + + @Test + void deleteResourceDeletesFileForFileType() { + UUID id = UUID.randomUUID(); + CareerResource resource = new CareerResource(); + resource.setSubmittedByEmail("owner@example.com"); + resource.setResourceType("FILE"); + resource.setUrl("https://cdn/path/file.pdf"); + + when(resourceRepository.findById(id)).thenReturn(Optional.of(resource)); + + service.deleteResource("owner@example.com", id); + + verify(storageService).deleteFile("https://cdn/path/file.pdf"); + verify(resourceRepository).delete(resource); + } +} diff --git a/backend/src/test/java/com/thughari/jobtrackerpro/service/mock/LocalStorageServiceTest.java b/backend/src/test/java/com/thughari/jobtrackerpro/service/mock/LocalStorageServiceTest.java new file mode 100644 index 0000000..53e328e --- /dev/null +++ b/backend/src/test/java/com/thughari/jobtrackerpro/service/mock/LocalStorageServiceTest.java @@ -0,0 +1,39 @@ +package com.thughari.jobtrackerpro.service.mock; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.util.ReflectionTestUtils; + +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.*; + +class LocalStorageServiceTest { + + @TempDir + Path tempDir; + + @Test + void uploadFileWritesToDiskAndReturnsUrl() { + System.setProperty("user.dir", tempDir.toString()); + LocalStorageService service = new LocalStorageService(); + ReflectionTestUtils.setField(service, "baseUrl", "http://localhost:8080"); + + MockMultipartFile file = new MockMultipartFile("file", "avatar.jpg", "image/jpeg", "abc".getBytes()); + + String url = service.uploadFile(file, "user1"); + + assertTrue(url.startsWith("http://localhost:8080/api/storage/files/user1-")); + } + + @Test + void uploadResourceFileRejectsUnsupportedExtension() { + LocalStorageService service = new LocalStorageService(); + ReflectionTestUtils.setField(service, "baseUrl", "http://localhost:8080"); + + MockMultipartFile file = new MockMultipartFile("file", "bad.exe", "application/octet-stream", "abc".getBytes()); + + assertThrows(IllegalArgumentException.class, () -> service.uploadResourceFile(file, "user1")); + } +} diff --git a/backend/src/test/java/com/thughari/jobtrackerpro/service/mock/MockGeminiServiceTest.java b/backend/src/test/java/com/thughari/jobtrackerpro/service/mock/MockGeminiServiceTest.java new file mode 100644 index 0000000..bbbbda6 --- /dev/null +++ b/backend/src/test/java/com/thughari/jobtrackerpro/service/mock/MockGeminiServiceTest.java @@ -0,0 +1,20 @@ +package com.thughari.jobtrackerpro.service.mock; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class MockGeminiServiceTest { + + @Test + void buildsMockJobFromEmailAndSubject() { + MockGeminiService service = new MockGeminiService(); + + var result = service.extractJobFromEmail("hr@acme.com", "Backend Engineer", "Body"); + + assertEquals("acme", result.getCompany()); + assertEquals("Backend Engineer", result.getRole()); + assertEquals("Applied", result.getStatus()); + assertNotNull(result.getAppliedDate()); + } +}