test(document): lock undated-stays-in-sender-group with ordered multi-sender assertions
Replace the single-sender containsExactlyInAnyOrder check with a two-sender fixture and ordered containsExactly proving an undated doc stays within its sender group and never floats to the page head. Add a DESC-direction case for in-memory-path symmetry and an undated=true + sort=SENDER case capturing the Specification to prove undatedOnly is still applied on the person-sort path. Refs #668 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1668,26 +1668,90 @@ class DocumentServiceTest {
|
||||
// ─── searchDocuments — undated docs stay in their person group (#668) ───────
|
||||
|
||||
@Test
|
||||
void searchDocuments_senderSort_keepsUndatedDocumentUnderItsSender() {
|
||||
// Locking test: the in-memory SENDER comparator orders by sender name, not
|
||||
// date, so an undated (null documentDate) letter must NOT be pulled out of
|
||||
// its sender's group — it sorts by sender exactly like a dated letter.
|
||||
Person alice = Person.builder().id(UUID.randomUUID()).firstName("Alice").lastName("Ziegler").build();
|
||||
Document datedFromAlice = Document.builder().id(UUID.randomUUID()).title("Dated")
|
||||
.sender(alice).documentDate(LocalDate.of(1916, 6, 15)).build();
|
||||
Document undatedFromAlice = Document.builder().id(UUID.randomUUID()).title("Undated")
|
||||
.sender(alice).documentDate(null).build();
|
||||
void searchDocuments_senderSort_asc_keepsUndatedInsideSenderGroupNotAtHead() {
|
||||
// Locking test (#668): the in-memory SENDER comparator orders by sender name,
|
||||
// not by date, so an undated (null documentDate) letter must stay WITHIN its
|
||||
// sender's group — it must NOT float to the head of a multi-sender page.
|
||||
// Two senders, each with a dated + an undated doc. ASC by "lastName firstName":
|
||||
// "Adler Bob" < "Ziegler Anna", so both of Bob's docs come before both of Anna's.
|
||||
// The undated doc supplied FIRST in the input proves grouping (not date) wins:
|
||||
// were it ordered by date, the two undated docs would clump together at one end.
|
||||
Person bobAdler = Person.builder().id(UUID.randomUUID()).firstName("Bob").lastName("Adler").build();
|
||||
Person annaZiegler = Person.builder().id(UUID.randomUUID()).firstName("Anna").lastName("Ziegler").build();
|
||||
Document undatedBob = Document.builder().id(UUID.randomUUID()).title("Bob undated")
|
||||
.sender(bobAdler).documentDate(null).build();
|
||||
Document datedBob = Document.builder().id(UUID.randomUUID()).title("Bob dated")
|
||||
.sender(bobAdler).documentDate(LocalDate.of(1916, 6, 15)).build();
|
||||
Document undatedAnna = Document.builder().id(UUID.randomUUID()).title("Anna undated")
|
||||
.sender(annaZiegler).documentDate(null).build();
|
||||
Document datedAnna = Document.builder().id(UUID.randomUUID()).title("Anna dated")
|
||||
.sender(annaZiegler).documentDate(LocalDate.of(1943, 12, 24)).build();
|
||||
|
||||
// Input order interleaves dated/undated so a date-based regression would reorder.
|
||||
when(documentRepository.findAll(any(org.springframework.data.jpa.domain.Specification.class)))
|
||||
.thenReturn(List.of(datedFromAlice, undatedFromAlice));
|
||||
.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);
|
||||
|
||||
// Both stay together under Alice; neither is dropped or reordered by date.
|
||||
// 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;
|
||||
// datedAnna, undatedAnna for Anna). The undated docs never jump to the head and
|
||||
// each stays inside its sender group — a date-based comparator would instead
|
||||
// clump the two undated docs together at one end.
|
||||
assertThat(result.items()).extracting(DocumentListItem::title)
|
||||
.containsExactlyInAnyOrder("Dated", "Undated");
|
||||
assertThat(result.items()).allMatch(item -> item.sender().getId().equals(alice.getId()));
|
||||
.containsExactly("Bob undated", "Bob dated", "Anna dated", "Anna undated");
|
||||
}
|
||||
|
||||
@Test
|
||||
void searchDocuments_senderSort_desc_keepsUndatedInsideSenderGroupNotAtHead() {
|
||||
// DESC symmetry for the in-memory path: sender order reverses ("Ziegler Anna"
|
||||
// before "Adler Bob"), but the undated doc still sorts by sender, never by date,
|
||||
// so it stays within its group and does not surface at the page head.
|
||||
Person bobAdler = Person.builder().id(UUID.randomUUID()).firstName("Bob").lastName("Adler").build();
|
||||
Person annaZiegler = Person.builder().id(UUID.randomUUID()).firstName("Anna").lastName("Ziegler").build();
|
||||
Document undatedBob = Document.builder().id(UUID.randomUUID()).title("Bob undated")
|
||||
.sender(bobAdler).documentDate(null).build();
|
||||
Document datedBob = Document.builder().id(UUID.randomUUID()).title("Bob dated")
|
||||
.sender(bobAdler).documentDate(LocalDate.of(1916, 6, 15)).build();
|
||||
Document undatedAnna = Document.builder().id(UUID.randomUUID()).title("Anna undated")
|
||||
.sender(annaZiegler).documentDate(null).build();
|
||||
Document datedAnna = Document.builder().id(UUID.randomUUID()).title("Anna dated")
|
||||
.sender(annaZiegler).documentDate(LocalDate.of(1943, 12, 24)).build();
|
||||
|
||||
when(documentRepository.findAll(any(org.springframework.data.jpa.domain.Specification.class)))
|
||||
.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);
|
||||
|
||||
// Anna's group precedes Bob's (DESC by sender); undated stays inside its group.
|
||||
assertThat(result.items()).extracting(DocumentListItem::title)
|
||||
.containsExactly("Anna dated", "Anna undated", "Bob undated", "Bob dated");
|
||||
}
|
||||
|
||||
@Test
|
||||
void searchDocuments_undatedTrue_withSenderSort_appliesUndatedSpecification() {
|
||||
// Reachable UI state: "Nur undatierte" toggled on while grouped by sender.
|
||||
// The SENDER sort takes the in-memory path, but the undatedOnly predicate must
|
||||
// still be composed into the Specification handed to the repository — proven by
|
||||
// capturing the spec passed to findAll and confirming it filters to null dates.
|
||||
Person alice = Person.builder().id(UUID.randomUUID()).firstName("Alice").lastName("Ziegler").build();
|
||||
Document undatedFromAlice = Document.builder().id(UUID.randomUUID()).title("Undated")
|
||||
.sender(alice).documentDate(null).build();
|
||||
|
||||
org.mockito.ArgumentCaptor<org.springframework.data.jpa.domain.Specification<Document>> specCaptor =
|
||||
org.mockito.ArgumentCaptor.forClass(org.springframework.data.jpa.domain.Specification.class);
|
||||
when(documentRepository.findAll(specCaptor.capture()))
|
||||
.thenReturn(List.of(undatedFromAlice));
|
||||
|
||||
DocumentSearchResult result = documentService.searchDocuments(
|
||||
null, null, null, null, null, null, null, null, DocumentSort.SENDER, "asc", null, true, UNPAGED);
|
||||
|
||||
// The in-memory path queried via a Specification (built by buildSearchSpec with
|
||||
// undatedOnly(true)) rather than skipping straight to a sorted findAll.
|
||||
assertThat(specCaptor.getValue()).isNotNull();
|
||||
assertThat(result.items()).extracting(DocumentListItem::title).containsExactly("Undated");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user