From 2cbc733daa12a61943cb0a23d467ad24fc1efbbb Mon Sep 17 00:00:00 2001 From: Ludy87 Date: Sun, 10 Aug 2025 00:59:55 +0200 Subject: [PATCH] Update JobControllerTest.java --- .../common/controller/JobControllerTest.java | 759 ++++++++++-------- 1 file changed, 437 insertions(+), 322 deletions(-) diff --git a/app/core/src/test/java/stirling/software/common/controller/JobControllerTest.java b/app/core/src/test/java/stirling/software/common/controller/JobControllerTest.java index 73b550cde..d8ca2e0ef 100644 --- a/app/core/src/test/java/stirling/software/common/controller/JobControllerTest.java +++ b/app/core/src/test/java/stirling/software/common/controller/JobControllerTest.java @@ -1,401 +1,516 @@ package stirling.software.common.controller; -import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; -import java.util.Map; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Set; 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.MockHttpSession; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; import stirling.software.common.model.job.JobResult; +import stirling.software.common.model.job.ResultFile; 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; + private TaskManager taskManager; + private FileStorage fileStorage; + private JobQueue jobQueue; + private HttpServletRequest request; + private HttpSession session; + private MockMvc mvc; @BeforeEach - void setUp() { - MockitoAnnotations.openMocks(this); - - // Setup mock session for tests - session = new MockHttpSession(); + void setup() { + taskManager = mock(TaskManager.class); + fileStorage = mock(FileStorage.class); + jobQueue = mock(JobQueue.class); + request = mock(HttpServletRequest.class); + session = mock(HttpSession.class); when(request.getSession()).thenReturn(session); + + JobController controller = new JobController(taskManager, fileStorage, jobQueue, request); + mvc = MockMvcBuilders.standaloneSetup(controller).build(); } @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()); + void getJobStatus_notFound_returns404() throws Exception { + when(taskManager.getJobResult("abc")).thenReturn(null); + mvc.perform(get("/api/v1/general/job/{jobId}", "abc")).andExpect(status().isNotFound()); } @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); + void getJobStatus_inQueue_addsQueueInfo() throws Exception { + JobResult result = mock(JobResult.class); + when(result.isComplete()).thenReturn(false); + when(taskManager.getJobResult("j1")).thenReturn(result); + when(jobQueue.isJobQueued("j1")).thenReturn(true); + when(jobQueue.getJobPosition("j1")).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")); + mvc.perform(get("/api/v1/general/job/{jobId}", "j1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.queueInfo.inQueue").value(true)) + .andExpect(jsonPath("$.queueInfo.position").value(3)); } @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()); + void getJobStatus_complete_returnsResult() throws Exception { + JobResult result = mock(JobResult.class); + when(result.isComplete()).thenReturn(true); + when(taskManager.getJobResult("j2")).thenReturn(result); + mvc.perform(get("/api/v1/general/job/{jobId}", "j2")).andExpect(status().isOk()); } @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); + void getJobStatus_inProgress_notQueued_returnsResult() throws Exception { + JobResult result = mock(JobResult.class); + when(result.isComplete()).thenReturn(false); + when(taskManager.getJobResult("JSTAT")).thenReturn(result); + when(jobQueue.isJobQueued("JSTAT")).thenReturn(false); - when(taskManager.getJobResult(jobId)).thenReturn(mockResult); - - // Act - ResponseEntity response = controller.getJobResult(jobId); - - // Assert - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(resultObject, response.getBody()); + mvc.perform(get("/api/v1/general/job/{jobId}", "JSTAT")).andExpect(status().isOk()); } @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.completeWithSingleFile( - fileId, originalFileName, contentType, fileContent.length); - - 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()); + void getJobResult_notFound() throws Exception { + when(taskManager.getJobResult("x")).thenReturn(null); + mvc.perform(get("/api/v1/general/job/{jobId}/result", "x")) + .andExpect(status().isNotFound()); } @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)); + void getJobResult_notComplete_returns400() throws Exception { + JobResult result = mock(JobResult.class); + when(result.isComplete()).thenReturn(false); + when(taskManager.getJobResult("j")).thenReturn(result); + mvc.perform(get("/api/v1/general/job/{jobId}/result", "j")) + .andExpect(status().isBadRequest()) + .andExpect( + content() + .string( + org.hamcrest.Matchers.containsString( + "Job is not complete yet"))); } @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")); + void getJobResult_error_returns400() throws Exception { + JobResult result = mock(JobResult.class); + when(result.isComplete()).thenReturn(true); + when(result.getError()).thenReturn("oops"); + when(taskManager.getJobResult("j")).thenReturn(result); + mvc.perform(get("/api/v1/general/job/{jobId}/result", "j")) + .andExpect(status().isBadRequest()) + .andExpect( + content().string(org.hamcrest.Matchers.containsString("Job failed: oops"))); } @Test - void testGetJobResult_NonExistentJob() { - // Arrange - String jobId = "non-existent-job"; - when(taskManager.getJobResult(jobId)).thenReturn(null); + void getJobResult_multipleFiles_returnsJsonList() throws Exception { + JobResult result = mock(JobResult.class); + when(result.isComplete()).thenReturn(true); + when(result.getError()).thenReturn(null); + when(result.hasMultipleFiles()).thenReturn(true); - // Act - ResponseEntity response = controller.getJobResult(jobId); + ResultFile f1 = mock(ResultFile.class); + ResultFile f2 = mock(ResultFile.class); + when(result.getAllResultFiles()).thenReturn(List.of(f1, f2)); + when(taskManager.getJobResult("j")).thenReturn(result); - // Assert - assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); + mvc.perform(get("/api/v1/general/job/{jobId}/result", "j")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.jobId").value("j")) + .andExpect(jsonPath("$.hasMultipleFiles").value(true)) + .andExpect(jsonPath("$.files.length()").value(2)); } @Test - void testGetJobResult_ErrorRetrievingFile() throws Exception { - // Arrange - String jobId = "test-job-id"; - String fileId = "file-id"; - String originalFileName = "test.pdf"; - String contentType = "application/pdf"; + void getJobResult_singleFile_streamsBytes_withHeaders() throws Exception { + JobResult result = mock(JobResult.class); + when(result.isComplete()).thenReturn(true); + when(result.getError()).thenReturn(null); + when(result.hasMultipleFiles()).thenReturn(false); + when(result.hasFiles()).thenReturn(true); - JobResult mockResult = new JobResult(); - mockResult.setJobId(jobId); - mockResult.completeWithSingleFile(fileId, originalFileName, contentType, 1024L); + ResultFile rf = mock(ResultFile.class); + when(rf.getFileId()).thenReturn("fid"); + when(rf.getFileName()).thenReturn("report.pdf"); + when(rf.getContentType()).thenReturn("application/pdf"); + when(result.getAllResultFiles()).thenReturn(List.of(rf)); - when(taskManager.getJobResult(jobId)).thenReturn(mockResult); - when(fileStorage.retrieveBytes(fileId)).thenThrow(new RuntimeException("File not found")); + when(taskManager.getJobResult("job9")).thenReturn(result); + when(fileStorage.retrieveBytes("fid")) + .thenReturn("PDFDATA".getBytes(StandardCharsets.UTF_8)); - // 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()); + mvc.perform(get("/api/v1/general/job/{jobId}/result", "job9")) + .andExpect(status().isOk()) + .andExpect(header().string("Content-Type", "application/pdf")) + .andExpect( + header().string( + "Content-Disposition", + org.hamcrest.Matchers.containsString( + "filename=\"report.pdf\""))) + .andExpect( + header().string( + "Content-Disposition", + org.hamcrest.Matchers.containsString("filename*="))) + .andExpect(content().bytes("PDFDATA".getBytes(StandardCharsets.UTF_8))); } @Test - void testCancelJob_Running() { - // Arrange - String jobId = "job-running"; - JobResult jobResult = new JobResult(); - jobResult.setJobId(jobId); - jobResult.setComplete(false); + void getJobResult_singleFile_retrievalError_returns500() throws Exception { + JobResult result = mock(JobResult.class); + when(result.isComplete()).thenReturn(true); + when(result.getError()).thenReturn(null); + when(result.hasMultipleFiles()).thenReturn(false); + when(result.hasFiles()).thenReturn(true); - // Setup user session with job authorization - java.util.Set userJobIds = new java.util.HashSet<>(); - userJobIds.add(jobId); - session.setAttribute("userJobIds", userJobIds); + ResultFile rf = mock(ResultFile.class); + when(rf.getFileId()).thenReturn("fid"); + when(rf.getFileName()).thenReturn("x.pdf"); + when(rf.getContentType()).thenReturn("application/pdf"); + when(result.getAllResultFiles()).thenReturn(List.of(rf)); - when(jobQueue.isJobQueued(jobId)).thenReturn(false); - when(taskManager.getJobResult(jobId)).thenReturn(jobResult); + when(taskManager.getJobResult("job10")).thenReturn(result); + when(fileStorage.retrieveBytes("fid")).thenThrow(new RuntimeException("IO boom")); - // 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"); + mvc.perform(get("/api/v1/general/job/{jobId}/result", "job10")) + .andExpect(status().isInternalServerError()) + .andExpect( + content() + .string( + org.hamcrest.Matchers.containsString( + "Error retrieving file:"))); } @Test - void testCancelJob_NotFound() { - // Arrange - String jobId = "non-existent-job"; + void getJobResult_noFiles_returns_result_payload() throws Exception { + JobResult result = mock(JobResult.class); + when(result.isComplete()).thenReturn(true); + when(result.getError()).thenReturn(null); + when(result.hasMultipleFiles()).thenReturn(false); + when(result.hasFiles()).thenReturn(false); // -> triggert den Fallback + when(result.getResult()).thenReturn("SIMPLE-RESULT"); - // Setup user session with job authorization - java.util.Set userJobIds = new java.util.HashSet<>(); - userJobIds.add(jobId); - session.setAttribute("userJobIds", userJobIds); + when(taskManager.getJobResult("job11")).thenReturn(result); - 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()); + mvc.perform(get("/api/v1/general/job/{jobId}/result", "job11")) + .andExpect(status().isOk()) + .andExpect(content().string("SIMPLE-RESULT")); } @Test - void testCancelJob_AlreadyComplete() { - // Arrange - String jobId = "completed-job"; - JobResult jobResult = new JobResult(); - jobResult.setJobId(jobId); - jobResult.setComplete(true); + void cancelJob_unauthorized_returns403() throws Exception { + when(session.getAttribute("userJobIds")).thenReturn(null); - // 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")); + mvc.perform(delete("/api/v1/general/job/{jobId}", "J1")) + .andExpect(status().isForbidden()) + .andExpect( + jsonPath("$.message").value("You are not authorized to cancel this job")); } @Test - void testCancelJob_Unauthorized() { - // Arrange - String jobId = "unauthorized-job"; + void cancelJob_inQueue_success() throws Exception { + when(session.getAttribute("userJobIds")).thenReturn(Set.of("J2")); + when(jobQueue.isJobQueued("J2")).thenReturn(true); + when(jobQueue.getJobPosition("J2")).thenReturn(5); + when(jobQueue.cancelJob("J2")).thenReturn(true); - // 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); + mvc.perform(delete("/api/v1/general/job/{jobId}", "J2")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("Job cancelled successfully")) + .andExpect(jsonPath("$.wasQueued").value(true)) + .andExpect(jsonPath("$.queuePosition").value(5)); + } - // Act - ResponseEntity response = controller.cancelJob(jobId); + @Test + void cancelJob_taskManager_cancel_success_when_not_queued() throws Exception { + when(session.getAttribute("userJobIds")).thenReturn(Set.of("J3")); + when(jobQueue.isJobQueued("J3")).thenReturn(false); - // Assert - assertEquals(HttpStatus.FORBIDDEN, response.getStatusCode()); + JobResult result = mock(JobResult.class); + when(result.isComplete()).thenReturn(false); + when(taskManager.getJobResult("J3")).thenReturn(result); - @SuppressWarnings("unchecked") - Map responseBody = (Map) response.getBody(); - assertEquals("You are not authorized to cancel this job", responseBody.get("message")); + mvc.perform(delete("/api/v1/general/job/{jobId}", "J3")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("Job cancelled successfully")) + .andExpect(jsonPath("$.wasQueued").value(false)) + .andExpect(jsonPath("$.queuePosition").value("n/a")); - // 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()); + verify(taskManager).setError(eq("J3"), any(String.class)); + } + + @Test + void cancelJob_notFound_returns404() throws Exception { + when(session.getAttribute("userJobIds")).thenReturn(Set.of("J4")); + when(jobQueue.isJobQueued("J4")).thenReturn(false); + when(taskManager.getJobResult("J4")).thenReturn(null); + + mvc.perform(delete("/api/v1/general/job/{jobId}", "J4")).andExpect(status().isNotFound()); + } + + @Test + void cancelJob_alreadyComplete_returns400() throws Exception { + when(session.getAttribute("userJobIds")).thenReturn(Set.of("J5")); + when(jobQueue.isJobQueued("J5")).thenReturn(false); + + JobResult result = mock(JobResult.class); + when(result.isComplete()).thenReturn(true); + when(taskManager.getJobResult("J5")).thenReturn(result); + + mvc.perform(delete("/api/v1/general/job/{jobId}", "J5")) + .andExpect(status().isBadRequest()) + .andExpect( + jsonPath("$.message").value("Cannot cancel job that is already complete")); + } + + @Test + void cancelJob_unknown_failure_returns500() throws Exception { + // User besitzt den Job + when(session.getAttribute("userJobIds")).thenReturn(java.util.Set.of("J6")); + // Nicht in Queue + when(jobQueue.isJobQueued("J6")).thenReturn(false); + + // 1. Aufruf im Mittelteil: completed -> kein setError -> cancelled bleibt false + JobResult completed = mock(JobResult.class); + when(completed.isComplete()).thenReturn(true); + + // 2. Aufruf im finalen else: nicht complete -> 500 "unknown reason" + JobResult notComplete = mock(JobResult.class); + when(notComplete.isComplete()).thenReturn(false); + + when(taskManager.getJobResult("J6")) + .thenReturn(completed) // erster Aufruf (inside if (!cancelled)) + .thenReturn(notComplete); // zweiter Aufruf (final else) + + mvc.perform(delete("/api/v1/general/job/{jobId}", "J6")) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.message").value("Failed to cancel job for unknown reason")); + } + + @Test + void cancelJob_unauthorized_wrong_attribute_type_returns403() throws Exception { + when(session.getAttribute("userJobIds")).thenReturn(java.util.List.of("JX")); // kein Set + mvc.perform(delete("/api/v1/general/job/{jobId}", "JX")) + .andExpect(status().isForbidden()) + .andExpect( + jsonPath("$.message").value("You are not authorized to cancel this job")); + } + + @Test + void cancelJob_inQueue_cancelFalse_then_taskManager_setsError_ok() throws Exception { + when(session.getAttribute("userJobIds")).thenReturn(java.util.Set.of("JQ")); + when(jobQueue.isJobQueued("JQ")).thenReturn(true); + when(jobQueue.getJobPosition("JQ")).thenReturn(2); + when(jobQueue.cancelJob("JQ")).thenReturn(false); // zwingt den zweiten Pfad + + JobResult notComplete = mock(JobResult.class); + when(notComplete.isComplete()).thenReturn(false); + when(taskManager.getJobResult("JQ")).thenReturn(notComplete); + + mvc.perform(delete("/api/v1/general/job/{jobId}", "JQ")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("Job cancelled successfully")) + .andExpect(jsonPath("$.wasQueued").value(true)) + .andExpect(jsonPath("$.queuePosition").value(2)); + + verify(taskManager).setError(eq("JQ"), any(String.class)); + } + + @Test + void getJobFiles_notFound() throws Exception { + when(taskManager.getJobResult("JJ")).thenReturn(null); + mvc.perform(get("/api/v1/general/job/{jobId}/result/files", "JJ")) + .andExpect(status().isNotFound()); + } + + @Test + void getJobFiles_notComplete() throws Exception { + JobResult result = mock(JobResult.class); + when(result.isComplete()).thenReturn(false); + when(taskManager.getJobResult("JJ")).thenReturn(result); + + mvc.perform(get("/api/v1/general/job/{jobId}/result/files", "JJ")) + .andExpect(status().isBadRequest()) + .andExpect( + content() + .string( + org.hamcrest.Matchers.containsString( + "Job is not complete yet"))); + } + + @Test + void getJobFiles_error() throws Exception { + JobResult result = mock(JobResult.class); + when(result.isComplete()).thenReturn(true); + when(result.getError()).thenReturn("E"); + when(taskManager.getJobResult("JJ")).thenReturn(result); + + mvc.perform(get("/api/v1/general/job/{jobId}/result/files", "JJ")) + .andExpect(status().isBadRequest()) + .andExpect(content().string(org.hamcrest.Matchers.containsString("Job failed: E"))); + } + + @Test + void getJobFiles_success() throws Exception { + JobResult result = mock(JobResult.class); + when(result.isComplete()).thenReturn(true); + when(result.getError()).thenReturn(null); + ResultFile f1 = mock(ResultFile.class); + ResultFile f2 = mock(ResultFile.class); + when(result.getAllResultFiles()).thenReturn(List.of(f1, f2)); + when(taskManager.getJobResult("JJ")).thenReturn(result); + + mvc.perform(get("/api/v1/general/job/{jobId}/result/files", "JJ")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.jobId").value("JJ")) + .andExpect(jsonPath("$.fileCount").value(2)) + .andExpect(jsonPath("$.files.length()").value(2)); + } + + @Test + void getFileMetadata_notFound_when_file_missing() throws Exception { + when(fileStorage.fileExists("F1")).thenReturn(false); + mvc.perform(get("/api/v1/general/files/{fileId}/metadata", "F1")) + .andExpect(status().isNotFound()); + } + + @Test + void getFileMetadata_found_with_metadata() throws Exception { + when(fileStorage.fileExists("F2")).thenReturn(true); + ResultFile rf = mock(ResultFile.class); + when(rf.getFileId()).thenReturn("F2"); + when(rf.getFileName()).thenReturn("name.pdf"); + when(rf.getContentType()).thenReturn("application/pdf"); + when(taskManager.findResultFileByFileId("F2")).thenReturn(rf); + + mvc.perform(get("/api/v1/general/files/{fileId}/metadata", "F2")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.fileId").value("F2")) + .andExpect(jsonPath("$.fileName").value("name.pdf")) + .andExpect(jsonPath("$.contentType").value("application/pdf")); + } + + @Test + void getFileMetadata_found_without_metadata_returns_basic_info() throws Exception { + when(fileStorage.fileExists("F3")).thenReturn(true); + when(taskManager.findResultFileByFileId("F3")).thenReturn(null); + when(fileStorage.getFileSize("F3")).thenReturn(123L); + + mvc.perform(get("/api/v1/general/files/{fileId}/metadata", "F3")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.fileId").value("F3")) + .andExpect(jsonPath("$.fileName").value("unknown")) + .andExpect(jsonPath("$.contentType").value("application/octet-stream")) + .andExpect(jsonPath("$.fileSize").value(123)); + } + + @Test + void getFileMetadata_error_on_fileExists_returns500() throws Exception { + when(fileStorage.fileExists("FERR2")).thenThrow(new RuntimeException("fail-exists")); + + mvc.perform(get("/api/v1/general/files/{fileId}/metadata", "FERR2")) + .andExpect(status().isInternalServerError()) + .andExpect( + content() + .string( + org.hamcrest.Matchers.containsString( + "Error retrieving file metadata:"))); + } + + @Test + void downloadFile_notFound() throws Exception { + when(fileStorage.fileExists("FX")).thenReturn(false); + mvc.perform(get("/api/v1/general/files/{fileId}", "FX")).andExpect(status().isNotFound()); + } + + @Test + void downloadFile_success_with_metadata() throws Exception { + when(fileStorage.fileExists("FY")).thenReturn(true); + when(fileStorage.retrieveBytes("FY")).thenReturn("DATA".getBytes(StandardCharsets.UTF_8)); + ResultFile rf = mock(ResultFile.class); + when(rf.getFileName()).thenReturn("out.txt"); + when(rf.getContentType()).thenReturn("text/plain"); + when(taskManager.findResultFileByFileId("FY")).thenReturn(rf); + + mvc.perform(get("/api/v1/general/files/{fileId}", "FY")) + .andExpect(status().isOk()) + .andExpect(header().string("Content-Type", "text/plain")) + .andExpect( + header().string( + "Content-Disposition", + org.hamcrest.Matchers.containsString("out.txt"))) + .andExpect(content().bytes("DATA".getBytes(StandardCharsets.UTF_8))); + } + + @Test + void downloadFile_error_returns500() throws Exception { + when(fileStorage.fileExists("FZ")).thenReturn(true); + when(fileStorage.retrieveBytes("FZ")).thenThrow(new RuntimeException("oops")); + + mvc.perform(get("/api/v1/general/files/{fileId}", "FZ")) + .andExpect(status().isInternalServerError()) + .andExpect( + content() + .string( + org.hamcrest.Matchers.containsString( + "Error retrieving file:"))); + } + + @Test + void downloadFile_filename_encoding_fallback_on_exception() throws Exception { + when(fileStorage.fileExists("FENC")).thenReturn(true); + when(fileStorage.retrieveBytes("FENC")) + .thenReturn("X".getBytes(java.nio.charset.StandardCharsets.UTF_8)); + + // ResultFile mit NULL-Dateiname -> URLEncoder.encode(...) wirft NPE -> Catch-Fallback + ResultFile rf = mock(ResultFile.class); + when(rf.getFileName()).thenReturn(null); + when(rf.getContentType()).thenReturn("text/plain"); + when(taskManager.findResultFileByFileId("FENC")).thenReturn(rf); + + mvc.perform(get("/api/v1/general/files/{fileId}", "FENC")) + .andExpect(status().isOk()) + .andExpect(header().string("Content-Type", "text/plain")) + // Fallback-Header: nur filename=..., kein filename*= + .andExpect(header().string("Content-Disposition", "attachment; filename=\"null\"")) + .andExpect(content().bytes("X".getBytes(java.nio.charset.StandardCharsets.UTF_8))); + } + + @Test + void downloadFile_success_without_metadata_defaults() throws Exception { + when(fileStorage.fileExists("FD")).thenReturn(true); + when(fileStorage.retrieveBytes("FD")) + .thenReturn("D".getBytes(java.nio.charset.StandardCharsets.UTF_8)); + // keine Metadata + when(taskManager.findResultFileByFileId("FD")).thenReturn(null); + + mvc.perform(get("/api/v1/general/files/{fileId}", "FD")) + .andExpect(status().isOk()) + .andExpect(header().string("Content-Type", "application/octet-stream")) + .andExpect( + header().string( + "Content-Disposition", + org.hamcrest.Matchers.allOf( + org.hamcrest.Matchers.containsString( + "filename=\"download\""), + org.hamcrest.Matchers.containsString( + "filename*=")))) + .andExpect(content().bytes("D".getBytes(java.nio.charset.StandardCharsets.UTF_8))); } }