feat: Sprint 13 — Production Hardening (security fixes, CI gate, rate limiting, tests)
Deploy to TrueNAS / deploy (push) Failing after 12s
Deploy to TrueNAS / deploy (push) Failing after 12s
This commit is contained in:
@@ -267,6 +267,83 @@ class DocumentServiceTest {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Additional security tests for Sprint 13 ---
|
||||
|
||||
@Test
|
||||
void testUploadDocument_sanitizesPathTraversal_toBasename() throws IOException {
|
||||
// Verify that "../../etc/passwd" is stripped to just "passwd"
|
||||
MultipartFile file = mockValidFile("../../etc/passwd", "application/pdf", 512);
|
||||
when(documentRepository.save(any(Document.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
try (MockedStatic<Files> filesMock = mockStatic(Files.class)) {
|
||||
filesMock.when(() -> Files.createDirectories(any(Path.class))).thenReturn(null);
|
||||
filesMock.when(() -> Files.write(any(Path.class), any(byte[].class))).thenReturn(null);
|
||||
|
||||
Document result = documentService.uploadDocument(
|
||||
clubId, "Path Traversal Test", DocumentCategory.SONSTIGES,
|
||||
DocumentAccessLevel.ALL_MEMBERS, null, file, uploadedBy);
|
||||
|
||||
assertThat(result.getFilename()).isEqualTo("passwd");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUploadDocument_nullFilename_usesFallback() throws IOException {
|
||||
MultipartFile file = mockValidFile(null, "application/pdf", 512);
|
||||
when(documentRepository.save(any(Document.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
try (MockedStatic<Files> filesMock = mockStatic(Files.class)) {
|
||||
filesMock.when(() -> Files.createDirectories(any(Path.class))).thenReturn(null);
|
||||
filesMock.when(() -> Files.write(any(Path.class), any(byte[].class))).thenReturn(null);
|
||||
|
||||
Document result = documentService.uploadDocument(
|
||||
clubId, "Null Filename", DocumentCategory.SONSTIGES,
|
||||
DocumentAccessLevel.ALL_MEMBERS, null, file, uploadedBy);
|
||||
|
||||
// Null filename should produce a non-blank fallback (UUID)
|
||||
assertThat(result.getFilename()).isNotBlank();
|
||||
assertThat(result.getFilename()).doesNotContain("..");
|
||||
assertThat(result.getFilename()).doesNotContain("/");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUploadDocument_normalFilename_preserved() throws IOException {
|
||||
MultipartFile file = mockValidFile("report.pdf", "application/pdf", 1024);
|
||||
when(documentRepository.save(any(Document.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
try (MockedStatic<Files> filesMock = mockStatic(Files.class)) {
|
||||
filesMock.when(() -> Files.createDirectories(any(Path.class))).thenReturn(null);
|
||||
filesMock.when(() -> Files.write(any(Path.class), any(byte[].class))).thenReturn(null);
|
||||
|
||||
Document result = documentService.uploadDocument(
|
||||
clubId, "Normal File", DocumentCategory.PROTOKOLL,
|
||||
DocumentAccessLevel.ALL_MEMBERS, null, file, uploadedBy);
|
||||
|
||||
assertThat(result.getFilename()).isEqualTo("report.pdf");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDownloadDocument_documentNotFound_throwsException() {
|
||||
UUID nonExistentId = UUID.randomUUID();
|
||||
when(documentRepository.findById(nonExistentId)).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> documentService.downloadDocument(nonExistentId))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("not found");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteDocument_documentNotFound_throwsException() {
|
||||
UUID nonExistentId = UUID.randomUUID();
|
||||
when(documentRepository.findById(nonExistentId)).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> documentService.deleteDocument(nonExistentId, uploadedBy, clubId))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("not found");
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
private MultipartFile mockValidFile(String filename, String contentType, long size) {
|
||||
|
||||
Reference in New Issue
Block a user