PDF & ZIP IMPORT

This commit is contained in:
Stefan Hardegger
2025-12-05 10:21:03 +01:00
parent b1b5bbbccd
commit 77aec8a849
18 changed files with 3490 additions and 22 deletions

View File

@@ -0,0 +1,296 @@
package com.storycove.service;
import com.storycove.dto.FileImportResponse;
import com.storycove.dto.PDFImportRequest;
import com.storycove.entity.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mock.web.MockMultipartFile;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* Tests for PDFImportService.
* Note: These tests mock the PDF parsing since Apache PDFBox is complex to test.
* Integration tests should be added separately to test actual PDF file parsing.
*/
@ExtendWith(MockitoExtension.class)
class PDFImportServiceTest {
@Mock
private StoryService storyService;
@Mock
private AuthorService authorService;
@Mock
private SeriesService seriesService;
@Mock
private TagService tagService;
@Mock
private HtmlSanitizationService sanitizationService;
@Mock
private ImageService imageService;
@Mock
private LibraryService libraryService;
@InjectMocks
private PDFImportService pdfImportService;
private PDFImportRequest testRequest;
private Story testStory;
private Author testAuthor;
private Series testSeries;
private UUID storyId;
@BeforeEach
void setUp() {
storyId = UUID.randomUUID();
testStory = new Story();
testStory.setId(storyId);
testStory.setTitle("Test Story");
testStory.setWordCount(1000);
testAuthor = new Author();
testAuthor.setId(UUID.randomUUID());
testAuthor.setName("Test Author");
testSeries = new Series();
testSeries.setId(UUID.randomUUID());
testSeries.setName("Test Series");
testRequest = new PDFImportRequest();
}
// ========================================
// File Validation Tests
// ========================================
@Test
@DisplayName("Should reject null PDF file")
void testNullPDFFile() {
// Arrange
testRequest.setPdfFile(null);
// Act
FileImportResponse response = pdfImportService.importPDF(testRequest);
// Assert
assertFalse(response.isSuccess());
assertEquals("PDF file is required", response.getMessage());
verify(storyService, never()).create(any(Story.class));
}
@Test
@DisplayName("Should reject empty PDF file")
void testEmptyPDFFile() {
// Arrange
MockMultipartFile emptyFile = new MockMultipartFile(
"file", "test.pdf", "application/pdf", new byte[0]
);
testRequest.setPdfFile(emptyFile);
// Act
FileImportResponse response = pdfImportService.importPDF(testRequest);
// Assert
assertFalse(response.isSuccess());
assertEquals("PDF file is required", response.getMessage());
verify(storyService, never()).create(any(Story.class));
}
@Test
@DisplayName("Should reject non-PDF file by extension")
void testInvalidFileExtension() {
// Arrange
MockMultipartFile invalidFile = new MockMultipartFile(
"file", "test.txt", "text/plain", "test content".getBytes()
);
testRequest.setPdfFile(invalidFile);
// Act
FileImportResponse response = pdfImportService.importPDF(testRequest);
// Assert
assertFalse(response.isSuccess());
assertTrue(response.getMessage().contains("Invalid PDF file format"));
verify(storyService, never()).create(any(Story.class));
}
@Test
@DisplayName("Should reject file exceeding 300MB size limit")
void testFileSizeExceedsLimit() {
// Arrange
long fileSize = 301L * 1024 * 1024; // 301 MB
MockMultipartFile largeFile = new MockMultipartFile(
"file", "test.pdf", "application/pdf", new byte[(int)Math.min(fileSize, 1000)]
) {
@Override
public long getSize() {
return fileSize;
}
};
testRequest.setPdfFile(largeFile);
// Act
FileImportResponse response = pdfImportService.importPDF(testRequest);
// Assert
assertFalse(response.isSuccess());
assertTrue(response.getMessage().contains("Invalid PDF file format"));
verify(storyService, never()).create(any(Story.class));
}
// ========================================
// Author Handling Tests
// ========================================
@Test
@DisplayName("Should require author name when not in metadata")
void testRequiresAuthorName() {
// Arrange - Create a minimal valid PDF (will fail parsing but test validation)
MockMultipartFile pdfFile = new MockMultipartFile(
"file", "test.pdf", "application/pdf",
"%PDF-1.4\n%%EOF".getBytes()
);
testRequest.setPdfFile(pdfFile);
testRequest.setAuthorName(null);
testRequest.setAuthorId(null);
// Act
FileImportResponse response = pdfImportService.importPDF(testRequest);
// Assert
assertFalse(response.isSuccess());
// Should fail during import because author is required
verify(storyService, never()).create(any(Story.class));
}
// ========================================
// Validation Method Tests
// ========================================
@Test
@DisplayName("Should validate PDF file successfully")
void testValidatePDFFile_Valid() {
// Arrange
MockMultipartFile pdfFile = new MockMultipartFile(
"file", "test.pdf", "application/pdf",
new byte[100]
);
// Act
List<String> errors = pdfImportService.validatePDFFile(pdfFile);
// Assert - Will have errors because it's not a real PDF, but tests the method exists
assertNotNull(errors);
}
@Test
@DisplayName("Should return errors for null file in validation")
void testValidatePDFFile_Null() {
// Act
List<String> errors = pdfImportService.validatePDFFile(null);
// Assert
assertNotNull(errors);
assertFalse(errors.isEmpty());
assertTrue(errors.get(0).contains("required"));
}
@Test
@DisplayName("Should return errors for empty file in validation")
void testValidatePDFFile_Empty() {
// Arrange
MockMultipartFile emptyFile = new MockMultipartFile(
"file", "test.pdf", "application/pdf", new byte[0]
);
// Act
List<String> errors = pdfImportService.validatePDFFile(emptyFile);
// Assert
assertNotNull(errors);
assertFalse(errors.isEmpty());
assertTrue(errors.get(0).contains("required"));
}
@Test
@DisplayName("Should return errors for oversized file in validation")
void testValidatePDFFile_Oversized() {
// Arrange
long fileSize = 301L * 1024 * 1024; // 301 MB
MockMultipartFile largeFile = new MockMultipartFile(
"file", "test.pdf", "application/pdf", new byte[1000]
) {
@Override
public long getSize() {
return fileSize;
}
};
// Act
List<String> errors = pdfImportService.validatePDFFile(largeFile);
// Assert
assertNotNull(errors);
assertFalse(errors.isEmpty());
assertTrue(errors.stream().anyMatch(e -> e.contains("300MB")));
}
// ========================================
// Integration Tests (Mocked)
// ========================================
@Test
@DisplayName("Should handle extraction images flag")
void testExtractImagesFlag() {
// Arrange
MockMultipartFile pdfFile = new MockMultipartFile(
"file", "test.pdf", "application/pdf",
"%PDF-1.4\n%%EOF".getBytes()
);
testRequest.setPdfFile(pdfFile);
testRequest.setAuthorName("Test Author");
testRequest.setExtractImages(false);
// Act
FileImportResponse response = pdfImportService.importPDF(testRequest);
// Assert - Will fail parsing but tests that the flag is accepted
assertNotNull(response);
}
@Test
@DisplayName("Should accept tags in request")
void testAcceptTags() {
// Arrange
MockMultipartFile pdfFile = new MockMultipartFile(
"file", "test.pdf", "application/pdf",
"%PDF-1.4\n%%EOF".getBytes()
);
testRequest.setPdfFile(pdfFile);
testRequest.setAuthorName("Test Author");
testRequest.setTags(Arrays.asList("tag1", "tag2"));
// Act
FileImportResponse response = pdfImportService.importPDF(testRequest);
// Assert - Will fail parsing but tests that tags are accepted
assertNotNull(response);
}
}

