diff --git a/backend/src/test/java/org/raddatz/familienarchiv/repository/MigrationIntegrationTest.java b/backend/src/test/java/org/raddatz/familienarchiv/repository/MigrationIntegrationTest.java new file mode 100644 index 00000000..7fb3c406 --- /dev/null +++ b/backend/src/test/java/org/raddatz/familienarchiv/repository/MigrationIntegrationTest.java @@ -0,0 +1,136 @@ +package org.raddatz.familienarchiv.repository; + +import jakarta.persistence.EntityManager; +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.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest; +import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase; +import org.springframework.context.annotation.Import; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Integration tests that verify DB-level constraints introduced in the OCR pipeline migrations + * are actually enforced by PostgreSQL. These tests exercise constraints that cannot be verified + * by unit tests alone. + */ +@DataJpaTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@Import({PostgresContainerConfig.class, FlywayConfig.class}) +class MigrationIntegrationTest { + + @Autowired JdbcTemplate jdbc; + @Autowired DocumentRepository documentRepository; + @Autowired EntityManager em; + + // ─── V23: chk_annotation_polygon_quad CHECK constraint ─────────────────── + + @Test + void v23_polygonCheckConstraint_rejectsNonQuadrilateral() { + UUID docId = createDocument(); + + // A 3-point polygon violates chk_annotation_polygon_quad (must be exactly 4 points or NULL) + assertThatThrownBy(() -> + jdbc.update( + """ + INSERT INTO document_annotations + (id, document_id, page_number, x, y, width, height, color, polygon) + VALUES (gen_random_uuid(), ?, 1, 0.1, 0.1, 0.5, 0.1, '#00C7B1', + '[[0.1,0.1],[0.9,0.1],[0.9,0.2]]'::jsonb) + """, + docId) + ).isInstanceOf(DataIntegrityViolationException.class); + } + + @Test + void v23_polygonCheckConstraint_allowsNullPolygon() { + UUID docId = createDocument(); + + int rows = jdbc.update( + """ + INSERT INTO document_annotations + (id, document_id, page_number, x, y, width, height, color, polygon) + VALUES (gen_random_uuid(), ?, 1, 0.1, 0.1, 0.5, 0.1, '#00C7B1', NULL) + """, + docId); + + assertThat(rows).isEqualTo(1); + } + + @Test + void v23_polygonCheckConstraint_allowsQuadrilateral() { + UUID docId = createDocument(); + + int rows = jdbc.update( + """ + INSERT INTO document_annotations + (id, document_id, page_number, x, y, width, height, color, polygon) + VALUES (gen_random_uuid(), ?, 1, 0.1, 0.1, 0.5, 0.1, '#00C7B1', + '[[0.1,0.1],[0.9,0.1],[0.9,0.2],[0.1,0.2]]'::jsonb) + """, + docId); + + assertThat(rows).isEqualTo(1); + } + + // ─── V30: idx_ocr_training_runs_one_running partial unique index ────────── + + @Test + @Transactional(propagation = Propagation.NOT_SUPPORTED) + void v30_partialUniqueIndex_preventsTwoRunningTrainingRuns() { + jdbc.update(""" + INSERT INTO ocr_training_runs (id, status, block_count, document_count, model_name) + VALUES (gen_random_uuid(), 'RUNNING', 10, 2, 'kurrent_v1') + """); + + // A second RUNNING row violates the partial unique index + assertThatThrownBy(() -> + jdbc.update(""" + INSERT INTO ocr_training_runs (id, status, block_count, document_count, model_name) + VALUES (gen_random_uuid(), 'RUNNING', 5, 1, 'kurrent_v1') + """) + ).isInstanceOf(DataIntegrityViolationException.class); + + // Clean up — runs outside the DataJpaTest transaction, so must be explicit + jdbc.update("DELETE FROM ocr_training_runs WHERE status = 'RUNNING'"); + } + + @Test + void v30_partialUniqueIndex_allowsMultipleDoneRuns() { + int rows1 = jdbc.update(""" + INSERT INTO ocr_training_runs (id, status, block_count, document_count, model_name) + VALUES (gen_random_uuid(), 'DONE', 10, 2, 'kurrent_v1') + """); + int rows2 = jdbc.update(""" + INSERT INTO ocr_training_runs (id, status, block_count, document_count, model_name) + VALUES (gen_random_uuid(), 'DONE', 15, 3, 'kurrent_v2') + """); + + assertThat(rows1).isEqualTo(1); + assertThat(rows2).isEqualTo(1); + } + + // ─── helpers ───────────────────────────────────────────────────────────── + + private UUID createDocument() { + Document doc = documentRepository.save(Document.builder() + .title("Testdokument") + .originalFilename("test.pdf") + .status(DocumentStatus.UPLOADED) + .build()); + // Flush so the row is visible to subsequent JdbcTemplate queries within the same transaction + em.flush(); + return doc.getId(); + } +}