diff --git a/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java b/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java index d66e935a..9f3e7c6f 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java @@ -13,6 +13,7 @@ import java.util.UUID; import org.raddatz.familienarchiv.dto.DocumentUpdateDTO; import org.raddatz.familienarchiv.dto.DocumentVersionSummary; +import org.raddatz.familienarchiv.dto.IncompleteDocumentDTO; import org.raddatz.familienarchiv.exception.DomainException; import org.raddatz.familienarchiv.exception.ErrorCode; import org.raddatz.familienarchiv.model.Document; @@ -164,8 +165,9 @@ public class DocumentController { } @GetMapping("/incomplete") - public List getIncomplete() { - return documentService.findIncompleteDocuments(); + public List getIncomplete( + @RequestParam(defaultValue = "10") int size) { + return documentService.findIncompleteDocuments(size); } @GetMapping("/incomplete/next") diff --git a/backend/src/main/java/org/raddatz/familienarchiv/dto/IncompleteDocumentDTO.java b/backend/src/main/java/org/raddatz/familienarchiv/dto/IncompleteDocumentDTO.java new file mode 100644 index 00000000..38cf4e93 --- /dev/null +++ b/backend/src/main/java/org/raddatz/familienarchiv/dto/IncompleteDocumentDTO.java @@ -0,0 +1,10 @@ +package org.raddatz.familienarchiv.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.UUID; + +public record IncompleteDocumentDTO( + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) UUID id, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) String title +) {} diff --git a/backend/src/main/java/org/raddatz/familienarchiv/repository/DocumentRepository.java b/backend/src/main/java/org/raddatz/familienarchiv/repository/DocumentRepository.java index a878b67a..5614fa3b 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/repository/DocumentRepository.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/repository/DocumentRepository.java @@ -2,6 +2,8 @@ package org.raddatz.familienarchiv.repository; import org.raddatz.familienarchiv.model.Document; import org.raddatz.familienarchiv.model.DocumentStatus; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; @@ -46,6 +48,8 @@ public interface DocumentRepository extends JpaRepository, JpaSp List findByMetadataCompleteFalse(Sort sort); + Page findByMetadataCompleteFalse(Pageable pageable); + Optional findFirstByMetadataCompleteFalseAndIdNot(UUID id, Sort sort); @Query("SELECT DISTINCT d FROM Document d " + diff --git a/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java b/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java index 4b332b51..c9eb4a42 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java @@ -4,11 +4,13 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.raddatz.familienarchiv.dto.DocumentUpdateDTO; +import org.raddatz.familienarchiv.dto.IncompleteDocumentDTO; import org.raddatz.familienarchiv.model.Document; import org.raddatz.familienarchiv.model.DocumentStatus; import org.raddatz.familienarchiv.model.Person; import org.raddatz.familienarchiv.model.Tag; import org.raddatz.familienarchiv.repository.DocumentRepository; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.Specification; import org.raddatz.familienarchiv.exception.DomainException; @@ -313,8 +315,12 @@ public class DocumentService { return documentRepository.countByMetadataCompleteFalse(); } - public List findIncompleteDocuments() { - return documentRepository.findByMetadataCompleteFalse(Sort.by(Sort.Direction.DESC, "createdAt")); + public List findIncompleteDocuments(int size) { + PageRequest pageable = PageRequest.of(0, size, Sort.by(Sort.Direction.DESC, "createdAt")); + return documentRepository.findByMetadataCompleteFalse(pageable) + .stream() + .map(doc -> new IncompleteDocumentDTO(doc.getId(), doc.getTitle())) + .toList(); } public Optional findNextIncompleteDocument(UUID currentId) { diff --git a/backend/src/test/java/org/raddatz/familienarchiv/controller/DocumentControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/controller/DocumentControllerTest.java index d200c6f9..87b4c341 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/controller/DocumentControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/controller/DocumentControllerTest.java @@ -2,6 +2,7 @@ package org.raddatz.familienarchiv.controller; import org.junit.jupiter.api.Test; import org.raddatz.familienarchiv.dto.DocumentVersionSummary; +import org.raddatz.familienarchiv.dto.IncompleteDocumentDTO; import org.raddatz.familienarchiv.model.Document; import org.raddatz.familienarchiv.model.DocumentVersion; import org.raddatz.familienarchiv.security.PermissionAspect; @@ -25,6 +26,8 @@ import java.util.Optional; import java.util.UUID; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; @@ -315,16 +318,39 @@ class DocumentControllerTest { @Test @WithMockUser - void getIncomplete_returns200_withList() throws Exception { - Document doc = Document.builder() - .id(UUID.randomUUID()).title("Unvollständig").originalFilename("scan.pdf").build(); - when(documentService.findIncompleteDocuments()).thenReturn(List.of(doc)); + void getIncomplete_returns200_withDTOList() throws Exception { + UUID id = UUID.randomUUID(); + IncompleteDocumentDTO dto = new IncompleteDocumentDTO(id, "Unvollständig"); + when(documentService.findIncompleteDocuments(anyInt())).thenReturn(List.of(dto)); mockMvc.perform(get("/api/documents/incomplete")) .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(id.toString())) .andExpect(jsonPath("$[0].title").value("Unvollständig")); } + @Test + @WithMockUser + void getIncomplete_withSizeParam_passesItToService() throws Exception { + when(documentService.findIncompleteDocuments(5)).thenReturn(List.of()); + + mockMvc.perform(get("/api/documents/incomplete").param("size", "5")) + .andExpect(status().isOk()); + + verify(documentService).findIncompleteDocuments(5); + } + + @Test + @WithMockUser + void getIncomplete_usesDefaultSizeWhenNotSpecified() throws Exception { + when(documentService.findIncompleteDocuments(anyInt())).thenReturn(List.of()); + + mockMvc.perform(get("/api/documents/incomplete")) + .andExpect(status().isOk()); + + verify(documentService).findIncompleteDocuments(10); + } + // ─── GET /api/documents/incomplete/next ────────────────────────────────── @Test diff --git a/backend/src/test/java/org/raddatz/familienarchiv/repository/DocumentRepositoryTest.java b/backend/src/test/java/org/raddatz/familienarchiv/repository/DocumentRepositoryTest.java index 0a960117..04cab9d1 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/repository/DocumentRepositoryTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/repository/DocumentRepositoryTest.java @@ -11,6 +11,10 @@ import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabas import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest; import org.springframework.context.annotation.Import; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; + import java.util.List; import java.util.Optional; @@ -147,4 +151,25 @@ class DocumentRepositoryTest { assertThat(documentRepository.countByMetadataCompleteFalse()).isEqualTo(1); } + + // ─── findByMetadataCompleteFalse (Pageable) ─────────────────────────────── + + @Test + void findByMetadataCompleteFalse_withPageable_returnsOnlyIncompleteAndRespectsSizeCap() { + for (int i = 0; i < 5; i++) { + documentRepository.save(Document.builder() + .title("Incomplete " + i).originalFilename("inc" + i + ".pdf") + .status(DocumentStatus.UPLOADED).metadataComplete(false).build()); + } + documentRepository.save(Document.builder() + .title("Complete").originalFilename("complete.pdf") + .status(DocumentStatus.REVIEWED).metadataComplete(true).build()); + + Page result = documentRepository.findByMetadataCompleteFalse( + PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "createdAt"))); + + assertThat(result.getContent()).hasSize(3); + assertThat(result.getTotalElements()).isEqualTo(5); + assertThat(result.getContent()).allMatch(d -> !d.isMetadataComplete()); + } } diff --git a/backend/src/test/java/org/raddatz/familienarchiv/service/DocumentServiceTest.java b/backend/src/test/java/org/raddatz/familienarchiv/service/DocumentServiceTest.java index 80efd3e4..4d7c27ec 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/service/DocumentServiceTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/service/DocumentServiceTest.java @@ -7,12 +7,16 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.raddatz.familienarchiv.dto.DocumentUpdateDTO; +import org.raddatz.familienarchiv.dto.IncompleteDocumentDTO; import org.raddatz.familienarchiv.exception.DomainException; import org.raddatz.familienarchiv.model.Document; import org.raddatz.familienarchiv.model.DocumentStatus; import org.raddatz.familienarchiv.model.Person; import org.raddatz.familienarchiv.model.Tag; import org.raddatz.familienarchiv.repository.DocumentRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.mock.web.MockMultipartFile; @@ -360,12 +364,30 @@ class DocumentServiceTest { // ─── findIncompleteDocuments ────────────────────────────────────────────── @Test - void findIncompleteDocuments_returnsDocumentsOrderedByCreatedAtDesc() { - Document doc = Document.builder().id(UUID.randomUUID()).title("Test").build(); - when(documentRepository.findByMetadataCompleteFalse(any(Sort.class))).thenReturn(List.of(doc)); + void findIncompleteDocuments_returnsDTOsWithIdAndTitle() { + UUID id = UUID.randomUUID(); + Document doc = Document.builder().id(id).title("Unvollständig").build(); + when(documentRepository.findByMetadataCompleteFalse(any(Pageable.class))) + .thenReturn(new PageImpl<>(List.of(doc))); - assertThat(documentService.findIncompleteDocuments()).containsExactly(doc); - verify(documentRepository).findByMetadataCompleteFalse(Sort.by(Sort.Direction.DESC, "createdAt")); + List result = documentService.findIncompleteDocuments(3); + + assertThat(result).hasSize(1); + assertThat(result.get(0).id()).isEqualTo(id); + assertThat(result.get(0).title()).isEqualTo("Unvollständig"); + } + + @Test + void findIncompleteDocuments_passesSizeToPageable() { + when(documentRepository.findByMetadataCompleteFalse(any(Pageable.class))) + .thenReturn(Page.empty()); + + documentService.findIncompleteDocuments(3); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Pageable.class); + verify(documentRepository).findByMetadataCompleteFalse(captor.capture()); + assertThat(captor.getValue().getPageSize()).isEqualTo(3); + assertThat(captor.getValue().getSort()).isEqualTo(Sort.by(Sort.Direction.DESC, "createdAt")); } // ─── findNextIncompleteDocument ───────────────────────────────────────────