View File

@@ -0,0 +1,310 @@
package com.storycove.service;
import com.storycove.dto.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mock.web.MockMultipartFile;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* Tests for ZIPImportService.
*/
@ExtendWith(MockitoExtension.class)
class ZIPImportServiceTest {
@Mock
private EPUBImportService epubImportService;
@Mock
private PDFImportService pdfImportService;
@InjectMocks
private ZIPImportService zipImportService;
private ZIPImportRequest testImportRequest;
@BeforeEach
void setUp() {
testImportRequest = new ZIPImportRequest();
}
// ========================================
// File Validation Tests
// ========================================
@Test
@DisplayName("Should reject null ZIP file")
void testNullZIPFile() {
// Act
ZIPAnalysisResponse response = zipImportService.analyzeZIPFile(null);
// Assert
assertFalse(response.isSuccess());
assertEquals("ZIP file is required", response.getMessage());
}
@Test
@DisplayName("Should reject empty ZIP file")
void testEmptyZIPFile() {
// Arrange
MockMultipartFile emptyFile = new MockMultipartFile(
"file", "test.zip", "application/zip", new byte[0]
);
// Act
ZIPAnalysisResponse response = zipImportService.analyzeZIPFile(emptyFile);
// Assert
assertFalse(response.isSuccess());
assertEquals("ZIP file is required", response.getMessage());
}
@Test
@DisplayName("Should reject non-ZIP file")
void testInvalidFileType() {
// Arrange
MockMultipartFile invalidFile = new MockMultipartFile(
"file", "test.txt", "text/plain", "test content".getBytes()
);
// Act
ZIPAnalysisResponse response = zipImportService.analyzeZIPFile(invalidFile);
// Assert
assertFalse(response.isSuccess());
assertTrue(response.getMessage().contains("Invalid ZIP file format"));
}
@Test
@DisplayName("Should reject oversized ZIP file")
void testOversizedZIPFile() {
// Arrange
long fileSize = 1025L * 1024 * 1024; // 1025 MB (> 1GB limit)
MockMultipartFile largeFile = new MockMultipartFile(
"file", "test.zip", "application/zip", new byte[1000]
) {
@Override
public long getSize() {
return fileSize;
}
};
// Act
ZIPAnalysisResponse response = zipImportService.analyzeZIPFile(largeFile);
// Assert
assertFalse(response.isSuccess());
assertTrue(response.getMessage().contains("exceeds"));
assertTrue(response.getMessage().contains("1024MB") || response.getMessage().contains("1GB"));
}
// ========================================
// Import Request Validation Tests
// ========================================
@Test
@DisplayName("Should reject import with invalid session ID")
void testInvalidSessionId() {
// Arrange
testImportRequest.setZipSessionId("invalid-session-id");
testImportRequest.setSelectedFiles(Arrays.asList("file1.epub"));
// Act
ZIPImportResponse response = zipImportService.importFromZIP(testImportRequest);
// Assert
assertFalse(response.isSuccess());
assertTrue(response.getMessage().contains("Invalid") || response.getMessage().contains("expired"));
}
@Test
@DisplayName("Should reject import with no selected files")
void testNoSelectedFiles() {
// Arrange
testImportRequest.setZipSessionId("some-session-id");
testImportRequest.setSelectedFiles(Collections.emptyList());
// Act
ZIPImportResponse response = zipImportService.importFromZIP(testImportRequest);
// Assert
assertFalse(response.isSuccess());
assertTrue(response.getMessage().contains("No files selected") || response.getMessage().contains("Invalid"));
}
@Test
@DisplayName("Should reject import with null selected files")
void testNullSelectedFiles() {
// Arrange
testImportRequest.setZipSessionId("some-session-id");
testImportRequest.setSelectedFiles(null);
// Act
ZIPImportResponse response = zipImportService.importFromZIP(testImportRequest);
// Assert
assertFalse(response.isSuccess());
assertTrue(response.getMessage().contains("No files selected") || response.getMessage().contains("Invalid"));
}
// ========================================
// ZIP Analysis Tests
// ========================================
@Test
@DisplayName("Should handle corrupted ZIP file gracefully")
void testCorruptedZIPFile() {
// Arrange
MockMultipartFile corruptedFile = new MockMultipartFile(
"file", "test.zip", "application/zip",
"PK\3\4corrupted data".getBytes()
);
// Act
ZIPAnalysisResponse response = zipImportService.analyzeZIPFile(corruptedFile);
// Assert
assertFalse(response.isSuccess());
assertNotNull(response.getMessage());
}
// ========================================
// Helper Method Tests
// ========================================
@Test
@DisplayName("Should accept default metadata in import request")
void testDefaultMetadata() {
// Arrange
testImportRequest.setZipSessionId("test-session");
testImportRequest.setSelectedFiles(Arrays.asList("file1.epub"));
testImportRequest.setDefaultAuthorName("Default Author");
testImportRequest.setDefaultTags(Arrays.asList("tag1", "tag2"));
// Act - will fail due to invalid session, but tests that metadata is accepted
ZIPImportResponse response = zipImportService.importFromZIP(testImportRequest);
// Assert
assertNotNull(response);
assertFalse(response.isSuccess()); // Expected to fail due to invalid session
}
@Test
@DisplayName("Should accept per-file metadata in import request")
void testPerFileMetadata() {
// Arrange
Map<String, ZIPImportRequest.FileImportMetadata> fileMetadata = new HashMap<>();
ZIPImportRequest.FileImportMetadata metadata = new ZIPImportRequest.FileImportMetadata();
metadata.setAuthorName("Specific Author");
metadata.setTags(Arrays.asList("tag1"));
fileMetadata.put("file1.epub", metadata);
testImportRequest.setZipSessionId("test-session");
testImportRequest.setSelectedFiles(Arrays.asList("file1.epub"));
testImportRequest.setFileMetadata(fileMetadata);
// Act - will fail due to invalid session, but tests that metadata is accepted
ZIPImportResponse response = zipImportService.importFromZIP(testImportRequest);
// Assert
assertNotNull(response);
assertFalse(response.isSuccess()); // Expected to fail due to invalid session
}
@Test
@DisplayName("Should accept createMissing flags")
void testCreateMissingFlags() {
// Arrange
testImportRequest.setZipSessionId("test-session");
testImportRequest.setSelectedFiles(Arrays.asList("file1.epub"));
testImportRequest.setCreateMissingAuthor(false);
testImportRequest.setCreateMissingSeries(false);
testImportRequest.setExtractImages(false);
// Act - will fail due to invalid session, but tests that flags are accepted
ZIPImportResponse response = zipImportService.importFromZIP(testImportRequest);
// Assert
assertNotNull(response);
}
// ========================================
// Response Object Tests
// ========================================
@Test
@DisplayName("ZIPImportResponse should calculate statistics correctly")
void testZIPImportResponseStatistics() {
// Arrange
List<FileImportResponse> results = new ArrayList<>();
FileImportResponse success1 = FileImportResponse.success(UUID.randomUUID(), "Story 1", "EPUB");
FileImportResponse success2 = FileImportResponse.success(UUID.randomUUID(), "Story 2", "PDF");
FileImportResponse failure = FileImportResponse.error("Import failed", "story3.epub");
results.add(success1);
results.add(success2);
results.add(failure);
// Act
ZIPImportResponse response = ZIPImportResponse.create(results);
// Assert
assertNotNull(response);
assertEquals(3, response.getTotalFiles());
assertEquals(2, response.getSuccessfulImports());
assertEquals(1, response.getFailedImports());
assertTrue(response.isSuccess()); // Partial success
assertTrue(response.getMessage().contains("2 imported"));
}
@Test
@DisplayName("ZIPImportResponse should handle all failures")
void testZIPImportResponseAllFailures() {
// Arrange
List<FileImportResponse> results = new ArrayList<>();
results.add(FileImportResponse.error("Error 1", "file1.epub"));
results.add(FileImportResponse.error("Error 2", "file2.pdf"));
// Act
ZIPImportResponse response = ZIPImportResponse.create(results);
// Assert
assertNotNull(response);
assertEquals(2, response.getTotalFiles());
assertEquals(0, response.getSuccessfulImports());
assertEquals(2, response.getFailedImports());
assertFalse(response.isSuccess());
assertTrue(response.getMessage().contains("failed"));
}
@Test
@DisplayName("ZIPImportResponse should handle all successes")
void testZIPImportResponseAllSuccesses() {
// Arrange
List<FileImportResponse> results = new ArrayList<>();
results.add(FileImportResponse.success(UUID.randomUUID(), "Story 1", "EPUB"));
results.add(FileImportResponse.success(UUID.randomUUID(), "Story 2", "PDF"));
// Act
ZIPImportResponse response = ZIPImportResponse.create(results);
// Assert
assertNotNull(response);
assertEquals(2, response.getTotalFiles());
assertEquals(2, response.getSuccessfulImports());
assertEquals(0, response.getFailedImports());
assertTrue(response.isSuccess());
assertTrue(response.getMessage().contains("All files imported successfully"));
}
}