feat(person): add filter-aware paged repository queries
Add PersonSearchResult (mirrors DocumentSearchResult shape) and PersonFilter records, plus paired findByFilter/countByFilter native queries sharing one WHERE clause so the rendered page and totalElements can never drift. Filters (type, familyOnly, hasDocuments, provisional, readerDefault, q) each disable via a null/false param. Tested against real Postgres via Testcontainers. Refs #667 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -505,4 +505,156 @@ class PersonRepositoryTest {
|
||||
.filter(p -> p.getId().equals(provisional.getId())).findFirst().orElseThrow();
|
||||
assertThat(summary.isProvisional()).isTrue();
|
||||
}
|
||||
|
||||
// ─── #667: filter-aware paged slice + paired COUNT (Postgres-only) ────────
|
||||
// The slice query (findByFilter) and the count query (countByFilter) MUST share one
|
||||
// WHERE clause so totalElements can never drift from the rendered page. These tests run
|
||||
// against real Postgres because the slice ORDER BY uses a computed alias that fails on H2.
|
||||
|
||||
private void seedDirectoryFixture() {
|
||||
// Register family member, no documents — visible by reader default (familyMember)
|
||||
personRepository.save(Person.builder().firstName("Karl").lastName("Register").familyMember(true).build());
|
||||
// Person with one document — visible by reader default (documentCount > 0)
|
||||
Person hasDoc = personRepository.save(Person.builder().firstName("Doku").lastName("Person").build());
|
||||
documentRepository.save(Document.builder().title("B").originalFilename("b.pdf")
|
||||
.status(DocumentStatus.UPLOADED).sender(hasDoc).build());
|
||||
// Provisional, zero-document, non-family — hidden by reader default
|
||||
personRepository.save(Person.builder().firstName("Unbe").lastName("Staetigt").provisional(true).build());
|
||||
// An institution with no documents, non-family, non-provisional
|
||||
personRepository.save(Person.builder().lastName("Verlag GmbH").personType(PersonType.INSTITUTION).build());
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByFilter_readerDefault_returnsOnlyFamilyOrWithDocuments() {
|
||||
seedDirectoryFixture();
|
||||
|
||||
List<PersonSummaryDTO> slice = personRepository.findByFilter(
|
||||
null, null, null, null, true, null, 50, 0);
|
||||
|
||||
assertThat(slice).extracting(PersonSummaryDTO::getLastName)
|
||||
.containsExactlyInAnyOrder("Register", "Person");
|
||||
}
|
||||
|
||||
@Test
|
||||
void countByFilter_readerDefault_matchesSliceSize() {
|
||||
seedDirectoryFixture();
|
||||
|
||||
long count = personRepository.countByFilter(null, null, null, null, true, null);
|
||||
|
||||
assertThat(count).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByFilter_showAll_returnsEveryone() {
|
||||
seedDirectoryFixture();
|
||||
|
||||
List<PersonSummaryDTO> slice = personRepository.findByFilter(
|
||||
null, null, null, null, false, null, 50, 0);
|
||||
|
||||
assertThat(slice).hasSize(4);
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByFilter_typeInstitution_returnsOnlyInstitutions() {
|
||||
seedDirectoryFixture();
|
||||
|
||||
List<PersonSummaryDTO> slice = personRepository.findByFilter(
|
||||
"INSTITUTION", null, null, null, false, null, 50, 0);
|
||||
|
||||
assertThat(slice).extracting(PersonSummaryDTO::getLastName).containsExactly("Verlag GmbH");
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByFilter_familyOnly_returnsOnlyFamilyMembers() {
|
||||
seedDirectoryFixture();
|
||||
|
||||
List<PersonSummaryDTO> slice = personRepository.findByFilter(
|
||||
null, true, null, null, false, null, 50, 0);
|
||||
|
||||
assertThat(slice).extracting(PersonSummaryDTO::getLastName).containsExactly("Register");
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByFilter_hasDocuments_returnsOnlyPersonsWithDocuments() {
|
||||
seedDirectoryFixture();
|
||||
|
||||
List<PersonSummaryDTO> slice = personRepository.findByFilter(
|
||||
null, null, true, null, false, null, 50, 0);
|
||||
|
||||
assertThat(slice).extracting(PersonSummaryDTO::getLastName).containsExactly("Person");
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByFilter_provisionalTrue_returnsOnlyProvisional() {
|
||||
seedDirectoryFixture();
|
||||
|
||||
List<PersonSummaryDTO> slice = personRepository.findByFilter(
|
||||
null, null, null, true, false, null, 50, 0);
|
||||
|
||||
assertThat(slice).extracting(PersonSummaryDTO::getLastName).containsExactly("Staetigt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByFilter_combinedFilters_andTogether() {
|
||||
seedDirectoryFixture();
|
||||
// family + has-documents → intersection is empty (Register has no docs, Doku is not family)
|
||||
List<PersonSummaryDTO> slice = personRepository.findByFilter(
|
||||
null, true, true, null, false, null, 50, 0);
|
||||
|
||||
assertThat(slice).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByFilter_query_combinesWithFilters() {
|
||||
seedDirectoryFixture();
|
||||
|
||||
List<PersonSummaryDTO> slice = personRepository.findByFilter(
|
||||
null, null, null, null, false, "Verlag", 50, 0);
|
||||
|
||||
assertThat(slice).extracting(PersonSummaryDTO::getLastName).containsExactly("Verlag GmbH");
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByFilter_pageBeyondRange_returnsEmptySlice() {
|
||||
seedDirectoryFixture();
|
||||
|
||||
List<PersonSummaryDTO> slice = personRepository.findByFilter(
|
||||
null, null, null, null, false, null, 50, 999 * 50);
|
||||
|
||||
assertThat(slice).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByFilter_respectsPageSize() {
|
||||
seedDirectoryFixture();
|
||||
|
||||
List<PersonSummaryDTO> firstPage = personRepository.findByFilter(
|
||||
null, null, null, null, false, null, 2, 0);
|
||||
List<PersonSummaryDTO> secondPage = personRepository.findByFilter(
|
||||
null, null, null, null, false, null, 2, 2);
|
||||
|
||||
assertThat(firstPage).hasSize(2);
|
||||
assertThat(secondPage).hasSize(2);
|
||||
assertThat(firstPage).extracting(PersonSummaryDTO::getId)
|
||||
.doesNotContainAnyElementsOf(secondPage.stream().map(PersonSummaryDTO::getId).toList());
|
||||
}
|
||||
|
||||
@Test
|
||||
void countByFilter_typeInstitution_matchesSlice() {
|
||||
seedDirectoryFixture();
|
||||
|
||||
long count = personRepository.countByFilter("INSTITUTION", null, null, null, false, null);
|
||||
|
||||
assertThat(count).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByFilter_projectsDocumentCount() {
|
||||
seedDirectoryFixture();
|
||||
|
||||
List<PersonSummaryDTO> slice = personRepository.findByFilter(
|
||||
null, null, true, null, false, null, 50, 0);
|
||||
|
||||
assertThat(slice.get(0).getDocumentCount()).isEqualTo(1);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user