diff --git a/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentController.java b/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentController.java index b77b57b7..b2ee5f34 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentController.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentController.java @@ -316,7 +316,8 @@ public class DocumentController { @RequestParam(required = false) Boolean undated, Authentication authentication) { TagOperator operator = "OR".equalsIgnoreCase(tagOp) ? TagOperator.OR : TagOperator.AND; - List ids = documentService.findIdsForFilter(q, from, to, senderId, receiverId, tags, tagQ, status, operator, Boolean.TRUE.equals(undated)); + SearchFilters filters = new SearchFilters(q, from, to, senderId, receiverId, tags, tagQ, status, operator, Boolean.TRUE.equals(undated)); + List ids = documentService.findIdsForFilter(filters); if (ids.size() > BULK_EDIT_FILTER_MAX_IDS) { throw DomainException.badRequest(ErrorCode.BULK_EDIT_TOO_MANY_IDS, "Filter matches " + ids.size() + " documents — refine filter (max " + BULK_EDIT_FILTER_MAX_IDS + ")"); @@ -388,8 +389,9 @@ public class DocumentController { // tagOp is a raw String at the HTTP boundary; any value other than "OR" (case-insensitive) // defaults to AND, which matches the frontend default and keeps old clients working. TagOperator operator = "OR".equalsIgnoreCase(tagOp) ? TagOperator.OR : TagOperator.AND; + SearchFilters filters = new SearchFilters(q, from, to, senderId, receiverId, tags, tagQ, status, operator, Boolean.TRUE.equals(undated)); Pageable pageable = PageRequest.of(page, size); - return ResponseEntity.ok(documentService.searchDocuments(q, from, to, senderId, receiverId, tags, tagQ, status, sort, dir, operator, Boolean.TRUE.equals(undated), pageable)); + return ResponseEntity.ok(documentService.searchDocuments(filters, sort, dir, pageable)); } @GetMapping(value = "/density", produces = MediaType.APPLICATION_JSON_VALUE) diff --git a/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentService.java b/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentService.java index 93570e49..faa24de3 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentService.java @@ -167,11 +167,13 @@ public class DocumentService { /** Loads matching documents and projects to non-null {@link LocalDate}s. */ private List loadFilteredDates(DensityFilters filters, List ftsIds) { boolean hasFts = ftsIds != null; - Specification spec = buildSearchSpec( - hasFts, ftsIds, null, null, - filters.sender(), filters.receiver(), - filters.tags(), filters.tagQ(), - filters.status(), filters.tagOperator(), false); + // Density and search keep separate filter records (DensityFilters has no + // date/undated fields); adapt to SearchFilters here to reuse buildSearchSpec. + // Date bounds stay null and undated=false — the density path never filters by date. + SearchFilters searchFilters = new SearchFilters( + filters.text(), null, null, filters.sender(), filters.receiver(), + filters.tags(), filters.tagQ(), filters.status(), filters.tagOperator(), false); + Specification spec = buildSearchSpec(hasFts, ftsIds, searchFilters); return documentRepository.findAll(spec).stream() .map(Document::getDocumentDate) .filter(Objects::nonNull) @@ -500,18 +502,15 @@ public class DocumentService { * round-trip. */ @Transactional(readOnly = true) - public List findIdsForFilter(String text, LocalDate from, LocalDate to, UUID sender, UUID receiver, - List tags, String tagQ, DocumentStatus status, TagOperator tagOperator, - boolean undated) { - boolean hasText = StringUtils.hasText(text); + public List findIdsForFilter(SearchFilters filters) { + boolean hasText = StringUtils.hasText(filters.text()); List rankedIds = null; if (hasText) { - rankedIds = documentRepository.findAllMatchingIdsByFts(text); + rankedIds = documentRepository.findAllMatchingIdsByFts(filters.text()); if (rankedIds.isEmpty()) return List.of(); } - Specification spec = buildSearchSpec( - hasText, rankedIds, from, to, sender, receiver, tags, tagQ, status, tagOperator, undated); + Specification spec = buildSearchSpec(hasText, rankedIds, filters); return documentRepository.findAll(spec).stream().map(Document::getId).toList(); } @@ -521,23 +520,18 @@ public class DocumentService { * (uncapped, ID-only). Caller does its own FTS short-circuit when the * full-text query returned no rows. */ - private Specification buildSearchSpec(boolean hasText, List ftsIds, - LocalDate from, LocalDate to, - UUID sender, UUID receiver, - List tags, String tagQ, - DocumentStatus status, TagOperator tagOperator, - boolean undated) { - boolean useOrLogic = tagOperator == TagOperator.OR; - List> expandedTagSets = tagService.expandTagNamesToDescendantIdSets(tags); + private Specification buildSearchSpec(boolean hasText, List ftsIds, SearchFilters filters) { + boolean useOrLogic = filters.tagOperator() == TagOperator.OR; + List> expandedTagSets = tagService.expandTagNamesToDescendantIdSets(filters.tags()); Specification textSpec = hasText ? hasIds(ftsIds) : (root, query, cb) -> null; return Specification.where(textSpec) - .and(isBetween(from, to)) - .and(hasSender(sender)) - .and(hasReceiver(receiver)) + .and(isBetween(filters.from(), filters.to())) + .and(hasSender(filters.sender())) + .and(hasReceiver(filters.receiver())) .and(hasTags(expandedTagSets, useOrLogic)) - .and(hasTagPartial(tagQ)) - .and(hasStatus(status)) - .and(undatedOnly(undated)); + .and(hasTagPartial(filters.tagQ())) + .and(hasStatus(filters.status())) + .and(undatedOnly(filters.undated())); } /** @@ -666,8 +660,8 @@ public class DocumentService { } // 1. Allgemeine Suche (für das Suchfeld im Frontend) - public DocumentSearchResult searchDocuments(String text, LocalDate from, LocalDate to, UUID sender, UUID receiver, List tags, String tagQ, DocumentStatus status, DocumentSort sort, String dir, TagOperator tagOperator, boolean undated, Pageable pageable) { - boolean hasText = StringUtils.hasText(text); + public DocumentSearchResult searchDocuments(SearchFilters filters, DocumentSort sort, String dir, Pageable pageable) { + boolean hasText = StringUtils.hasText(filters.text()); // Pure-text RELEVANCE: push pagination + ts_rank ordering into SQL — skip // findAllMatchingIdsByFts entirely (ADR-008). This must run BEFORE any @@ -677,13 +671,13 @@ public class DocumentService { // no date/sender/receiver/tag/status filters, and undated documents are valid // FTS hits already folded into the ranked page, so there is no separate undated // count to report here. - if (!undated && isPureTextRelevance(hasText, sort, from, to, sender, receiver, tags, tagQ, status)) { - return relevanceSortedPageFromSql(text, pageable); + if (!filters.undated() && isPureTextRelevance(hasText, sort, filters)) { + return relevanceSortedPageFromSql(filters.text(), pageable); } List rankedIds = null; if (hasText) { - rankedIds = documentRepository.findAllMatchingIdsByFts(text); + rankedIds = documentRepository.findAllMatchingIdsByFts(filters.text()); // FTS matched nothing → no results and, by definition, no undated matches either. if (rankedIds.isEmpty()) return DocumentSearchResult.of(List.of()); } @@ -691,37 +685,32 @@ public class DocumentService { // Global undated count for the current filter (q/tags/sender/receiver/status), // forcing undatedOnly(true) and IGNORING the user's "Nur undatierte" toggle so // it never collapses to the page slice and never double-counts (issue #668). - long undatedCount = countUndatedForFilter(hasText, rankedIds, from, to, sender, receiver, tags, tagQ, status, tagOperator); + long undatedCount = countUndatedForFilter(hasText, rankedIds, filters.withUndated(true)); - return runSearch(text, hasText, rankedIds, from, to, sender, receiver, tags, tagQ, status, sort, dir, tagOperator, undated, pageable) + return runSearch(hasText, rankedIds, filters, sort, dir, pageable) .withUndatedCount(undatedCount); } /** * Counts every undated document (meta_date IS NULL) matching the active filter, - * across all pages, independent of the undated toggle. Reuses {@link #buildSearchSpec} - * with {@code undated=true} forced so the count tracks q/tags/sender/receiver/status. - * A {@code from}/{@code to} range excludes undated rows by the collision rule (#668), - * so the count is legitimately 0 inside a date range. + * across all pages, independent of the undated toggle. The caller passes + * {@code filters.withUndated(true)} so the count tracks q/tags/sender/receiver/status + * regardless of the user's "Nur undatierte" toggle. A {@code from}/{@code to} range + * excludes undated rows by the collision rule (#668), so the count is legitimately 0 + * inside a date range. */ - private long countUndatedForFilter(boolean hasText, List ftsIds, - LocalDate from, LocalDate to, UUID sender, UUID receiver, - List tags, String tagQ, DocumentStatus status, TagOperator tagOperator) { - Specification undatedSpec = buildSearchSpec( - hasText, ftsIds, from, to, sender, receiver, tags, tagQ, status, tagOperator, true); + private long countUndatedForFilter(boolean hasText, List ftsIds, SearchFilters filters) { + Specification undatedSpec = buildSearchSpec(hasText, ftsIds, filters); return documentRepository.count(undatedSpec); } /** The original search dispatch — produces the page slice + totals, sans undated count. */ - private DocumentSearchResult runSearch(String text, boolean hasText, List rankedIds, - LocalDate from, LocalDate to, UUID sender, UUID receiver, - List tags, String tagQ, DocumentStatus status, - DocumentSort sort, String dir, TagOperator tagOperator, - boolean undated, Pageable pageable) { + private DocumentSearchResult runSearch(boolean hasText, List rankedIds, SearchFilters filters, + DocumentSort sort, String dir, Pageable pageable) { // The pure-text RELEVANCE fast path is handled by the caller (searchDocuments) // before findAllMatchingIdsByFts runs, so it never reaches here (ADR-008). - Specification spec = buildSearchSpec( - hasText, rankedIds, from, to, sender, receiver, tags, tagQ, status, tagOperator, undated); + Specification spec = buildSearchSpec(hasText, rankedIds, filters); + String text = filters.text(); // SENDER and RECEIVER sorts load the full match set and slice in-memory. // JPA's Sort.by("sender.lastName") generates an INNER JOIN that silently drops @@ -755,12 +744,12 @@ public class DocumentService { return buildResultPaged(page.getContent(), text, pageable, page.getTotalElements()); } - private static boolean isPureTextRelevance(boolean hasText, DocumentSort sort, - LocalDate from, LocalDate to, UUID sender, UUID receiver, - List tags, String tagQ, DocumentStatus status) { + private static boolean isPureTextRelevance(boolean hasText, DocumentSort sort, SearchFilters filters) { return hasText && (sort == null || sort == DocumentSort.RELEVANCE) - && from == null && to == null && sender == null && receiver == null - && (tags == null || tags.isEmpty()) && (tagQ == null || tagQ.isBlank()) && status == null; + && filters.from() == null && filters.to() == null + && filters.sender() == null && filters.receiver() == null + && (filters.tags() == null || filters.tags().isEmpty()) + && (filters.tagQ() == null || filters.tagQ().isBlank()) && filters.status() == null; } /** diff --git a/backend/src/main/java/org/raddatz/familienarchiv/document/SearchFilters.java b/backend/src/main/java/org/raddatz/familienarchiv/document/SearchFilters.java new file mode 100644 index 00000000..07ce4ab8 --- /dev/null +++ b/backend/src/main/java/org/raddatz/familienarchiv/document/SearchFilters.java @@ -0,0 +1,40 @@ +package org.raddatz.familienarchiv.document; + +import org.raddatz.familienarchiv.tag.TagOperator; + +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; + +/** + * The filter predicates honoured by {@link DocumentService#searchDocuments} and + * {@link DocumentService#findIdsForFilter}. Sort, direction, and pagination are + * deliberately excluded — they are not filter predicates, and {@code findIdsForFilter} + * needs none of them; they are passed as separate arguments instead. + * + * Kept as a record so the ten values are passed as one named bundle instead of a + * positional argument list where two UUIDs (sender vs. receiver) or two dates + * (from vs. to) can be swapped by accident at the call site — a transposition that + * compiles cleanly and silently returns the wrong rows. + * + * Sibling of {@link DensityFilters} (= these fields minus from/to/undated); kept + * separate on purpose, so the density call path never reasons about date/undated + * fields it deliberately excludes. + */ +public record SearchFilters( + String text, + LocalDate from, + LocalDate to, + UUID sender, + UUID receiver, + List tags, + String tagQ, + DocumentStatus status, + TagOperator tagOperator, + boolean undated) { + + /** Returns a copy with {@code undated} overridden — used by the undated-count path. */ + public SearchFilters withUndated(boolean undated) { + return new SearchFilters(text, from, to, sender, receiver, tags, tagQ, status, tagOperator, undated); + } +} diff --git a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentControllerTest.java index 8a09ed73..f1e34554 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentControllerTest.java @@ -38,7 +38,6 @@ import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; @@ -76,7 +75,7 @@ class DocumentControllerTest { @Test @WithMockUser void search_returns200_whenAuthenticated() throws Exception { - when(documentService.searchDocuments(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), anyBoolean(), any())) + when(documentService.searchDocuments(any(), any(), any(), any())) .thenReturn(DocumentSearchResult.of(List.of())); mockMvc.perform(get("/api/documents/search")) @@ -88,7 +87,7 @@ class DocumentControllerTest { void search_undatedTrue_isReachableByAuthenticatedUser() throws Exception { // The read GET must stay reachable for READ_ALL users — guards against a // future refactor accidentally write-guarding the undated triage path (#668). - when(documentService.searchDocuments(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), anyBoolean(), any())) + when(documentService.searchDocuments(any(), any(), any(), any())) .thenReturn(DocumentSearchResult.of(List.of())); mockMvc.perform(get("/api/documents/search").param("undated", "true")) @@ -104,41 +103,43 @@ class DocumentControllerTest { @Test @WithMockUser void search_undatedTrue_isForwardedToServiceAsTrue() throws Exception { - ArgumentCaptor undatedCaptor = ArgumentCaptor.forClass(Boolean.class); - when(documentService.searchDocuments(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), anyBoolean(), any())) + ArgumentCaptor filtersCaptor = ArgumentCaptor.forClass(SearchFilters.class); + when(documentService.searchDocuments(any(), any(), any(), any())) .thenReturn(DocumentSearchResult.of(List.of())); mockMvc.perform(get("/api/documents/search").param("undated", "true")) .andExpect(status().isOk()); - verify(documentService).searchDocuments(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), undatedCaptor.capture(), any()); - assertThat(undatedCaptor.getValue()).isTrue(); + verify(documentService).searchDocuments(filtersCaptor.capture(), any(), any(), any()); + assertThat(filtersCaptor.getValue().undated()).isTrue(); } @Test @WithMockUser void search_withoutUndatedParam_forwardsFalseToService() throws Exception { - ArgumentCaptor undatedCaptor = ArgumentCaptor.forClass(Boolean.class); - when(documentService.searchDocuments(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), anyBoolean(), any())) + ArgumentCaptor filtersCaptor = ArgumentCaptor.forClass(SearchFilters.class); + when(documentService.searchDocuments(any(), any(), any(), any())) .thenReturn(DocumentSearchResult.of(List.of())); mockMvc.perform(get("/api/documents/search")) .andExpect(status().isOk()); - verify(documentService).searchDocuments(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), undatedCaptor.capture(), any()); - assertThat(undatedCaptor.getValue()).isFalse(); + verify(documentService).searchDocuments(filtersCaptor.capture(), any(), any(), any()); + assertThat(filtersCaptor.getValue().undated()).isFalse(); } @Test @WithMockUser void search_withStatusParam_passesItToService() throws Exception { - when(documentService.searchDocuments(any(), any(), any(), any(), any(), any(), any(), eq(DocumentStatus.REVIEWED), any(), any(), any(), anyBoolean(), any())) + ArgumentCaptor filtersCaptor = ArgumentCaptor.forClass(SearchFilters.class); + when(documentService.searchDocuments(any(), any(), any(), any())) .thenReturn(DocumentSearchResult.of(List.of())); mockMvc.perform(get("/api/documents/search").param("status", "REVIEWED")) .andExpect(status().isOk()); - verify(documentService).searchDocuments(any(), any(), any(), any(), any(), any(), any(), eq(DocumentStatus.REVIEWED), any(), any(), any(), anyBoolean(), any()); + verify(documentService).searchDocuments(filtersCaptor.capture(), any(), any(), any()); + assertThat(filtersCaptor.getValue().status()).isEqualTo(DocumentStatus.REVIEWED); } @Test @@ -165,7 +166,7 @@ class DocumentControllerTest { @Test @WithMockUser void search_responseContainsTotalCount() throws Exception { - when(documentService.searchDocuments(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), anyBoolean(), any())) + when(documentService.searchDocuments(any(), any(), any(), any())) .thenReturn(DocumentSearchResult.of(List.of())); mockMvc.perform(get("/api/documents/search")) @@ -180,7 +181,7 @@ class DocumentControllerTest { UUID docId = UUID.randomUUID(); var matchData = new SearchMatchData( "Er schrieb einen langen Brief", List.of(), false, List.of(), List.of(), List.of(), null, List.of()); - when(documentService.searchDocuments(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), anyBoolean(), any())) + when(documentService.searchDocuments(any(), any(), any(), any())) .thenReturn(DocumentSearchResult.of(List.of(new DocumentListItem( docId, "Brief an Anna", "brief.pdf", null, null, DatePrecision.UNKNOWN, null, null, @@ -200,7 +201,7 @@ class DocumentControllerTest { void search_returns_flat_item_with_id_and_without_sensitive_fields() throws Exception { UUID docId = UUID.randomUUID(); var matchData = new SearchMatchData(null, List.of(), false, List.of(), List.of(), List.of(), null, List.of()); - when(documentService.searchDocuments(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), anyBoolean(), any())) + when(documentService.searchDocuments(any(), any(), any(), any())) .thenReturn(DocumentSearchResult.of(List.of(new DocumentListItem( docId, "Brief an Anna", "brief.pdf", null, null, DatePrecision.UNKNOWN, null, null, @@ -223,7 +224,7 @@ class DocumentControllerTest { @Test @WithMockUser void search_responseExposesPagingFields() throws Exception { - when(documentService.searchDocuments(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), anyBoolean(), any())) + when(documentService.searchDocuments(any(), any(), any(), any())) .thenReturn(DocumentSearchResult.of(List.of())); mockMvc.perform(get("/api/documents/search")) @@ -268,7 +269,7 @@ class DocumentControllerTest { @Test @WithMockUser void search_passesPageRequestToService() throws Exception { - when(documentService.searchDocuments(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), anyBoolean(), any())) + when(documentService.searchDocuments(any(), any(), any(), any())) .thenReturn(DocumentSearchResult.of(List.of())); mockMvc.perform(get("/api/documents/search").param("page", "2").param("size", "25")) @@ -276,7 +277,7 @@ class DocumentControllerTest { org.mockito.ArgumentCaptor captor = org.mockito.ArgumentCaptor.forClass(org.springframework.data.domain.Pageable.class); - verify(documentService).searchDocuments(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), anyBoolean(), captor.capture()); + verify(documentService).searchDocuments(any(), any(), any(), captor.capture()); org.springframework.data.domain.Pageable pageable = captor.getValue(); org.assertj.core.api.Assertions.assertThat(pageable.getPageNumber()).isEqualTo(2); org.assertj.core.api.Assertions.assertThat(pageable.getPageSize()).isEqualTo(25); @@ -1208,7 +1209,7 @@ class DocumentControllerTest { void getDocumentIds_returns200_andDelegatesToService() throws Exception { when(userService.findByEmail(any())).thenReturn(AppUser.builder().id(UUID.randomUUID()).build()); UUID id = UUID.randomUUID(); - when(documentService.findIdsForFilter(any(), any(), any(), any(), any(), any(), any(), any(), any(), anyBoolean())) + when(documentService.findIdsForFilter(any())) .thenReturn(List.of(id)); mockMvc.perform(get("/api/documents/ids")) @@ -1221,13 +1222,33 @@ class DocumentControllerTest { void getDocumentIds_passesSenderIdParamToService() throws Exception { when(userService.findByEmail(any())).thenReturn(AppUser.builder().id(UUID.randomUUID()).build()); UUID senderId = UUID.randomUUID(); - when(documentService.findIdsForFilter(any(), any(), any(), eq(senderId), any(), any(), any(), any(), any(), anyBoolean())) + ArgumentCaptor filtersCaptor = ArgumentCaptor.forClass(SearchFilters.class); + when(documentService.findIdsForFilter(any())) .thenReturn(List.of()); mockMvc.perform(get("/api/documents/ids").param("senderId", senderId.toString())) .andExpect(status().isOk()); - verify(documentService).findIdsForFilter(any(), any(), any(), eq(senderId), any(), any(), any(), any(), any(), anyBoolean()); + verify(documentService).findIdsForFilter(filtersCaptor.capture()); + assertThat(filtersCaptor.getValue().sender()).isEqualTo(senderId); + } + + @Test + @WithMockUser(authorities = "WRITE_ALL") + void getDocumentIds_withoutUndatedParam_coercesNullToFalse() throws Exception { + // The controller coerces a null boxed Boolean to primitive false + // (Boolean.TRUE.equals(undated)) so the absent param never NPEs and the + // record always holds a concrete boolean. + when(userService.findByEmail(any())).thenReturn(AppUser.builder().id(UUID.randomUUID()).build()); + ArgumentCaptor filtersCaptor = ArgumentCaptor.forClass(SearchFilters.class); + when(documentService.findIdsForFilter(any())) + .thenReturn(List.of()); + + mockMvc.perform(get("/api/documents/ids")) + .andExpect(status().isOk()); + + verify(documentService).findIdsForFilter(filtersCaptor.capture()); + assertThat(filtersCaptor.getValue().undated()).isFalse(); } @Test @@ -1237,7 +1258,7 @@ class DocumentControllerTest { // Service returns 5001 IDs — one over BULK_EDIT_FILTER_MAX_IDS (5000). java.util.List tooMany = new java.util.ArrayList<>(5001); for (int i = 0; i < 5001; i++) tooMany.add(UUID.randomUUID()); - when(documentService.findIdsForFilter(any(), any(), any(), any(), any(), any(), any(), any(), any(), anyBoolean())) + when(documentService.findIdsForFilter(any())) .thenReturn(tooMany); mockMvc.perform(get("/api/documents/ids")) diff --git a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentLazyLoadingTest.java b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentLazyLoadingTest.java index 1b5a4b1e..6768991f 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentLazyLoadingTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentLazyLoadingTest.java @@ -24,6 +24,7 @@ import java.util.Set; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; +import static org.raddatz.familienarchiv.document.SearchFiltersFixtures.noFilters; import static org.assertj.core.api.Assertions.assertThatCode; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; @@ -122,8 +123,8 @@ class DocumentLazyLoadingTest { savedDocument("SrDoc", "sr_doc.pdf", sender, Set.of(receiver), Set.of(tag)); DocumentSearchResult result = documentService.searchDocuments( - null, null, null, null, null, null, null, null, - DocumentSort.RECEIVER, "asc", null, false, PageRequest.of(0, 20)); + noFilters(), + DocumentSort.RECEIVER, "asc", PageRequest.of(0, 20)); assertThat(result.totalElements()).isGreaterThan(0); assertThatCode(() -> result.items().forEach(i -> { if (i.sender() != null) i.sender().getLastName(); })) @@ -137,8 +138,8 @@ class DocumentLazyLoadingTest { savedDocument("SsDoc", "ss_doc.pdf", sender, Set.of(), Set.of(tag)); assertThatCode(() -> documentService.searchDocuments( - null, null, null, null, null, null, null, null, - DocumentSort.SENDER, "asc", null, false, PageRequest.of(0, 20))) + noFilters(), + DocumentSort.SENDER, "asc", PageRequest.of(0, 20))) .doesNotThrowAnyException(); } diff --git a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentListItemIntegrationTest.java b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentListItemIntegrationTest.java index 3d0e4b90..09144d9e 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentListItemIntegrationTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentListItemIntegrationTest.java @@ -17,6 +17,7 @@ import java.util.HashSet; import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; +import static org.raddatz.familienarchiv.document.SearchFiltersFixtures.noFilters; import static org.assertj.core.api.Assertions.assertThatCode; /** @@ -55,8 +56,8 @@ class DocumentListItemIntegrationTest { .build()); assertThatCode(() -> documentService.searchDocuments( - null, null, null, null, null, null, null, null, - DocumentSort.DATE, "DESC", null, false, PageRequest.of(0, 50))) + noFilters(), + DocumentSort.DATE, "DESC", PageRequest.of(0, 50))) .doesNotThrowAnyException(); } @@ -70,8 +71,8 @@ class DocumentListItemIntegrationTest { .build()); DocumentSearchResult result = documentService.searchDocuments( - null, null, null, null, null, null, null, null, - DocumentSort.DATE, "DESC", null, false, PageRequest.of(0, 50)); + noFilters(), + DocumentSort.DATE, "DESC", PageRequest.of(0, 50)); assertThat(result.totalElements()).isGreaterThan(0); DocumentListItem item = result.items().get(0); @@ -91,8 +92,8 @@ class DocumentListItemIntegrationTest { .build()); DocumentSearchResult result = documentService.searchDocuments( - null, null, null, null, null, null, null, null, - DocumentSort.DATE, "DESC", null, false, PageRequest.of(0, 50)); + noFilters(), + DocumentSort.DATE, "DESC", PageRequest.of(0, 50)); DocumentListItem item = result.items().stream() .filter(i -> i.title().equals("Range Brief")).findFirst().orElseThrow(); diff --git a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentSearchPagedIntegrationTest.java b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentSearchPagedIntegrationTest.java index 3d65cbac..0e067fcd 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentSearchPagedIntegrationTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentSearchPagedIntegrationTest.java @@ -21,6 +21,7 @@ import java.time.LocalDate; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; +import static org.raddatz.familienarchiv.document.SearchFiltersFixtures.noFilters; /** * End-to-end paged search test with real PostgreSQL (Testcontainers). Covers the @@ -61,8 +62,8 @@ class DocumentSearchPagedIntegrationTest { @Test void search_firstPage_returnsExactlyPageSizeItems_andCorrectTotalElements() { DocumentSearchResult result = documentService.searchDocuments( - null, null, null, null, null, null, null, null, - DocumentSort.DATE, "DESC", null, false, PageRequest.of(0, 50)); + noFilters(), + DocumentSort.DATE, "DESC", PageRequest.of(0, 50)); assertThat(result.items()).hasSize(50); assertThat(result.totalElements()).isEqualTo(FIXTURE_SIZE); @@ -74,8 +75,8 @@ class DocumentSearchPagedIntegrationTest { @Test void search_lastPartialPage_returnsRemainingItems() { DocumentSearchResult result = documentService.searchDocuments( - null, null, null, null, null, null, null, null, - DocumentSort.DATE, "DESC", null, false, PageRequest.of(2, 50)); + noFilters(), + DocumentSort.DATE, "DESC", PageRequest.of(2, 50)); // Page 2 (offset 100) of 120 docs → exactly 20 items on the tail. assertThat(result.items()).hasSize(20); @@ -86,8 +87,8 @@ class DocumentSearchPagedIntegrationTest { @Test void search_pageBeyondLast_returnsEmptyContent_totalElementsStillCorrect() { DocumentSearchResult result = documentService.searchDocuments( - null, null, null, null, null, null, null, null, - DocumentSort.DATE, "DESC", null, false, PageRequest.of(99, 50)); + noFilters(), + DocumentSort.DATE, "DESC", PageRequest.of(99, 50)); assertThat(result.items()).isEmpty(); assertThat(result.totalElements()).isEqualTo(FIXTURE_SIZE); @@ -99,8 +100,8 @@ class DocumentSearchPagedIntegrationTest { // comment in DocumentService). Proves that the in-memory slice path // returns the correct total from a real repository fetch. DocumentSearchResult result = documentService.searchDocuments( - null, null, null, null, null, null, null, null, - DocumentSort.SENDER, "asc", null, false, PageRequest.of(1, 50)); + noFilters(), + DocumentSort.SENDER, "asc", PageRequest.of(1, 50)); assertThat(result.items()).hasSize(50); assertThat(result.totalElements()).isEqualTo(FIXTURE_SIZE); @@ -125,8 +126,8 @@ class DocumentSearchPagedIntegrationTest { } DocumentSearchResult result = documentService.searchDocuments( - null, null, null, null, null, null, null, null, - DocumentSort.DATE, "DESC", null, false, PageRequest.of(0, 50)); + noFilters(), + DocumentSort.DATE, "DESC", PageRequest.of(0, 50)); // Global undated count is the full undated total, independent of page size. assertThat(result.undatedCount()).isEqualTo(undatedTotal); @@ -153,11 +154,11 @@ class DocumentSearchPagedIntegrationTest { } DocumentSearchResult unfiltered = documentService.searchDocuments( - null, null, null, null, null, null, null, null, - DocumentSort.DATE, "DESC", null, false, PageRequest.of(0, 50)); + noFilters(), + DocumentSort.DATE, "DESC", PageRequest.of(0, 50)); DocumentSearchResult undatedOnly = documentService.searchDocuments( - null, null, null, null, null, null, null, null, - DocumentSort.DATE, "DESC", null, true, PageRequest.of(0, 50)); + noFilters().withUndated(true), + DocumentSort.DATE, "DESC", PageRequest.of(0, 50)); assertThat(unfiltered.undatedCount()).isEqualTo(undatedTotal); assertThat(undatedOnly.undatedCount()).isEqualTo(undatedTotal); @@ -178,9 +179,9 @@ class DocumentSearchPagedIntegrationTest { } DocumentSearchResult result = documentService.searchDocuments( - null, LocalDate.of(1900, 1, 1), LocalDate.of(2000, 12, 31), - null, null, null, null, null, - DocumentSort.DATE, "DESC", null, false, PageRequest.of(0, 50)); + new SearchFilters(null, LocalDate.of(1900, 1, 1), LocalDate.of(2000, 12, 31), + null, null, null, null, null, null, false), + DocumentSort.DATE, "DESC", PageRequest.of(0, 50)); assertThat(result.undatedCount()).isZero(); } @@ -188,11 +189,11 @@ class DocumentSearchPagedIntegrationTest { @Test void search_differentPagesReturnDisjointSlices() { DocumentSearchResult page0 = documentService.searchDocuments( - null, null, null, null, null, null, null, null, - DocumentSort.DATE, "DESC", null, false, PageRequest.of(0, 50)); + noFilters(), + DocumentSort.DATE, "DESC", PageRequest.of(0, 50)); DocumentSearchResult page1 = documentService.searchDocuments( - null, null, null, null, null, null, null, null, - DocumentSort.DATE, "DESC", null, false, PageRequest.of(1, 50)); + noFilters(), + DocumentSort.DATE, "DESC", PageRequest.of(1, 50)); // No document id should appear on both pages — slicing must be exclusive. var idsOnPage0 = page0.items().stream() diff --git a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentServiceSortTest.java b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentServiceSortTest.java index c3d00619..bbc058ac 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentServiceSortTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentServiceSortTest.java @@ -67,7 +67,8 @@ class DocumentServiceSortTest { .thenReturn(new PageImpl<>(List.of(newer, older))); DocumentSearchResult result = documentService.searchDocuments( - "Brief", null, null, null, null, null, null, null, DocumentSort.DATE, "DESC", null, false, PAGE); + new SearchFilters("Brief", null, null, null, null, null, null, null, null, false), + DocumentSort.DATE, "DESC", PAGE); assertThat(result.items()).hasSize(2); assertThat(result.items().get(0).id()).isEqualTo(id2); // newer first @@ -84,7 +85,8 @@ class DocumentServiceSortTest { .thenReturn(List.of(doc(id1))); documentService.searchDocuments( - "Brief", null, null, null, null, null, null, null, DocumentSort.RELEVANCE, null, null, false, PAGE); + new SearchFilters("Brief", null, null, null, null, null, null, null, null, false), + DocumentSort.RELEVANCE, null, PAGE); verify(documentRepository).findFtsPageRaw(anyString(), anyInt(), anyInt()); verify(documentRepository, never()).findAllMatchingIdsByFts(anyString()); @@ -102,7 +104,8 @@ class DocumentServiceSortTest { when(documentRepository.findAllById(any())).thenReturn(List.of(doc(id2), doc(id1))); // unordered from JPA DocumentSearchResult result = documentService.searchDocuments( - "Brief", null, null, null, null, null, null, null, DocumentSort.RELEVANCE, null, null, false, PAGE); + new SearchFilters("Brief", null, null, null, null, null, null, null, null, false), + DocumentSort.RELEVANCE, null, PAGE); assertThat(result.items().get(0).id()).isEqualTo(id1); } @@ -119,7 +122,8 @@ class DocumentServiceSortTest { when(documentRepository.findAllById(any())).thenReturn(List.of(doc(id2), doc(id1))); DocumentSearchResult result = documentService.searchDocuments( - "Brief", null, null, null, null, null, null, null, null, null, null, false, PAGE); + new SearchFilters("Brief", null, null, null, null, null, null, null, null, false), + null, null, PAGE); assertThat(result.items().get(0).id()).isEqualTo(id1); } @@ -132,8 +136,8 @@ class DocumentServiceSortTest { Pageable hugePage = org.springframework.data.domain.PageRequest.of(Integer.MAX_VALUE / 10 + 1, 10); DocumentSearchResult result = documentService.searchDocuments( - "Brief", null, null, null, null, null, null, null, - DocumentSort.RELEVANCE, null, null, false, hugePage); + new SearchFilters("Brief", null, null, null, null, null, null, null, null, false), + DocumentSort.RELEVANCE, null, hugePage); assertThat(result.items()).isEmpty(); verify(documentRepository, never()).findFtsPageRaw(anyString(), anyInt(), anyInt()); @@ -152,8 +156,8 @@ class DocumentServiceSortTest { when(documentRepository.findAllById(any())).thenReturn(List.of(doc(uuidId))); DocumentSearchResult result = documentService.searchDocuments( - "Brief", null, null, null, null, null, null, null, - DocumentSort.RELEVANCE, null, null, false, PAGE); + new SearchFilters("Brief", null, null, null, null, null, null, null, null, false), + DocumentSort.RELEVANCE, null, PAGE); assertThat(result.items()).hasSize(1); assertThat(result.items().get(0).id()).isEqualTo(uuidId); @@ -173,7 +177,8 @@ class DocumentServiceSortTest { // sender filter is active → triggers in-memory path, not findFtsPageRaw LocalDate from = LocalDate.of(1900, 1, 1); documentService.searchDocuments( - "Brief", from, null, null, null, null, null, null, DocumentSort.RELEVANCE, null, null, false, PAGE); + new SearchFilters("Brief", from, null, null, null, null, null, null, null, false), + DocumentSort.RELEVANCE, null, PAGE); verify(documentRepository, never()).findFtsPageRaw(anyString(), anyInt(), anyInt()); verify(documentRepository).findAllMatchingIdsByFts("Brief"); diff --git a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentServiceTest.java b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentServiceTest.java index ccf162f9..9257aafe 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentServiceTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentServiceTest.java @@ -45,6 +45,7 @@ import java.util.Set; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; +import static org.raddatz.familienarchiv.document.SearchFiltersFixtures.noFilters; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; @@ -1441,8 +1442,9 @@ class DocumentServiceTest { when(documentRepository.findAll(any(org.springframework.data.jpa.domain.Specification.class), any(Pageable.class))) .thenReturn(new PageImpl<>(List.of())); - documentService.searchDocuments(null, null, null, null, null, null, null, null, - org.raddatz.familienarchiv.document.DocumentSort.DATE, "DESC", null, false, org.springframework.data.domain.PageRequest.of(1, 50)); + documentService.searchDocuments( + noFilters(), + org.raddatz.familienarchiv.document.DocumentSort.DATE, "DESC", org.springframework.data.domain.PageRequest.of(1, 50)); verify(documentRepository).findAll(any(org.springframework.data.jpa.domain.Specification.class), any(Pageable.class)); verify(documentRepository, never()).findAll(any(org.springframework.data.jpa.domain.Specification.class), any(Sort.class)); @@ -1454,8 +1456,9 @@ class DocumentServiceTest { when(documentRepository.findAll(any(org.springframework.data.jpa.domain.Specification.class), any(Pageable.class))) .thenReturn(new PageImpl<>(List.of())); - documentService.searchDocuments(null, null, null, null, null, null, null, null, - org.raddatz.familienarchiv.document.DocumentSort.DATE, "DESC", null, false, org.springframework.data.domain.PageRequest.of(3, 25)); + documentService.searchDocuments( + noFilters(), + org.raddatz.familienarchiv.document.DocumentSort.DATE, "DESC", org.springframework.data.domain.PageRequest.of(3, 25)); verify(documentRepository).findAll(any(org.springframework.data.jpa.domain.Specification.class), captor.capture()); assertThat(captor.getValue().getPageNumber()).isEqualTo(3); @@ -1470,8 +1473,9 @@ class DocumentServiceTest { when(documentRepository.findAll(any(org.springframework.data.jpa.domain.Specification.class), any(Pageable.class))) .thenReturn(new PageImpl<>(List.of(d), org.springframework.data.domain.PageRequest.of(0, 50), 120L)); - DocumentSearchResult result = documentService.searchDocuments(null, null, null, null, null, null, null, null, - org.raddatz.familienarchiv.document.DocumentSort.DATE, "DESC", null, false, org.springframework.data.domain.PageRequest.of(0, 50)); + DocumentSearchResult result = documentService.searchDocuments( + noFilters(), + org.raddatz.familienarchiv.document.DocumentSort.DATE, "DESC", org.springframework.data.domain.PageRequest.of(0, 50)); assertThat(result.totalElements()).isEqualTo(120L); assertThat(result.pageNumber()).isZero(); @@ -1486,8 +1490,9 @@ class DocumentServiceTest { when(documentRepository.findAll(any(org.springframework.data.jpa.domain.Specification.class), any(Pageable.class))) .thenReturn(new PageImpl<>(List.of())); - documentService.searchDocuments(null, null, null, null, null, null, null, null, - DocumentSort.DATE, "DESC", null, false, org.springframework.data.domain.PageRequest.of(0, 5)); + documentService.searchDocuments( + noFilters(), + DocumentSort.DATE, "DESC", org.springframework.data.domain.PageRequest.of(0, 5)); verify(documentRepository).findAll(any(org.springframework.data.jpa.domain.Specification.class), captor.capture()); Sort.Order dateOrder = captor.getValue().getSort().getOrderFor("documentDate"); @@ -1509,8 +1514,9 @@ class DocumentServiceTest { when(documentRepository.findAll(any(org.springframework.data.jpa.domain.Specification.class), any(Pageable.class))) .thenReturn(new PageImpl<>(List.of())); - documentService.searchDocuments(null, null, null, null, null, null, null, null, - DocumentSort.DATE, "ASC", null, false, org.springframework.data.domain.PageRequest.of(0, 5)); + documentService.searchDocuments( + noFilters(), + DocumentSort.DATE, "ASC", org.springframework.data.domain.PageRequest.of(0, 5)); verify(documentRepository).findAll(any(org.springframework.data.jpa.domain.Specification.class), captor.capture()); Sort.Order dateOrder = captor.getValue().getSort().getOrderFor("documentDate"); @@ -1530,8 +1536,9 @@ class DocumentServiceTest { when(documentRepository.findAll(any(org.springframework.data.jpa.domain.Specification.class), any(Pageable.class))) .thenReturn(new PageImpl<>(List.of())); - documentService.searchDocuments(null, null, null, null, null, null, null, null, - DocumentSort.UPDATED_AT, "DESC", null, false, org.springframework.data.domain.PageRequest.of(0, 5)); + documentService.searchDocuments( + noFilters(), + DocumentSort.UPDATED_AT, "DESC", org.springframework.data.domain.PageRequest.of(0, 5)); verify(documentRepository).findAll(any(org.springframework.data.jpa.domain.Specification.class), captor.capture()); assertThat(captor.getValue().getSort()) @@ -1554,8 +1561,9 @@ class DocumentServiceTest { when(documentRepository.findAll(any(org.springframework.data.jpa.domain.Specification.class))) .thenReturn(all); - DocumentSearchResult result = documentService.searchDocuments(null, null, null, null, null, null, null, null, - org.raddatz.familienarchiv.document.DocumentSort.SENDER, "asc", null, false, org.springframework.data.domain.PageRequest.of(1, 50)); + DocumentSearchResult result = documentService.searchDocuments( + noFilters(), + org.raddatz.familienarchiv.document.DocumentSort.SENDER, "asc", org.springframework.data.domain.PageRequest.of(1, 50)); assertThat(result.totalElements()).isEqualTo(120L); assertThat(result.pageNumber()).isEqualTo(1); @@ -1578,8 +1586,9 @@ class DocumentServiceTest { when(documentRepository.findAll(any(org.springframework.data.jpa.domain.Specification.class))) .thenReturn(all); - DocumentSearchResult result = documentService.searchDocuments(null, null, null, null, null, null, null, null, - org.raddatz.familienarchiv.document.DocumentSort.SENDER, "asc", null, false, org.springframework.data.domain.PageRequest.of(10, 50)); + DocumentSearchResult result = documentService.searchDocuments( + noFilters(), + org.raddatz.familienarchiv.document.DocumentSort.SENDER, "asc", org.springframework.data.domain.PageRequest.of(10, 50)); assertThat(result.items()).isEmpty(); assertThat(result.totalElements()).isEqualTo(30L); @@ -1592,7 +1601,8 @@ class DocumentServiceTest { when(documentRepository.findAll(any(org.springframework.data.jpa.domain.Specification.class), any(Pageable.class))) .thenReturn(new PageImpl<>(List.of())); - documentService.searchDocuments(null, null, null, null, null, null, null, DocumentStatus.REVIEWED, null, null, null, false, UNPAGED); + documentService.searchDocuments( + new SearchFilters(null, null, null, null, null, null, null, DocumentStatus.REVIEWED, null, false), null, null, UNPAGED); verify(documentRepository).findAll(any(org.springframework.data.jpa.domain.Specification.class), any(Pageable.class)); } @@ -1602,7 +1612,8 @@ class DocumentServiceTest { when(documentRepository.findAll(any(org.springframework.data.jpa.domain.Specification.class), any(Pageable.class))) .thenReturn(new PageImpl<>(List.of())); - documentService.searchDocuments(null, null, null, null, null, null, null, null, null, null, null, false, UNPAGED); + documentService.searchDocuments( + noFilters(), null, null, UNPAGED); verify(documentRepository).findAll(any(org.springframework.data.jpa.domain.Specification.class), any(Pageable.class)); } @@ -1680,7 +1691,8 @@ class DocumentServiceTest { .thenReturn(List.of(withSender, noSender)); DocumentSearchResult result = documentService.searchDocuments( - null, null, null, null, null, null, null, null, DocumentSort.SENDER, "asc", null, false, UNPAGED); + noFilters(), + DocumentSort.SENDER, "asc", UNPAGED); assertThat(result.items()).hasSize(2); assertThat(result.items()).extracting(DocumentListItem::title).containsExactly("Has Sender", "No Sender"); @@ -1700,7 +1712,8 @@ class DocumentServiceTest { .thenReturn(List.of(noReceivers, withReceiver)); DocumentSearchResult result = documentService.searchDocuments( - null, null, null, null, null, null, null, null, DocumentSort.RECEIVER, "asc", null, false, UNPAGED); + noFilters(), + DocumentSort.RECEIVER, "asc", UNPAGED); assertThat(result.items()).extracting(DocumentListItem::title) .containsExactly("Has Receiver", "No Receivers"); @@ -1733,7 +1746,8 @@ class DocumentServiceTest { .thenReturn(List.of(undatedBob, datedAnna, datedBob, undatedAnna)); DocumentSearchResult result = documentService.searchDocuments( - null, null, null, null, null, null, null, null, DocumentSort.SENDER, "asc", null, false, UNPAGED); + noFilters(), + DocumentSort.SENDER, "asc", UNPAGED); // Bob's group precedes Anna's group (ASC by sender). The sort is stable, so // within each group the input order is preserved (undatedBob, datedBob for Bob; @@ -1764,7 +1778,8 @@ class DocumentServiceTest { .thenReturn(List.of(undatedBob, datedAnna, datedBob, undatedAnna)); DocumentSearchResult result = documentService.searchDocuments( - null, null, null, null, null, null, null, null, DocumentSort.SENDER, "desc", null, false, UNPAGED); + noFilters(), + DocumentSort.SENDER, "desc", UNPAGED); // Anna's group precedes Bob's (DESC by sender); undated stays inside its group. assertThat(result.items()).extracting(DocumentListItem::title) @@ -1787,7 +1802,8 @@ class DocumentServiceTest { .thenReturn(List.of(undatedFromAlice)); DocumentSearchResult result = documentService.searchDocuments( - null, null, null, null, null, null, null, null, DocumentSort.SENDER, "asc", null, true, UNPAGED); + noFilters().withUndated(true), + DocumentSort.SENDER, "asc", UNPAGED); // The in-memory path queried via a Specification (built by buildSearchSpec with // undatedOnly(true)) rather than skipping straight to a sorted findAll. @@ -1803,8 +1819,9 @@ class DocumentServiceTest { when(documentRepository.findAll(any(org.springframework.data.jpa.domain.Specification.class))) .thenReturn(List.of()); - documentService.searchDocuments("brief", null, null, null, null, null, null, null, - DocumentSort.RELEVANCE, null, null, true, UNPAGED); + documentService.searchDocuments( + new SearchFilters("brief", null, null, null, null, null, null, null, null, true), + DocumentSort.RELEVANCE, null, UNPAGED); // The FTS-id path (buildSearchSpec) ran; the raw-page SQL shortcut did not. verify(documentRepository).findAllMatchingIdsByFts("brief"); @@ -1827,7 +1844,8 @@ class DocumentServiceTest { .thenReturn(List.of(docNullName, docSmith)); DocumentSearchResult result = documentService.searchDocuments( - null, null, null, null, null, null, null, null, DocumentSort.SENDER, "asc", null, false, UNPAGED); + noFilters(), + DocumentSort.SENDER, "asc", UNPAGED); // null lastName should sort to end (treated as empty), not before "smith" (as "null") assertThat(result.items()).extracting(DocumentListItem::title) @@ -1850,7 +1868,8 @@ class DocumentServiceTest { when(documentRepository.findEnrichmentData(any(), eq("Brief"))).thenReturn(rows); DocumentSearchResult result = documentService.searchDocuments( - "Brief", null, null, null, null, null, null, null, DocumentSort.RELEVANCE, null, null, false, UNPAGED); + new SearchFilters("Brief", null, null, null, null, null, null, null, null, false), + DocumentSort.RELEVANCE, null, UNPAGED); assertThat(result.items()).hasSize(1); SearchMatchData md = result.items().get(0).matchData(); @@ -1864,7 +1883,8 @@ class DocumentServiceTest { .thenReturn(new PageImpl<>(List.of())); DocumentSearchResult result = documentService.searchDocuments( - null, null, null, null, null, null, null, null, null, null, null, false, UNPAGED); + noFilters(), + null, null, UNPAGED); assertThat(result.items()).isEmpty(); } @@ -1884,7 +1904,8 @@ class DocumentServiceTest { when(documentRepository.findEnrichmentData(any(), eq("Brief"))).thenReturn(rows); DocumentSearchResult result = documentService.searchDocuments( - "Brief", null, null, null, null, null, null, null, DocumentSort.RELEVANCE, null, null, false, UNPAGED); + new SearchFilters("Brief", null, null, null, null, null, null, null, null, false), + DocumentSort.RELEVANCE, null, UNPAGED); SearchMatchData md = result.items().get(0).matchData(); assertThat(md.transcriptionSnippet()).isEqualTo("Hier ist der Brief aus Berlin"); @@ -2401,7 +2422,7 @@ class DocumentServiceTest { .thenReturn(List.of(d1, d2)); List result = documentService.findIdsForFilter( - null, null, null, null, null, null, null, null, null, false); + noFilters()); assertThat(result).containsExactly(d1.getId(), d2.getId()); } @@ -2416,7 +2437,7 @@ class DocumentServiceTest { when(tagService.expandTagNamesToDescendantIdSets(any())).thenReturn(List.of()); documentService.findIdsForFilter( - null, null, null, null, null, List.of("Brief"), null, null, TagOperator.OR, false); + new SearchFilters(null, null, null, null, null, List.of("Brief"), null, null, TagOperator.OR, false)); // Spec built without throwing → OR branch was exercised. Coverage gain // is in not-throwing on the OR-specific code path; the actual SQL is @@ -2429,7 +2450,7 @@ class DocumentServiceTest { when(documentRepository.findAllMatchingIdsByFts("xyz")).thenReturn(List.of()); List result = documentService.findIdsForFilter( - "xyz", null, null, null, null, null, null, null, null, false); + new SearchFilters("xyz", null, null, null, null, null, null, null, null, false)); assertThat(result).isEmpty(); verify(documentRepository, never()).findAll(any(org.springframework.data.jpa.domain.Specification.class)); diff --git a/backend/src/test/java/org/raddatz/familienarchiv/document/SearchFiltersFixtures.java b/backend/src/test/java/org/raddatz/familienarchiv/document/SearchFiltersFixtures.java new file mode 100644 index 00000000..ac2a012c --- /dev/null +++ b/backend/src/test/java/org/raddatz/familienarchiv/document/SearchFiltersFixtures.java @@ -0,0 +1,17 @@ +package org.raddatz.familienarchiv.document; + +/** Test fixtures for {@link SearchFilters}. */ +final class SearchFiltersFixtures { + + private SearchFiltersFixtures() {} + + /** + * A {@link SearchFilters} with no predicate active — the common search-test + * baseline. Combine with {@code .withUndated(true)} for the undated-only case; + * construct {@code new SearchFilters(...)} directly when a test pins a specific + * field, so the intent stays visible at the call site. + */ + static SearchFilters noFilters() { + return new SearchFilters(null, null, null, null, null, null, null, null, null, false); + } +}