test(migrations): add Testcontainers integration tests for V23 + V30 constraints
V23 introduced a JSONB check constraint (chk_annotation_polygon_quad) requiring polygon arrays to have exactly 4 points. V30 introduced a partial unique index preventing two concurrent RUNNING training runs. These are DB-level invariants that unit tests cannot verify — five Testcontainers tests now assert they are correctly applied by Flyway and enforced by PostgreSQL. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user