tests and formatting

This commit is contained in:
Anthony Stirling 2025-07-10 15:22:37 +01:00
parent bb9f1d4f8b
commit 624e04a783
8 changed files with 142 additions and 102 deletions

View File

@ -10,7 +10,6 @@ import java.util.Properties;
import java.util.function.Predicate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -151,9 +150,8 @@ public class AppConfig {
@Bean(name = "activeSecurity")
public boolean missingActiveSecurity() {
return ClassUtils.isPresent(
"stirling.software.proprietary.security.configuration.SecurityConfiguration",
this.getClass().getClassLoader()
);
"stirling.software.proprietary.security.configuration.SecurityConfiguration",
this.getClass().getClassLoader());
}
@Bean(name = "directoryFilter")

View File

@ -59,7 +59,6 @@ public class JobResult {
.build();
}
/**
* Mark this job as complete with a general result
*
@ -101,13 +100,15 @@ public class JobResult {
* @param contentType The content type of the file
* @param fileSize The size of the file in bytes
*/
public void completeWithSingleFile(String fileId, String fileName, String contentType, long fileSize) {
ResultFile resultFile = ResultFile.builder()
.fileId(fileId)
.fileName(fileName)
.contentType(contentType)
.fileSize(fileSize)
.build();
public void completeWithSingleFile(
String fileId, String fileName, String contentType, long fileSize) {
ResultFile resultFile =
ResultFile.builder()
.fileId(fileId)
.fileName(fileName)
.contentType(contentType)
.fileSize(fileSize)
.build();
completeWithFiles(List.of(resultFile));
}

View File

@ -23,4 +23,4 @@ public class ResultFile {
/** Size of the file in bytes */
private long fileSize;
}
}

View File

