feat: Sprint 13 — Production Hardening (security fixes, CI gate, rate limiting, tests)
Deploy to TrueNAS / deploy (push) Failing after 12s

This commit is contained in:
Patrick Plate
2026-06-18 16:08:05 +02:00
parent 279487067e
commit f9a87efb7a
17 changed files with 1962 additions and 107 deletions
@@ -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) {