feat(upload): add POST /api/documents/quick-upload endpoint for bulk file upload

Adds a new multipart endpoint that accepts multiple files and creates one
document per file without requiring any form metadata. Each document gets
title = filename-without-extension and status = UPLOADED.

- Fix storeDocument() to strip the file extension from the document title
- Validate content type (PDF/JPEG/PNG/TIFF) server-side; unsupported files
  are skipped and returned as per-file errors in QuickUploadResult
- Tests cover 401/403 auth, success path, and unsupported file type

Closes #66 (backend part)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-03-26 09:59:59 +01:00
parent 29a71f4421
commit 332b5b3c40
4 changed files with 126 additions and 1 deletions

View File

@@ -23,6 +23,7 @@ import java.util.Collections;
import java.util.List;
import java.util.UUID;
import static org.hamcrest.Matchers.containsString;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@@ -121,6 +122,50 @@ class DocumentControllerTest {
.andExpect(status().isOk());
}
// ─── POST /api/documents/quick-upload ────────────────────────────────────
@Test
void quickUpload_returns401_whenUnauthenticated() throws Exception {
mockMvc.perform(multipart("/api/documents/quick-upload"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser
void quickUpload_returns403_whenMissingWritePermission() throws Exception {
mockMvc.perform(multipart("/api/documents/quick-upload"))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(authorities = "WRITE_ALL")
void quickUpload_returns200_withValidPdfFile() throws Exception {
Document doc = Document.builder()
.id(UUID.randomUUID()).title("scan001").originalFilename("scan001.pdf").build();
when(documentService.storeDocument(any())).thenReturn(doc);
org.springframework.mock.web.MockMultipartFile file =
new org.springframework.mock.web.MockMultipartFile("files", "scan001.pdf", "application/pdf", new byte[]{1});
mockMvc.perform(multipart("/api/documents/quick-upload").file(file))
.andExpect(status().isOk())
.andExpect(jsonPath("$.created[0].title").value("scan001"))
.andExpect(jsonPath("$.errors").isEmpty());
}
@Test
@WithMockUser(authorities = "WRITE_ALL")
void quickUpload_skipsUnsupportedFileType_andReturnsError() throws Exception {
org.springframework.mock.web.MockMultipartFile file =
new org.springframework.mock.web.MockMultipartFile("files", "report.docx",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document", new byte[]{1});
mockMvc.perform(multipart("/api/documents/quick-upload").file(file))
.andExpect(status().isOk())
.andExpect(jsonPath("$.created").isEmpty())
.andExpect(jsonPath("$.errors[0]").value(containsString("report.docx")));
}
// ─── GET /api/documents/{id}/versions ────────────────────────────────────
@Test

View File

@@ -212,6 +212,44 @@ class DocumentServiceTest {
verify(documentVersionService).recordVersion(any(Document.class));
}
// ─── storeDocument ───────────────────────────────────────────────────────
@Test
void storeDocument_setsTitle_withoutFileExtension_forNewDocument() throws Exception {
org.springframework.mock.web.MockMultipartFile file =
new org.springframework.mock.web.MockMultipartFile("file", "scan001.pdf", "application/pdf", new byte[]{1});
FileService.UploadResult uploadResult = new FileService.UploadResult("documents/uuid_scan001.pdf", "abc123");
Document saved = Document.builder().id(UUID.randomUUID()).title("scan001").originalFilename("scan001.pdf").build();
when(documentRepository.findByOriginalFilename("scan001.pdf")).thenReturn(Optional.empty());
when(documentRepository.save(any())).thenReturn(saved);
when(fileService.uploadFile(any(), any())).thenReturn(uploadResult);
org.mockito.ArgumentCaptor<Document> captor = org.mockito.ArgumentCaptor.forClass(Document.class);
documentService.storeDocument(file);
verify(documentRepository).save(captor.capture());
assertThat(captor.getValue().getTitle()).isEqualTo("scan001");
}
@Test
void storeDocument_preservesExistingTitle_whenPlaceholderAlreadyExists() throws Exception {
org.springframework.mock.web.MockMultipartFile file =
new org.springframework.mock.web.MockMultipartFile("file", "scan001.pdf", "application/pdf", new byte[]{1});
FileService.UploadResult uploadResult = new FileService.UploadResult("documents/uuid_scan001.pdf", "abc123");
Document placeholder = Document.builder()
.id(UUID.randomUUID()).title("Brief an Oma").originalFilename("scan001.pdf")
.status(org.raddatz.familienarchiv.model.DocumentStatus.PLACEHOLDER).build();
when(documentRepository.findByOriginalFilename("scan001.pdf")).thenReturn(Optional.of(placeholder));
when(documentRepository.save(any())).thenReturn(placeholder);
when(fileService.uploadFile(any(), any())).thenReturn(uploadResult);
documentService.storeDocument(file);
assertThat(placeholder.getTitle()).isEqualTo("Brief an Oma");
}
// ─── backfillFileHashes ───────────────────────────────────────────────────
@Test