test(#119): add Testcontainers @DataJpaTest against real PostgreSQL 16
Adds spring-boot-testcontainers and testcontainers-postgresql deps. PostgresContainerConfig declares a shared @ServiceConnection container used by DocumentRepositoryTest, PersonRepositoryTest, and an ApplicationContextTest smoke test. Flyway migrations are imported via FlywayConfig and run on every test execution, verifying the migration chain against a real PostgreSQL 16 container. No H2 is used. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -65,6 +65,16 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-jetty</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-testcontainers</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>testcontainers-postgresql</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator-test</artifactId>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.raddatz.familienarchiv;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
import org.testcontainers.containers.PostgreSQLContainer;
|
||||
import software.amazon.awssdk.services.s3.S3Client;
|
||||
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
|
||||
@ActiveProfiles("test")
|
||||
@Import(PostgresContainerConfig.class)
|
||||
class ApplicationContextTest {
|
||||
|
||||
@MockitoBean
|
||||
S3Client s3Client;
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
// verifies that the Spring context starts successfully with all beans wired,
|
||||
// Flyway migrations applied, and no configuration errors
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.raddatz.familienarchiv;
|
||||
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.testcontainers.containers.PostgreSQLContainer;
|
||||
|
||||
@TestConfiguration(proxyBeanMethods = false)
|
||||
public class PostgresContainerConfig {
|
||||
|
||||
@Bean
|
||||
@ServiceConnection
|
||||
PostgreSQLContainer<?> postgresContainer() {
|
||||
return new PostgreSQLContainer<>("postgres:16-alpine");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package org.raddatz.familienarchiv.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.raddatz.familienarchiv.PostgresContainerConfig;
|
||||
import org.raddatz.familienarchiv.config.FlywayConfig;
|
||||
import org.raddatz.familienarchiv.model.Document;
|
||||
import org.raddatz.familienarchiv.model.DocumentStatus;
|
||||
import org.raddatz.familienarchiv.model.Person;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase;
|
||||
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@DataJpaTest
|
||||
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
|
||||
@Import({PostgresContainerConfig.class, FlywayConfig.class})
|
||||
class DocumentRepositoryTest {
|
||||
|
||||
@Autowired
|
||||
private DocumentRepository documentRepository;
|
||||
|
||||
@Autowired
|
||||
private PersonRepository personRepository;
|
||||
|
||||
// ─── save and findById ────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void save_persistsDocument_andFindByIdReturnsSameDocument() {
|
||||
Document document = Document.builder()
|
||||
.title("Testbrief")
|
||||
.originalFilename("testbrief.pdf")
|
||||
.status(DocumentStatus.PLACEHOLDER)
|
||||
.build();
|
||||
|
||||
Document saved = documentRepository.save(document);
|
||||
Optional<Document> found = documentRepository.findById(saved.getId());
|
||||
|
||||
assertThat(found).isPresent();
|
||||
assertThat(found.get().getTitle()).isEqualTo("Testbrief");
|
||||
assertThat(found.get().getStatus()).isEqualTo(DocumentStatus.PLACEHOLDER);
|
||||
}
|
||||
|
||||
// ─── findByStatus ─────────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void findByStatus_returnsOnlyDocumentsWithMatchingStatus() {
|
||||
documentRepository.save(Document.builder()
|
||||
.title("Placeholder Doc")
|
||||
.originalFilename("placeholder.pdf")
|
||||
.status(DocumentStatus.PLACEHOLDER)
|
||||
.build());
|
||||
documentRepository.save(Document.builder()
|
||||
.title("Uploaded Doc")
|
||||
.originalFilename("uploaded.pdf")
|
||||
.status(DocumentStatus.UPLOADED)
|
||||
.build());
|
||||
|
||||
List<Document> placeholders = documentRepository.findByStatus(DocumentStatus.PLACEHOLDER);
|
||||
|
||||
assertThat(placeholders).extracting(Document::getStatus)
|
||||
.containsOnly(DocumentStatus.PLACEHOLDER);
|
||||
}
|
||||
|
||||
// ─── findByOriginalFilename ───────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void findByOriginalFilename_returnsDocument_whenFilenameMatches() {
|
||||
documentRepository.save(Document.builder()
|
||||
.title("Omas Brief")
|
||||
.originalFilename("omas_brief.pdf")
|
||||
.status(DocumentStatus.PLACEHOLDER)
|
||||
.build());
|
||||
|
||||
Optional<Document> found = documentRepository.findByOriginalFilename("omas_brief.pdf");
|
||||
|
||||
assertThat(found).isPresent();
|
||||
assertThat(found.get().getTitle()).isEqualTo("Omas Brief");
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByOriginalFilename_returnsEmpty_whenFilenameDoesNotExist() {
|
||||
Optional<Document> found = documentRepository.findByOriginalFilename("does_not_exist.pdf");
|
||||
|
||||
assertThat(found).isEmpty();
|
||||
}
|
||||
|
||||
// ─── existsByOriginalFilename ─────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void existsByOriginalFilename_returnsTrue_whenDocumentExists() {
|
||||
documentRepository.save(Document.builder()
|
||||
.title("Brief")
|
||||
.originalFilename("brief.pdf")
|
||||
.status(DocumentStatus.PLACEHOLDER)
|
||||
.build());
|
||||
|
||||
assertThat(documentRepository.existsByOriginalFilename("brief.pdf")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void existsByOriginalFilename_returnsFalse_whenDocumentDoesNotExist() {
|
||||
assertThat(documentRepository.existsByOriginalFilename("nonexistent.pdf")).isFalse();
|
||||
}
|
||||
|
||||
// ─── findBySenderId ───────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void findBySenderId_returnsDocuments_whereSenderIdMatches() {
|
||||
Person sender = personRepository.save(Person.builder()
|
||||
.firstName("Hans")
|
||||
.lastName("Müller")
|
||||
.build());
|
||||
documentRepository.save(Document.builder()
|
||||
.title("Brief von Hans")
|
||||
.originalFilename("brief_hans.pdf")
|
||||
.status(DocumentStatus.UPLOADED)
|
||||
.sender(sender)
|
||||
.build());
|
||||
|
||||
List<Document> docs = documentRepository.findBySenderId(sender.getId());
|
||||
|
||||
assertThat(docs).hasSize(1);
|
||||
assertThat(docs.get(0).getSender().getId()).isEqualTo(sender.getId());
|
||||
}
|
||||
|
||||
// ─── countByMetadataCompleteFalse ─────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void countByMetadataCompleteFalse_returnsNumberOfIncompleteDocuments() {
|
||||
documentRepository.save(Document.builder()
|
||||
.title("Incomplete")
|
||||
.originalFilename("incomplete.pdf")
|
||||
.status(DocumentStatus.PLACEHOLDER)
|
||||
.metadataComplete(false)
|
||||
.build());
|
||||
documentRepository.save(Document.builder()
|
||||
.title("Complete")
|
||||
.originalFilename("complete.pdf")
|
||||
.status(DocumentStatus.UPLOADED)
|
||||
.metadataComplete(true)
|
||||
.build());
|
||||
|
||||
assertThat(documentRepository.countByMetadataCompleteFalse()).isEqualTo(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package org.raddatz.familienarchiv.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.raddatz.familienarchiv.PostgresContainerConfig;
|
||||
import org.raddatz.familienarchiv.config.FlywayConfig;
|
||||
import org.raddatz.familienarchiv.model.Person;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase;
|
||||
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@DataJpaTest
|
||||
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
|
||||
@Import({PostgresContainerConfig.class, FlywayConfig.class})
|
||||
class PersonRepositoryTest {
|
||||
|
||||
@Autowired
|
||||
private PersonRepository personRepository;
|
||||
|
||||
// ─── save and findById ────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void save_persistsPerson_andFindByIdReturnsSamePerson() {
|
||||
Person person = Person.builder()
|
||||
.firstName("Anna")
|
||||
.lastName("Schmidt")
|
||||
.build();
|
||||
|
||||
Person saved = personRepository.save(person);
|
||||
Optional<Person> found = personRepository.findById(saved.getId());
|
||||
|
||||
assertThat(found).isPresent();
|
||||
assertThat(found.get().getFirstName()).isEqualTo("Anna");
|
||||
assertThat(found.get().getLastName()).isEqualTo("Schmidt");
|
||||
}
|
||||
|
||||
// ─── searchByName ─────────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void searchByName_findsByFirstName() {
|
||||
personRepository.save(Person.builder().firstName("Hans").lastName("Müller").build());
|
||||
personRepository.save(Person.builder().firstName("Anna").lastName("Schmidt").build());
|
||||
|
||||
List<Person> results = personRepository.searchByName("Hans");
|
||||
|
||||
assertThat(results).hasSize(1);
|
||||
assertThat(results.get(0).getFirstName()).isEqualTo("Hans");
|
||||
}
|
||||
|
||||
@Test
|
||||
void searchByName_findsByLastName() {
|
||||
personRepository.save(Person.builder().firstName("Hans").lastName("Müller").build());
|
||||
personRepository.save(Person.builder().firstName("Anna").lastName("Schmidt").build());
|
||||
|
||||
List<Person> results = personRepository.searchByName("Schmidt");
|
||||
|
||||
assertThat(results).hasSize(1);
|
||||
assertThat(results.get(0).getLastName()).isEqualTo("Schmidt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void searchByName_isCaseInsensitive() {
|
||||
personRepository.save(Person.builder().firstName("Hans").lastName("Müller").build());
|
||||
|
||||
List<Person> results = personRepository.searchByName("hans");
|
||||
|
||||
assertThat(results).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void searchByName_findsByAlias() {
|
||||
personRepository.save(Person.builder()
|
||||
.firstName("Hans").lastName("Müller").alias("Opa Hans").build());
|
||||
|
||||
List<Person> results = personRepository.searchByName("Opa Hans");
|
||||
|
||||
assertThat(results).hasSize(1);
|
||||
}
|
||||
|
||||
// ─── findAllByOrderByLastNameAscFirstNameAsc ──────────────────────────────
|
||||
|
||||
@Test
|
||||
void findAllByOrderByLastNameAscFirstNameAsc_returnsSortedByLastNameThenFirstName() {
|
||||
personRepository.save(Person.builder().firstName("Bernd").lastName("Ziegler").build());
|
||||
personRepository.save(Person.builder().firstName("Anna").lastName("Müller").build());
|
||||
personRepository.save(Person.builder().firstName("Clara").lastName("Müller").build());
|
||||
|
||||
List<Person> sorted = personRepository.findAllByOrderByLastNameAscFirstNameAsc();
|
||||
|
||||
assertThat(sorted).extracting(Person::getLastName)
|
||||
.startsWith("Müller", "Müller");
|
||||
assertThat(sorted.stream()
|
||||
.filter(p -> p.getLastName().equals("Müller"))
|
||||
.map(Person::getFirstName)
|
||||
.toList())
|
||||
.containsExactly("Anna", "Clara");
|
||||
}
|
||||
|
||||
// ─── findByAliasIgnoreCase ────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void findByAliasIgnoreCase_returnsMatchingPerson() {
|
||||
personRepository.save(Person.builder()
|
||||
.firstName("Karl").lastName("Brandt").alias("Opa Karl").build());
|
||||
|
||||
Optional<Person> found = personRepository.findByAliasIgnoreCase("opa karl");
|
||||
|
||||
assertThat(found).isPresent();
|
||||
assertThat(found.get().getFirstName()).isEqualTo("Karl");
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByAliasIgnoreCase_returnsEmpty_whenAliasDoesNotMatch() {
|
||||
Optional<Person> found = personRepository.findByAliasIgnoreCase("nobody");
|
||||
|
||||
assertThat(found).isEmpty();
|
||||
}
|
||||
|
||||
// ─── findByFirstNameIgnoreCaseAndLastNameIgnoreCase ───────────────────────
|
||||
|
||||
@Test
|
||||
void findByFirstNameIgnoreCaseAndLastNameIgnoreCase_returnsMatch() {
|
||||
personRepository.save(Person.builder().firstName("Maria").lastName("Raddatz").build());
|
||||
|
||||
Optional<Person> found = personRepository.findByFirstNameIgnoreCaseAndLastNameIgnoreCase(
|
||||
"maria", "raddatz");
|
||||
|
||||
assertThat(found).isPresent();
|
||||
assertThat(found.get().getFirstName()).isEqualTo("Maria");
|
||||
}
|
||||
}
|
||||
15
backend/src/test/resources/application-test.yaml
Normal file
15
backend/src/test/resources/application-test.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
app:
|
||||
s3:
|
||||
endpoint: http://localhost:9000
|
||||
access-key: dummy
|
||||
secret-key: dummy
|
||||
bucket: test-bucket
|
||||
region: us-east-1
|
||||
|
||||
spring:
|
||||
datasource:
|
||||
url: will-be-overridden-by-testcontainers
|
||||
username: test
|
||||
password: test
|
||||
mail:
|
||||
host: localhost
|
||||
Reference in New Issue
Block a user