Spaces:
Build error
Build error
Claude
fix(sprint-f0): fondations — lazy imports, schémas conformes, pydantic-settings
193eb98 unverified | """ | |
| Tests des endpoints /api/v1/pages/{id} (Sprint 4 — Session A). | |
| Stratégie : | |
| - Données BDD créées directement via la session SQLAlchemy (pas via l'API) | |
| - master.json mockés via monkeypatch sur les méthodes de Path | |
| - Vérifie : 200, 404, structure du master.json, liste des couches | |
| Tests : | |
| - GET /api/v1/pages/{id} → 200 ou 404 | |
| - GET /api/v1/pages/{id}/master-json → 200 (mock), 404 (pas de fichier) | |
| - GET /api/v1/pages/{id}/layers → liste des couches disponibles | |
| """ | |
| # 1. stdlib | |
| import json | |
| import uuid | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| from unittest.mock import MagicMock, patch | |
| # 2. third-party | |
| import pytest | |
| # 3. local | |
| from app.models.corpus import CorpusModel, ManuscriptModel, PageModel | |
| from tests.conftest_api import async_client, db_session # noqa: F401 | |
| _NOW = datetime.now(timezone.utc) | |
| # --------------------------------------------------------------------------- | |
| # Helpers — création de données de test en BDD | |
| # --------------------------------------------------------------------------- | |
| async def _create_corpus(db_session, slug="test-corpus"): | |
| corpus = CorpusModel( | |
| id=str(uuid.uuid4()), | |
| slug=slug, | |
| title="Test Corpus", | |
| profile_id="medieval-illuminated", | |
| created_at=_NOW, | |
| updated_at=_NOW, | |
| ) | |
| db_session.add(corpus) | |
| await db_session.commit() | |
| await db_session.refresh(corpus) | |
| return corpus | |
| async def _create_manuscript(db_session, corpus_id, ms_id=None): | |
| ms = ManuscriptModel( | |
| id=ms_id or str(uuid.uuid4()), | |
| corpus_id=corpus_id, | |
| title="Test Manuscript", | |
| shelfmark="Latin 8878", | |
| date_label="XIe siècle", | |
| total_pages=1, | |
| ) | |
| db_session.add(ms) | |
| await db_session.commit() | |
| await db_session.refresh(ms) | |
| return ms | |
| async def _create_page(db_session, manuscript_id, page_id=None): | |
| page = PageModel( | |
| id=page_id or str(uuid.uuid4()), | |
| manuscript_id=manuscript_id, | |
| folio_label="f001r", | |
| sequence=1, | |
| image_master_path="/data/master/f001r.tif", | |
| processing_status="ANALYZED", | |
| confidence_summary=0.87, | |
| ) | |
| db_session.add(page) | |
| await db_session.commit() | |
| await db_session.refresh(page) | |
| return page | |
| def _make_master_json(page_id: str, corpus_profile: str = "medieval-illuminated") -> str: | |
| data = { | |
| "schema_version": "1.0", | |
| "page_id": page_id, | |
| "corpus_profile": corpus_profile, | |
| "manuscript_id": "ms-test", | |
| "folio_label": "f001r", | |
| "sequence": 1, | |
| "image": { | |
| "master": "https://example.com/f001r.jpg", | |
| "derivative_web": "/data/deriv/f001r.jpg", | |
| "thumbnail": "/data/thumb/f001r.jpg", | |
| "width": 1500, | |
| "height": 2000, | |
| }, | |
| "layout": {"regions": []}, | |
| "ocr": { | |
| "diplomatic_text": "Incipit liber primus", | |
| "blocks": [], | |
| "lines": [], | |
| "language": "la", | |
| "confidence": 0.87, | |
| "uncertain_segments": [], | |
| }, | |
| "translation": {"fr": "Ici commence le premier livre", "en": ""}, | |
| "summary": {"short": "Prologue", "detailed": "Le prologue du commentaire"}, | |
| "commentary": { | |
| "public": "Ce folio ouvre l'œuvre", | |
| "scholarly": "Analyse paléographique détaillée", | |
| "claims": [], | |
| }, | |
| "editorial": { | |
| "status": "machine_draft", | |
| "validated": False, | |
| "validated_by": None, | |
| "version": 1, | |
| "notes": [], | |
| }, | |
| } | |
| return json.dumps(data) | |
| # --------------------------------------------------------------------------- | |
| # GET /api/v1/pages/{id} | |
| # --------------------------------------------------------------------------- | |
| async def test_get_page_not_found(async_client): | |
| response = await async_client.get("/api/v1/pages/nonexistent-page") | |
| assert response.status_code == 404 | |
| async def test_get_page_not_found_detail(async_client): | |
| response = await async_client.get("/api/v1/pages/unknown") | |
| assert "introuvable" in response.json()["detail"].lower() | |
| async def test_get_page_ok(async_client, db_session): | |
| corpus = await _create_corpus(db_session) | |
| ms = await _create_manuscript(db_session, corpus.id) | |
| page = await _create_page(db_session, ms.id) | |
| response = await async_client.get(f"/api/v1/pages/{page.id}") | |
| assert response.status_code == 200 | |
| async def test_get_page_fields(async_client, db_session): | |
| corpus = await _create_corpus(db_session) | |
| ms = await _create_manuscript(db_session, corpus.id) | |
| page = await _create_page(db_session, ms.id) | |
| data = (await async_client.get(f"/api/v1/pages/{page.id}")).json() | |
| assert data["id"] == page.id | |
| assert data["manuscript_id"] == ms.id | |
| assert data["folio_label"] == "f001r" | |
| assert data["sequence"] == 1 | |
| assert data["processing_status"] == "ANALYZED" | |
| assert data["confidence_summary"] == pytest.approx(0.87) | |
| # --------------------------------------------------------------------------- | |
| # GET /api/v1/pages/{id}/master-json | |
| # --------------------------------------------------------------------------- | |
| async def test_master_json_page_not_found(async_client): | |
| response = await async_client.get("/api/v1/pages/unknown/master-json") | |
| assert response.status_code == 404 | |
| async def test_master_json_file_not_found(async_client, db_session, monkeypatch): | |
| corpus = await _create_corpus(db_session) | |
| ms = await _create_manuscript(db_session, corpus.id) | |
| page = await _create_page(db_session, ms.id) | |
| # Simule l'absence du fichier | |
| monkeypatch.setattr(Path, "exists", lambda self: False) | |
| response = await async_client.get(f"/api/v1/pages/{page.id}/master-json") | |
| assert response.status_code == 404 | |
| async def test_master_json_ok(async_client, db_session, monkeypatch): | |
| corpus = await _create_corpus(db_session) | |
| ms = await _create_manuscript(db_session, corpus.id) | |
| page = await _create_page(db_session, ms.id) | |
| master_data = _make_master_json(page.id) | |
| monkeypatch.setattr(Path, "exists", lambda self: True) | |
| monkeypatch.setattr(Path, "read_text", lambda self, **kw: master_data) | |
| response = await async_client.get(f"/api/v1/pages/{page.id}/master-json") | |
| assert response.status_code == 200 | |
| async def test_master_json_contains_page_id(async_client, db_session, monkeypatch): | |
| corpus = await _create_corpus(db_session) | |
| ms = await _create_manuscript(db_session, corpus.id) | |
| page = await _create_page(db_session, ms.id) | |
| master_data = _make_master_json(page.id) | |
| monkeypatch.setattr(Path, "exists", lambda self: True) | |
| monkeypatch.setattr(Path, "read_text", lambda self, **kw: master_data) | |
| data = (await async_client.get(f"/api/v1/pages/{page.id}/master-json")).json() | |
| assert data["page_id"] == page.id | |
| async def test_master_json_schema_version(async_client, db_session, monkeypatch): | |
| corpus = await _create_corpus(db_session) | |
| ms = await _create_manuscript(db_session, corpus.id) | |
| page = await _create_page(db_session, ms.id) | |
| master_data = _make_master_json(page.id) | |
| monkeypatch.setattr(Path, "exists", lambda self: True) | |
| monkeypatch.setattr(Path, "read_text", lambda self, **kw: master_data) | |
| data = (await async_client.get(f"/api/v1/pages/{page.id}/master-json")).json() | |
| assert data["schema_version"] == "1.0" | |
| async def test_master_json_ocr_present(async_client, db_session, monkeypatch): | |
| corpus = await _create_corpus(db_session) | |
| ms = await _create_manuscript(db_session, corpus.id) | |
| page = await _create_page(db_session, ms.id) | |
| master_data = _make_master_json(page.id) | |
| monkeypatch.setattr(Path, "exists", lambda self: True) | |
| monkeypatch.setattr(Path, "read_text", lambda self, **kw: master_data) | |
| data = (await async_client.get(f"/api/v1/pages/{page.id}/master-json")).json() | |
| assert data["ocr"]["diplomatic_text"] == "Incipit liber primus" | |
| # --------------------------------------------------------------------------- | |
| # GET /api/v1/pages/{id}/layers | |
| # --------------------------------------------------------------------------- | |
| async def test_layers_page_not_found(async_client): | |
| response = await async_client.get("/api/v1/pages/unknown/layers") | |
| assert response.status_code == 404 | |
| async def test_layers_file_not_found(async_client, db_session, monkeypatch): | |
| corpus = await _create_corpus(db_session) | |
| ms = await _create_manuscript(db_session, corpus.id) | |
| page = await _create_page(db_session, ms.id) | |
| monkeypatch.setattr(Path, "exists", lambda self: False) | |
| response = await async_client.get(f"/api/v1/pages/{page.id}/layers") | |
| assert response.status_code == 404 | |
| async def test_layers_ok_is_list(async_client, db_session, monkeypatch): | |
| corpus = await _create_corpus(db_session) | |
| ms = await _create_manuscript(db_session, corpus.id) | |
| page = await _create_page(db_session, ms.id) | |
| monkeypatch.setattr(Path, "exists", lambda self: True) | |
| monkeypatch.setattr(Path, "read_text", lambda self, **kw: _make_master_json(page.id)) | |
| response = await async_client.get(f"/api/v1/pages/{page.id}/layers") | |
| assert response.status_code == 200 | |
| layers = response.json() | |
| assert isinstance(layers, list) | |
| assert len(layers) > 0 | |
| async def test_layers_contains_image(async_client, db_session, monkeypatch): | |
| corpus = await _create_corpus(db_session) | |
| ms = await _create_manuscript(db_session, corpus.id) | |
| page = await _create_page(db_session, ms.id) | |
| monkeypatch.setattr(Path, "exists", lambda self: True) | |
| monkeypatch.setattr(Path, "read_text", lambda self, **kw: _make_master_json(page.id)) | |
| layers = (await async_client.get(f"/api/v1/pages/{page.id}/layers")).json() | |
| types = [l["layer_type"] for l in layers] | |
| assert "image" in types | |
| async def test_layers_contains_ocr(async_client, db_session, monkeypatch): | |
| corpus = await _create_corpus(db_session) | |
| ms = await _create_manuscript(db_session, corpus.id) | |
| page = await _create_page(db_session, ms.id) | |
| monkeypatch.setattr(Path, "exists", lambda self: True) | |
| monkeypatch.setattr(Path, "read_text", lambda self, **kw: _make_master_json(page.id)) | |
| layers = (await async_client.get(f"/api/v1/pages/{page.id}/layers")).json() | |
| types = [l["layer_type"] for l in layers] | |
| assert "ocr_diplomatic" in types | |
| async def test_layers_contains_translation(async_client, db_session, monkeypatch): | |
| corpus = await _create_corpus(db_session) | |
| ms = await _create_manuscript(db_session, corpus.id) | |
| page = await _create_page(db_session, ms.id) | |
| monkeypatch.setattr(Path, "exists", lambda self: True) | |
| monkeypatch.setattr(Path, "read_text", lambda self, **kw: _make_master_json(page.id)) | |
| layers = (await async_client.get(f"/api/v1/pages/{page.id}/layers")).json() | |
| types = [l["layer_type"] for l in layers] | |
| assert "translation_fr" in types | |
| async def test_layers_have_status(async_client, db_session, monkeypatch): | |
| corpus = await _create_corpus(db_session) | |
| ms = await _create_manuscript(db_session, corpus.id) | |
| page = await _create_page(db_session, ms.id) | |
| monkeypatch.setattr(Path, "exists", lambda self: True) | |
| monkeypatch.setattr(Path, "read_text", lambda self, **kw: _make_master_json(page.id)) | |
| layers = (await async_client.get(f"/api/v1/pages/{page.id}/layers")).json() | |
| for layer in layers: | |
| assert "layer_type" in layer | |
| assert "status" in layer | |
| assert "has_content" in layer | |