diff --git a/backend/pom.xml b/backend/pom.xml index a5eddab8..81eacb23 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -65,6 +65,16 @@ org.springframework.boot spring-boot-starter-jetty + + org.springframework.boot + spring-boot-testcontainers + test + + + org.testcontainers + testcontainers-postgresql + test + org.springframework.boot spring-boot-starter-actuator-test diff --git a/backend/src/test/java/org/raddatz/familienarchiv/ApplicationContextTest.java b/backend/src/test/java/org/raddatz/familienarchiv/ApplicationContextTest.java new file mode 100644 index 00000000..899c526c --- /dev/null +++ b/backend/src/test/java/org/raddatz/familienarchiv/ApplicationContextTest.java @@ -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 + } +} diff --git a/backend/src/test/java/org/raddatz/familienarchiv/PostgresContainerConfig.java b/backend/src/test/java/org/raddatz/familienarchiv/PostgresContainerConfig.java new file mode 100644 index 00000000..20c0697e --- /dev/null +++ b/backend/src/test/java/org/raddatz/familienarchiv/PostgresContainerConfig.java @@ -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"); + } +} diff --git a/backend/src/test/java/org/raddatz/familienarchiv/repository/DocumentRepositoryTest.java b/backend/src/test/java/org/raddatz/familienarchiv/repository/DocumentRepositoryTest.java new file mode 100644 index 00000000..0a960117 --- /dev/null +++ b/backend/src/test/java/org/raddatz/familienarchiv/repository/DocumentRepositoryTest.java @@ -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 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 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 found = documentRepository.findByOriginalFilename("omas_brief.pdf"); + + assertThat(found).isPresent(); + assertThat(found.get().getTitle()).isEqualTo("Omas Brief"); + } + + @Test + void findByOriginalFilename_returnsEmpty_whenFilenameDoesNotExist() { + Optional 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 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); + } +} diff --git a/backend/src/test/java/org/raddatz/familienarchiv/repository/PersonRepositoryTest.java b/backend/src/test/java/org/raddatz/familienarchiv/repository/PersonRepositoryTest.java new file mode 100644 index 00000000..8ed8fb09 --- /dev/null +++ b/backend/src/test/java/org/raddatz/familienarchiv/repository/PersonRepositoryTest.java @@ -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 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 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 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 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 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 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 found = personRepository.findByAliasIgnoreCase("opa karl"); + + assertThat(found).isPresent(); + assertThat(found.get().getFirstName()).isEqualTo("Karl"); + } + + @Test + void findByAliasIgnoreCase_returnsEmpty_whenAliasDoesNotMatch() { + Optional found = personRepository.findByAliasIgnoreCase("nobody"); + + assertThat(found).isEmpty(); + } + + // ─── findByFirstNameIgnoreCaseAndLastNameIgnoreCase ─────────────────────── + + @Test + void findByFirstNameIgnoreCaseAndLastNameIgnoreCase_returnsMatch() { + personRepository.save(Person.builder().firstName("Maria").lastName("Raddatz").build()); + + Optional found = personRepository.findByFirstNameIgnoreCaseAndLastNameIgnoreCase( + "maria", "raddatz"); + + assertThat(found).isPresent(); + assertThat(found.get().getFirstName()).isEqualTo("Maria"); + } +} diff --git a/backend/src/test/resources/application-test.yaml b/backend/src/test/resources/application-test.yaml new file mode 100644 index 00000000..6a8cbad3 --- /dev/null +++ b/backend/src/test/resources/application-test.yaml @@ -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