@ -140,11 +140,11 @@ public class FileStorage {
*/
public long getFileSize(String fileId) throws IOException {
Path filePath = getFilePath(fileId);
if (!Files.exists(filePath)) {
throw new IOException("File not found with ID: " + fileId);
}
return Files.size(filePath);
}
@ -153,7 +153,8 @@ public class FileStorage {
*
* @param fileId The ID of the file
* @return The path to the file
* @throws IllegalArgumentException if fileId contains path traversal characters or resolves outside base directory
* @throws IllegalArgumentException if fileId contains path traversal characters or resolves
* outside base directory
*/
private Path getFilePath(String fileId) {
// Validate fileId to prevent path traversal

View File

@ -15,11 +15,10 @@ import java.util.concurrent.TimeUnit;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.springframework.http.MediaType;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import jakarta.annotation.PreDestroy;
@ -91,30 +90,36 @@ public class TaskManager {
public void setFileResult(
String jobId, String fileId, String originalFileName, String contentType) {
JobResult jobResult = getOrCreateJobResult(jobId);
// Check if this is a ZIP file that should be extracted
if (isZipFile(contentType, originalFileName)) {
try {
List<ResultFile> extractedFiles = extractZipToIndividualFiles(fileId, originalFileName);
List<ResultFile> extractedFiles =
extractZipToIndividualFiles(fileId, originalFileName);
if (!extractedFiles.isEmpty()) {
jobResult.completeWithFiles(extractedFiles);
log.debug("Set multiple file results for job ID: {} with {} files extracted from ZIP",
jobId, extractedFiles.size());
log.debug(
"Set multiple file results for job ID: {} with {} files extracted from ZIP",
jobId,
extractedFiles.size());
return;
}
} catch (Exception e) {
log.warn("Failed to extract ZIP file for job {}: {}. Falling back to single file result.",
jobId, e.getMessage());
log.warn(
"Failed to extract ZIP file for job {}: {}. Falling back to single file result.",
jobId,
e.getMessage());
}
}
// Handle as single file using new ResultFile approach
try {
long fileSize = fileStorage.getFileSize(fileId);
jobResult.completeWithSingleFile(fileId, originalFileName, contentType, fileSize);
log.debug("Set single file result for job ID: {} with file ID: {}", jobId, fileId);
} catch (Exception e) {
log.warn("Failed to get file size for job {}: {}. Using size 0.", jobId, e.getMessage());
log.warn(
"Failed to get file size for job {}: {}. Using size 0.", jobId, e.getMessage());
jobResult.completeWithSingleFile(fileId, originalFileName, contentType, 0);
}
}
@ -128,7 +133,10 @@ public class TaskManager {
public void setMultipleFileResults(String jobId, List<ResultFile> resultFiles) {
JobResult jobResult = getOrCreateJobResult(jobId);
jobResult.completeWithFiles(resultFiles);
log.debug("Set multiple file results for job ID: {} with {} files", jobId, resultFiles.size());
log.debug(
"Set multiple file results for job ID: {} with {} files",
jobId,
resultFiles.size());
}
/**
@ -329,32 +337,30 @@ public class TaskManager {
}
}
/**
* Check if a file is a ZIP file based on content type and filename
*/
/** Check if a file is a ZIP file based on content type and filename */
private boolean isZipFile(String contentType, String fileName) {
if (contentType != null && (contentType.equals("application/zip") ||
contentType.equals("application/x-zip-compressed"))) {
if (contentType != null
&& (contentType.equals("application/zip")
|| contentType.equals("application/x-zip-compressed"))) {
return true;
}
if (fileName != null && fileName.toLowerCase().endsWith(".zip")) {
return true;
}
return false;
}
/**
* Extract a ZIP file into individual files and store them
*/
private List<ResultFile> extractZipToIndividualFiles(String zipFileId, String originalZipFileName)
throws IOException {
/** Extract a ZIP file into individual files and store them */
private List<ResultFile> extractZipToIndividualFiles(
String zipFileId, String originalZipFileName) throws IOException {
List<ResultFile> extractedFiles = new ArrayList<>();
MultipartFile zipFile = fileStorage.retrieveFile(zipFileId);
try (ZipInputStream zipIn = new ZipInputStream(new ByteArrayInputStream(zipFile.getBytes()))) {
try (ZipInputStream zipIn =
new ZipInputStream(new ByteArrayInputStream(zipFile.getBytes()))) {
ZipEntry entry;
while ((entry = zipIn.getNextEntry()) != null) {
if (!entry.isDirectory()) {
@ -366,24 +372,28 @@ public class TaskManager {
out.write(buffer, 0, bytesRead);
}
byte[] fileContent = out.toByteArray();
String contentType = determineContentType(entry.getName());
String individualFileId = fileStorage.storeBytes(fileContent, entry.getName());
ResultFile resultFile = ResultFile.builder()
.fileId(individualFileId)
.fileName(entry.getName())
.contentType(contentType)
.fileSize(fileContent.length)
.build();
ResultFile resultFile =
ResultFile.builder()
.fileId(individualFileId)
.fileName(entry.getName())
.contentType(contentType)
.fileSize(fileContent.length)
.build();
extractedFiles.add(resultFile);
log.debug("Extracted file: {} (size: {} bytes)", entry.getName(), fileContent.length);
log.debug(
"Extracted file: {} (size: {} bytes)",
entry.getName(),
fileContent.length);
}
zipIn.closeEntry();
}
}
// Clean up the original ZIP file after extraction
try {
fileStorage.deleteFile(zipFileId);
@ -391,18 +401,16 @@ public class TaskManager {
} catch (Exception e) {
log.warn("Failed to clean up original ZIP file {}: {}", zipFileId, e.getMessage());
}
return extractedFiles;
}
/**
* Determine content type based on file extension
*/
/** Determine content type based on file extension */
private String determineContentType(String fileName) {
if (fileName == null) {
return MediaType.APPLICATION_OCTET_STREAM_VALUE;
}
String lowerName = fileName.toLowerCase();
if (lowerName.endsWith(".pdf")) {
return MediaType.APPLICATION_PDF_VALUE;
@ -421,9 +429,7 @@ public class TaskManager {
}
}
/**
* Clean up files associated with a job result
*/
/** Clean up files associated with a job result */
private void cleanupJobFiles(JobResult result, String jobId) {
// Clean up all result files
if (result.hasFiles()) {
@ -431,16 +437,17 @@ public class TaskManager {
try {
fileStorage.deleteFile(resultFile.getFileId());
} catch (Exception e) {
log.warn("Failed to delete file {} for job {}: {}",
resultFile.getFileId(), jobId, e.getMessage());
log.warn(
"Failed to delete file {} for job {}: {}",
resultFile.getFileId(),
jobId,
e.getMessage());
}
}
}
}
/**
* Find the ResultFile metadata for a given file ID by searching through all job results
*/
/** Find the ResultFile metadata for a given file ID by searching through all job results */
public ResultFile findResultFileByFileId(String fileId) {
for (JobResult jobResult : jobResults.values()) {
if (jobResult.hasFiles()) {

View File

@ -18,6 +18,7 @@ import org.springframework.test.util.ReflectionTestUtils;
import stirling.software.common.model.job.JobResult;
import stirling.software.common.model.job.JobStats;
import stirling.software.common.model.job.ResultFile;
class TaskManagerTest {
@ -73,13 +74,17 @@ class TaskManagerTest {
}
@Test
void testSetFileResult() {
void testSetFileResult() throws Exception {
// Arrange
String jobId = UUID.randomUUID().toString();
taskManager.createTask(jobId);
String fileId = "file-id";
String originalFileName = "test.pdf";
String contentType = "application/pdf";
long fileSize = 1024L;
// Mock the fileStorage.getFileSize() call
when(fileStorage.getFileSize(fileId)).thenReturn(fileSize);
// Act
taskManager.setFileResult(jobId, fileId, originalFileName, contentType);
@ -88,9 +93,17 @@ class TaskManagerTest {
JobResult result = taskManager.getJobResult(jobId);
assertNotNull(result);
assertTrue(result.isComplete());
assertEquals(fileId, result.getFileId());
assertEquals(originalFileName, result.getOriginalFileName());
assertEquals(contentType, result.getContentType());
assertTrue(result.hasFiles());
assertFalse(result.hasMultipleFiles());
var resultFiles = result.getAllResultFiles();
assertEquals(1, resultFiles.size());
ResultFile resultFile = resultFiles.get(0);
assertEquals(fileId, resultFile.getFileId());
assertEquals(originalFileName, resultFile.getFileName());
assertEquals(contentType, resultFile.getContentType());
assertEquals(fileSize, resultFile.getFileSize());
assertNotNull(result.getCompletedAt());
}
@ -163,8 +176,11 @@ class TaskManagerTest {
}
@Test
void testGetJobStats() {
void testGetJobStats() throws Exception {
// Arrange
// Mock fileStorage.getFileSize for file operations
when(fileStorage.getFileSize("file-id")).thenReturn(1024L);
// 1. Create active job
String activeJobId = "active-job";
taskManager.createTask(activeJobId);
@ -216,9 +232,15 @@ class TaskManagerTest {
LocalDateTime oldTime = LocalDateTime.now().minusHours(1);
ReflectionTestUtils.setField(oldJob, "completedAt", oldTime);
ReflectionTestUtils.setField(oldJob, "complete", true);
ReflectionTestUtils.setField(oldJob, "fileId", "file-id");
ReflectionTestUtils.setField(oldJob, "originalFileName", "test.pdf");
ReflectionTestUtils.setField(oldJob, "contentType", "application/pdf");
// Create a ResultFile and set it using the new approach
ResultFile resultFile = ResultFile.builder()
.fileId("file-id")
.fileName("test.pdf")
.contentType("application/pdf")
.fileSize(1024L)
.build();
ReflectionTestUtils.setField(oldJob, "resultFiles", java.util.List.of(resultFile));
when(fileStorage.deleteFile("file-id")).thenReturn(true);

View File

@ -87,11 +87,14 @@ public class JobController {
if (result.hasMultipleFiles()) {
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(Map.of(
"jobId", jobId,
"hasMultipleFiles", true,
"files", result.getAllResultFiles()
));
.body(
Map.of(
"jobId",
jobId,
"hasMultipleFiles",
true,
"files",
result.getAllResultFiles()));
}
// Handle single file (download directly)
@ -102,7 +105,9 @@ public class JobController {
byte[] fileContent = fileStorage.retrieveBytes(singleFile.getFileId());
return ResponseEntity.ok()
.header("Content-Type", singleFile.getContentType())
.header("Content-Disposition", createContentDispositionHeader(singleFile.getFileName()))
.header(
"Content-Disposition",
createContentDispositionHeader(singleFile.getFileName()))
.body(fileContent);
} catch (Exception e) {
log.error("Error retrieving file for job {}: {}", jobId, e.getMessage(), e);
@ -208,11 +213,11 @@ public class JobController {
}
List<ResultFile> files = result.getAllResultFiles();
return ResponseEntity.ok(Map.of(
"jobId", jobId,
"fileCount", files.size(),
"files", files
));
return ResponseEntity.ok(
Map.of(
"jobId", jobId,
"fileCount", files.size(),
"files", files));
}
/**
@ -231,18 +236,22 @@ public class JobController {
// Find the file metadata from any job that contains this file
ResultFile resultFile = taskManager.findResultFileByFileId(fileId);
if (resultFile != null) {
return ResponseEntity.ok(resultFile);
} else {
// File exists but no metadata found, get basic info efficiently
long fileSize = fileStorage.getFileSize(fileId);
return ResponseEntity.ok(Map.of(
"fileId", fileId,
"fileName", "unknown",
"contentType", "application/octet-stream",
"fileSize", fileSize
));
return ResponseEntity.ok(
Map.of(
"fileId",
fileId,
"fileName",
"unknown",
"contentType",
"application/octet-stream",
"fileSize",
fileSize));
}
} catch (Exception e) {
log.error("Error retrieving file metadata {}: {}", fileId, e.getMessage(), e);
@ -267,14 +276,15 @@ public class JobController {
// Retrieve file content
byte[] fileContent = fileStorage.retrieveBytes(fileId);
// Find the file metadata from any job that contains this file
// This is for getting the original filename and content type
ResultFile resultFile = taskManager.findResultFileByFileId(fileId);
String fileName = resultFile != null ? resultFile.getFileName() : "download";
String contentType = resultFile != null ? resultFile.getContentType() : "application/octet-stream";
String contentType =
resultFile != null ? resultFile.getContentType() : "application/octet-stream";
return ResponseEntity.ok()
.header("Content-Type", contentType)
.header("Content-Disposition", createContentDispositionHeader(fileName))
@ -286,7 +296,6 @@ public class JobController {
}
}
/**
* Create Content-Disposition header with UTF-8 filename support
*
@ -295,8 +304,9 @@ public class JobController {
*/
private String createContentDispositionHeader(String fileName) {
try {
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8)
.replace("+", "%20"); // URLEncoder uses + for spaces, but we want %20
String encodedFileName =
URLEncoder.encode(fileName, StandardCharsets.UTF_8)
.replace("+", "%20"); // URLEncoder uses + for spaces, but we want %20
return "attachment; filename=\"" + fileName + "\"; filename*=UTF-8''" + encodedFileName;
} catch (Exception e) {
// Fallback to basic filename if encoding fails

View File

@ -19,6 +19,7 @@ import jakarta.servlet.http.HttpSession;
import stirling.software.common.model.job.JobResult;
import stirling.software.common.model.job.JobStats;
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;
@ -138,7 +139,7 @@ class JobControllerTest {
JobResult mockResult = new JobResult();
mockResult.setJobId(jobId);
mockResult.completeWithFile(fileId, originalFileName, contentType);
mockResult.completeWithSingleFile(fileId, originalFileName, contentType, fileContent.length);
when(taskManager.getJobResult(jobId)).thenReturn(mockResult);
when(fileStorage.retrieveBytes(fileId)).thenReturn(fileContent);
@ -215,7 +216,7 @@ class JobControllerTest {
JobResult mockResult = new JobResult();
mockResult.setJobId(jobId);
mockResult.completeWithFile(fileId, originalFileName, contentType);
mockResult.completeWithSingleFile(fileId, originalFileName, contentType, 1024L);
when(taskManager.getJobResult(jobId)).thenReturn(mockResult);
when(fileStorage.retrieveBytes(fileId)).thenThrow(new RuntimeException("File not found"));