test(search): integration test covers paged search against real Postgres — address @saraholt
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 3m4s
CI / OCR Service Tests (pull_request) Successful in 32s
CI / Backend Unit Tests (pull_request) Failing after 2m53s
CI / Unit & Component Tests (push) Failing after 3m7s
CI / OCR Service Tests (push) Successful in 31s
CI / Backend Unit Tests (push) Failing after 2m57s

Seeds 120 UPLOADED docs with a deterministic date spread and runs
DocumentService.searchDocuments against a Testcontainers Postgres, not
a Mockito mock. Five cases:

  1. First page returns exactly page_size items + correct totalElements
  2. Last partial page returns the tail slice (offset 100 → 20 items)
  3. Page beyond last returns empty content, totalElements still 120
  4. SENDER sort path slices in-memory + reports correct total
  5. Different pages return disjoint document id sets

Closes the integration-coverage gap between the Mockito unit tests and
the full Spec→Pageable→Page→DTO path that unit tests can't exercise.
Runs in ~87 s against the shared Testcontainers instance. (#316)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-24 10:55:51 +02:00
parent a8edd0d9fd
commit 4f741a8701

View File

@@ -0,0 +1,137 @@
package org.raddatz.familienarchiv.service;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.raddatz.familienarchiv.PostgresContainerConfig;
import org.raddatz.familienarchiv.dto.DocumentSearchResult;
import org.raddatz.familienarchiv.dto.DocumentSort;
import org.raddatz.familienarchiv.model.Document;
import org.raddatz.familienarchiv.model.DocumentStatus;
import org.raddatz.familienarchiv.repository.DocumentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.data.domain.PageRequest;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import software.amazon.awssdk.services.s3.S3Client;
import java.time.LocalDate;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
/**
* End-to-end paged search test with real PostgreSQL (Testcontainers). Covers the
* Specification→Pageable→Page→DTO path that unit tests mock around. Seeds 120
* UPLOADED documents and asserts the slice/total/totalPages arithmetic holds
* against the actual JPA query.
*
* <p>Closes the integration-coverage gap Sara flagged on PR #316.
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@ActiveProfiles("test")
@Import(PostgresContainerConfig.class)
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
class DocumentSearchPagedIntegrationTest {
private static final int FIXTURE_SIZE = 120;
@MockitoBean S3Client s3Client;
@Autowired DocumentService documentService;
@Autowired DocumentRepository documentRepository;
@BeforeEach
void seed() {
// Deterministic date spread so DATE-DESC order is predictable:
// document #0 has the oldest date, document #119 has the newest.
for (int i = 0; i < FIXTURE_SIZE; i++) {
Document doc = Document.builder()
.title("Dok-" + String.format("%03d", i))
.originalFilename("dok-" + i + ".pdf")
.status(DocumentStatus.UPLOADED)
.documentDate(LocalDate.of(1900, 1, 1).plusDays(i))
.build();
documentRepository.save(doc);
}
assertThat(documentRepository.count()).isEqualTo(FIXTURE_SIZE);
}
@Test
void search_firstPage_returnsExactlyPageSizeItems_andCorrectTotalElements() {
DocumentSearchResult result = documentService.searchDocuments(
null, null, null, null, null, null, null, null,
DocumentSort.DATE, "DESC", null,
PageRequest.of(0, 50));
assertThat(result.items()).hasSize(50);
assertThat(result.totalElements()).isEqualTo(FIXTURE_SIZE);
assertThat(result.pageNumber()).isZero();
assertThat(result.pageSize()).isEqualTo(50);
assertThat(result.totalPages()).isEqualTo(3); // ceil(120 / 50)
}
@Test
void search_lastPartialPage_returnsRemainingItems() {
DocumentSearchResult result = documentService.searchDocuments(
null, null, null, null, null, null, null, null,
DocumentSort.DATE, "DESC", null,
PageRequest.of(2, 50));
// Page 2 (offset 100) of 120 docs → exactly 20 items on the tail.
assertThat(result.items()).hasSize(20);
assertThat(result.totalElements()).isEqualTo(FIXTURE_SIZE);
assertThat(result.pageNumber()).isEqualTo(2);
}
@Test
void search_pageBeyondLast_returnsEmptyContent_totalElementsStillCorrect() {
DocumentSearchResult result = documentService.searchDocuments(
null, null, null, null, null, null, null, null,
DocumentSort.DATE, "DESC", null,
PageRequest.of(99, 50));
assertThat(result.items()).isEmpty();
assertThat(result.totalElements()).isEqualTo(FIXTURE_SIZE);
}
@Test
void search_senderSort_pageOne_slicesInMemory_withCorrectTotal() {
// SENDER sort path fetches all + sorts + slices in-memory (see scaling
// 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,
PageRequest.of(1, 50));
assertThat(result.items()).hasSize(50);
assertThat(result.totalElements()).isEqualTo(FIXTURE_SIZE);
assertThat(result.pageNumber()).isEqualTo(1);
assertThat(result.totalPages()).isEqualTo(3);
}
@Test
void search_differentPagesReturnDisjointSlices() {
DocumentSearchResult page0 = documentService.searchDocuments(
null, null, null, null, null, null, null, null,
DocumentSort.DATE, "DESC", null,
PageRequest.of(0, 50));
DocumentSearchResult page1 = documentService.searchDocuments(
null, null, null, null, null, null, null, null,
DocumentSort.DATE, "DESC", null,
PageRequest.of(1, 50));
// No document id should appear on both pages — slicing must be exclusive.
var idsOnPage0 = page0.items().stream()
.map(item -> item.document().getId())
.toList();
var idsOnPage1 = page1.items().stream()
.map(item -> item.document().getId())
.toList();
for (UUID id : idsOnPage0) {
assertThat(idsOnPage1).doesNotContain(id);
}
}
}