resultWithQueueInfo =
+ Map.of(
+ "jobResult",
+ result,
+ "queueInfo",
+ Map.of("inQueue", true, "position", position));
+ return ResponseEntity.ok(resultWithQueueInfo);
+ }
+
+ return ResponseEntity.ok(result);
+ }
+
+ /**
+ * Get the result of a job
+ *
+ * @param jobId The job ID
+ * @return The job result
+ */
+ @GetMapping("/api/v1/general/job/{jobId}/result")
+ public ResponseEntity> getJobResult(@PathVariable("jobId") String jobId) {
+ JobResult result = taskManager.getJobResult(jobId);
+ if (result == null) {
+ return ResponseEntity.notFound().build();
+ }
+
+ if (!result.isComplete()) {
+ return ResponseEntity.badRequest().body("Job is not complete yet");
+ }
+
+ if (result.getError() != null) {
+ return ResponseEntity.badRequest().body("Job failed: " + result.getError());
+ }
+
+ if (result.getFileId() != null) {
+ try {
+ byte[] fileContent = fileStorage.retrieveBytes(result.getFileId());
+ return ResponseEntity.ok()
+ .header("Content-Type", result.getContentType())
+ .header(
+ "Content-Disposition",
+ "form-data; name=\"attachment\"; filename=\""
+ + result.getOriginalFileName()
+ + "\"")
+ .body(fileContent);
+ } catch (Exception e) {
+ log.error("Error retrieving file for job {}: {}", jobId, e.getMessage(), e);
+ return ResponseEntity.internalServerError()
+ .body("Error retrieving file: " + e.getMessage());
+ }
+ }
+
+ return ResponseEntity.ok(result.getResult());
+ }
+
+ // Admin-only endpoints have been moved to AdminJobController in the proprietary package
+
+ /**
+ * Cancel a job by its ID
+ *
+ * This method should only allow cancellation of jobs that were created by the current user.
+ * The jobId should be part of the user's session or otherwise linked to their identity.
+ *
+ * @param jobId The job ID
+ * @return Response indicating whether the job was cancelled
+ */
+ @DeleteMapping("/api/v1/general/job/{jobId}")
+ public ResponseEntity> cancelJob(@PathVariable("jobId") String jobId) {
+ log.debug("Request to cancel job: {}", jobId);
+
+ // Verify that this job belongs to the current user
+ // We can use the current request's session to validate ownership
+ Object sessionJobIds = request.getSession().getAttribute("userJobIds");
+ if (sessionJobIds == null
+ || !(sessionJobIds instanceof java.util.Set)
+ || !((java.util.Set>) sessionJobIds).contains(jobId)) {
+ // Either no jobs in session or jobId doesn't match user's jobs
+ log.warn("Unauthorized attempt to cancel job: {}", jobId);
+ return ResponseEntity.status(403)
+ .body(Map.of("message", "You are not authorized to cancel this job"));
+ }
+
+ // First check if the job is in the queue
+ boolean cancelled = false;
+ int queuePosition = -1;
+
+ if (jobQueue.isJobQueued(jobId)) {
+ queuePosition = jobQueue.getJobPosition(jobId);
+ cancelled = jobQueue.cancelJob(jobId);
+ log.info("Cancelled queued job: {} (was at position {})", jobId, queuePosition);
+ }
+
+ // If not in queue or couldn't cancel, try to cancel in TaskManager
+ if (!cancelled) {
+ JobResult result = taskManager.getJobResult(jobId);
+ if (result != null && !result.isComplete()) {
+ // Mark as error with cancellation message
+ taskManager.setError(jobId, "Job was cancelled by user");
+ cancelled = true;
+ log.info("Marked job as cancelled in TaskManager: {}", jobId);
+ }
+ }
+
+ if (cancelled) {
+ return ResponseEntity.ok(
+ Map.of(
+ "message",
+ "Job cancelled successfully",
+ "wasQueued",
+ queuePosition >= 0,
+ "queuePosition",
+ queuePosition >= 0 ? queuePosition : "n/a"));
+ } else {
+ // Job not found or already complete
+ JobResult result = taskManager.getJobResult(jobId);
+ if (result == null) {
+ return ResponseEntity.notFound().build();
+ } else if (result.isComplete()) {
+ return ResponseEntity.badRequest()
+ .body(Map.of("message", "Cannot cancel job that is already complete"));
+ } else {
+ return ResponseEntity.internalServerError()
+ .body(Map.of("message", "Failed to cancel job for unknown reason"));
+ }
+ }
+ }
+}
diff --git a/stirling-pdf/src/test/java/stirling/software/common/controller/JobControllerTest.java b/stirling-pdf/src/test/java/stirling/software/common/controller/JobControllerTest.java
new file mode 100644
index 000000000..4ed005835
--- /dev/null
+++ b/stirling-pdf/src/test/java/stirling/software/common/controller/JobControllerTest.java
@@ -0,0 +1,406 @@
+package stirling.software.common.controller;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.util.Map;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpSession;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpSession;
+
+import stirling.software.common.model.job.JobResult;
+import stirling.software.common.model.job.JobStats;
+import stirling.software.common.service.FileStorage;
+import stirling.software.common.service.JobQueue;
+import stirling.software.common.service.TaskManager;
+
+class JobControllerTest {
+
+ @Mock
+ private TaskManager taskManager;
+
+ @Mock
+ private FileStorage fileStorage;
+
+ @Mock
+ private JobQueue jobQueue;
+
+ @Mock
+ private HttpServletRequest request;
+
+ private MockHttpSession session;
+
+ @InjectMocks
+ private JobController controller;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.openMocks(this);
+
+ // Setup mock session for tests
+ session = new MockHttpSession();
+ when(request.getSession()).thenReturn(session);
+ }
+
+ @Test
+ void testGetJobStatus_ExistingJob() {
+ // Arrange
+ String jobId = "test-job-id";
+ JobResult mockResult = new JobResult();
+ mockResult.setJobId(jobId);
+ when(taskManager.getJobResult(jobId)).thenReturn(mockResult);
+
+ // Act
+ ResponseEntity> response = controller.getJobStatus(jobId);
+
+ // Assert
+ assertEquals(HttpStatus.OK, response.getStatusCode());
+ assertEquals(mockResult, response.getBody());
+ }
+
+ @Test
+ void testGetJobStatus_ExistingJobInQueue() {
+ // Arrange
+ String jobId = "test-job-id";
+ JobResult mockResult = new JobResult();
+ mockResult.setJobId(jobId);
+ mockResult.setComplete(false);
+ when(taskManager.getJobResult(jobId)).thenReturn(mockResult);
+ when(jobQueue.isJobQueued(jobId)).thenReturn(true);
+ when(jobQueue.getJobPosition(jobId)).thenReturn(3);
+
+ // Act
+ ResponseEntity> response = controller.getJobStatus(jobId);
+
+ // Assert
+ assertEquals(HttpStatus.OK, response.getStatusCode());
+
+ @SuppressWarnings("unchecked")
+ Map responseBody = (Map) response.getBody();
+ assertEquals(mockResult, responseBody.get("jobResult"));
+
+ @SuppressWarnings("unchecked")
+ Map queueInfo = (Map) responseBody.get("queueInfo");
+ assertTrue((Boolean) queueInfo.get("inQueue"));
+ assertEquals(3, queueInfo.get("position"));
+ }
+
+ @Test
+ void testGetJobStatus_NonExistentJob() {
+ // Arrange
+ String jobId = "non-existent-job";
+ when(taskManager.getJobResult(jobId)).thenReturn(null);
+
+ // Act
+ ResponseEntity> response = controller.getJobStatus(jobId);
+
+ // Assert
+ assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
+ }
+
+ @Test
+ void testGetJobResult_CompletedSuccessfulWithObject() {
+ // Arrange
+ String jobId = "test-job-id";
+ JobResult mockResult = new JobResult();
+ mockResult.setJobId(jobId);
+ mockResult.setComplete(true);
+ String resultObject = "Test result";
+ mockResult.completeWithResult(resultObject);
+
+ when(taskManager.getJobResult(jobId)).thenReturn(mockResult);
+
+ // Act
+ ResponseEntity> response = controller.getJobResult(jobId);
+
+ // Assert
+ assertEquals(HttpStatus.OK, response.getStatusCode());
+ assertEquals(resultObject, response.getBody());
+ }
+
+ @Test
+ void testGetJobResult_CompletedSuccessfulWithFile() throws Exception {
+ // Arrange
+ String jobId = "test-job-id";
+ String fileId = "file-id";
+ String originalFileName = "test.pdf";
+ String contentType = "application/pdf";
+ byte[] fileContent = "Test file content".getBytes();
+
+ JobResult mockResult = new JobResult();
+ mockResult.setJobId(jobId);
+ mockResult.completeWithFile(fileId, originalFileName, contentType);
+
+ when(taskManager.getJobResult(jobId)).thenReturn(mockResult);
+ when(fileStorage.retrieveBytes(fileId)).thenReturn(fileContent);
+
+ // Act
+ ResponseEntity> response = controller.getJobResult(jobId);
+
+ // Assert
+ assertEquals(HttpStatus.OK, response.getStatusCode());
+ assertEquals(contentType, response.getHeaders().getFirst("Content-Type"));
+ assertTrue(response.getHeaders().getFirst("Content-Disposition").contains(originalFileName));
+ assertEquals(fileContent, response.getBody());
+ }
+
+ @Test
+ void testGetJobResult_CompletedWithError() {
+ // Arrange
+ String jobId = "test-job-id";
+ String errorMessage = "Test error";
+
+ JobResult mockResult = new JobResult();
+ mockResult.setJobId(jobId);
+ mockResult.failWithError(errorMessage);
+
+ when(taskManager.getJobResult(jobId)).thenReturn(mockResult);
+
+ // Act
+ ResponseEntity> response = controller.getJobResult(jobId);
+
+ // Assert
+ assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
+ assertTrue(response.getBody().toString().contains(errorMessage));
+ }
+
+ @Test
+ void testGetJobResult_IncompleteJob() {
+ // Arrange
+ String jobId = "test-job-id";
+
+ JobResult mockResult = new JobResult();
+ mockResult.setJobId(jobId);
+ mockResult.setComplete(false);
+
+ when(taskManager.getJobResult(jobId)).thenReturn(mockResult);
+
+ // Act
+ ResponseEntity> response = controller.getJobResult(jobId);
+
+ // Assert
+ assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
+ assertTrue(response.getBody().toString().contains("not complete"));
+ }
+
+ @Test
+ void testGetJobResult_NonExistentJob() {
+ // Arrange
+ String jobId = "non-existent-job";
+ when(taskManager.getJobResult(jobId)).thenReturn(null);
+
+ // Act
+ ResponseEntity> response = controller.getJobResult(jobId);
+
+ // Assert
+ assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
+ }
+
+ @Test
+ void testGetJobResult_ErrorRetrievingFile() throws Exception {
+ // Arrange
+ String jobId = "test-job-id";
+ String fileId = "file-id";
+ String originalFileName = "test.pdf";
+ String contentType = "application/pdf";
+
+ JobResult mockResult = new JobResult();
+ mockResult.setJobId(jobId);
+ mockResult.completeWithFile(fileId, originalFileName, contentType);
+
+ when(taskManager.getJobResult(jobId)).thenReturn(mockResult);
+ when(fileStorage.retrieveBytes(fileId)).thenThrow(new RuntimeException("File not found"));
+
+ // Act
+ ResponseEntity> response = controller.getJobResult(jobId);
+
+ // Assert
+ assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode());
+ assertTrue(response.getBody().toString().contains("Error retrieving file"));
+ }
+
+ /*
+ * @Test void testGetJobStats() { // Arrange JobStats mockStats =
+ * JobStats.builder() .totalJobs(10) .activeJobs(3) .completedJobs(7) .build();
+ *
+ * when(taskManager.getJobStats()).thenReturn(mockStats);
+ *
+ * // Act ResponseEntity> response = controller.getJobStats();
+ *
+ * // Assert assertEquals(HttpStatus.OK, response.getStatusCode());
+ * assertEquals(mockStats, response.getBody()); }
+ *
+ * @Test void testCleanupOldJobs() { // Arrange when(taskManager.getJobStats())
+ * .thenReturn(JobStats.builder().totalJobs(10).build())
+ * .thenReturn(JobStats.builder().totalJobs(7).build());
+ *
+ * // Act ResponseEntity> response = controller.cleanupOldJobs();
+ *
+ * // Assert assertEquals(HttpStatus.OK, response.getStatusCode());
+ *
+ * @SuppressWarnings("unchecked") Map responseBody =
+ * (Map) response.getBody(); assertEquals("Cleanup complete",
+ * responseBody.get("message")); assertEquals(3,
+ * responseBody.get("removedJobs")); assertEquals(7,
+ * responseBody.get("remainingJobs"));
+ *
+ * verify(taskManager).cleanupOldJobs(); }
+ *
+ * @Test void testGetQueueStats() { // Arrange Map
+ * mockQueueStats = Map.of( "queuedJobs", 5, "queueCapacity", 10,
+ * "resourceStatus", "OK" );
+ *
+ * when(jobQueue.getQueueStats()).thenReturn(mockQueueStats);
+ *
+ * // Act ResponseEntity> response = controller.getQueueStats();
+ *
+ * // Assert assertEquals(HttpStatus.OK, response.getStatusCode());
+ * assertEquals(mockQueueStats, response.getBody());
+ * verify(jobQueue).getQueueStats(); }
+ */
+ @Test
+ void testCancelJob_InQueue() {
+ // Arrange
+ String jobId = "job-in-queue";
+
+ // Setup user session with job authorization
+ java.util.Set userJobIds = new java.util.HashSet<>();
+ userJobIds.add(jobId);
+ session.setAttribute("userJobIds", userJobIds);
+
+ when(jobQueue.isJobQueued(jobId)).thenReturn(true);
+ when(jobQueue.getJobPosition(jobId)).thenReturn(2);
+ when(jobQueue.cancelJob(jobId)).thenReturn(true);
+
+ // Act
+ ResponseEntity> response = controller.cancelJob(jobId);
+
+ // Assert
+ assertEquals(HttpStatus.OK, response.getStatusCode());
+
+ @SuppressWarnings("unchecked")
+ Map responseBody = (Map) response.getBody();
+ assertEquals("Job cancelled successfully", responseBody.get("message"));
+ assertTrue((Boolean) responseBody.get("wasQueued"));
+ assertEquals(2, responseBody.get("queuePosition"));
+
+ verify(jobQueue).cancelJob(jobId);
+ verify(taskManager, never()).setError(anyString(), anyString());
+ }
+
+ @Test
+ void testCancelJob_Running() {
+ // Arrange
+ String jobId = "job-running";
+ JobResult jobResult = new JobResult();
+ jobResult.setJobId(jobId);
+ jobResult.setComplete(false);
+
+ // Setup user session with job authorization
+ java.util.Set userJobIds = new java.util.HashSet<>();
+ userJobIds.add(jobId);
+ session.setAttribute("userJobIds", userJobIds);
+
+ when(jobQueue.isJobQueued(jobId)).thenReturn(false);
+ when(taskManager.getJobResult(jobId)).thenReturn(jobResult);
+
+ // Act
+ ResponseEntity> response = controller.cancelJob(jobId);
+
+ // Assert
+ assertEquals(HttpStatus.OK, response.getStatusCode());
+
+ @SuppressWarnings("unchecked")
+ Map responseBody = (Map) response.getBody();
+ assertEquals("Job cancelled successfully", responseBody.get("message"));
+ assertFalse((Boolean) responseBody.get("wasQueued"));
+ assertEquals("n/a", responseBody.get("queuePosition"));
+
+ verify(jobQueue, never()).cancelJob(jobId);
+ verify(taskManager).setError(jobId, "Job was cancelled by user");
+ }
+
+ @Test
+ void testCancelJob_NotFound() {
+ // Arrange
+ String jobId = "non-existent-job";
+
+ // Setup user session with job authorization
+ java.util.Set userJobIds = new java.util.HashSet<>();
+ userJobIds.add(jobId);
+ session.setAttribute("userJobIds", userJobIds);
+
+ when(jobQueue.isJobQueued(jobId)).thenReturn(false);
+ when(taskManager.getJobResult(jobId)).thenReturn(null);
+
+ // Act
+ ResponseEntity> response = controller.cancelJob(jobId);
+
+ // Assert
+ assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
+ }
+
+ @Test
+ void testCancelJob_AlreadyComplete() {
+ // Arrange
+ String jobId = "completed-job";
+ JobResult jobResult = new JobResult();
+ jobResult.setJobId(jobId);
+ jobResult.setComplete(true);
+
+ // Setup user session with job authorization
+ java.util.Set userJobIds = new java.util.HashSet<>();
+ userJobIds.add(jobId);
+ session.setAttribute("userJobIds", userJobIds);
+
+ when(jobQueue.isJobQueued(jobId)).thenReturn(false);
+ when(taskManager.getJobResult(jobId)).thenReturn(jobResult);
+
+ // Act
+ ResponseEntity> response = controller.cancelJob(jobId);
+
+ // Assert
+ assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
+
+ @SuppressWarnings("unchecked")
+ Map responseBody = (Map) response.getBody();
+ assertEquals("Cannot cancel job that is already complete", responseBody.get("message"));
+ }
+
+ @Test
+ void testCancelJob_Unauthorized() {
+ // Arrange
+ String jobId = "unauthorized-job";
+
+ // Setup user session with other job IDs but not this one
+ java.util.Set userJobIds = new java.util.HashSet<>();
+ userJobIds.add("other-job-1");
+ userJobIds.add("other-job-2");
+ session.setAttribute("userJobIds", userJobIds);
+
+ // Act
+ ResponseEntity> response = controller.cancelJob(jobId);
+
+ // Assert
+ assertEquals(HttpStatus.FORBIDDEN, response.getStatusCode());
+
+ @SuppressWarnings("unchecked")
+ Map responseBody = (Map) response.getBody();
+ assertEquals("You are not authorized to cancel this job", responseBody.get("message"));
+
+ // Verify no cancellation attempts were made
+ verify(jobQueue, never()).isJobQueued(anyString());
+ verify(jobQueue, never()).cancelJob(anyString());
+ verify(taskManager, never()).getJobResult(anyString());
+ verify(taskManager, never()).setError(anyString(), anyString());
+ }
+}
\ No newline at end of file