IIIF-Studio / backend /tests /test_api_pages.py
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}
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_get_page_not_found(async_client):
response = await async_client.get("/api/v1/pages/nonexistent-page")
assert response.status_code == 404
@pytest.mark.asyncio
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()
@pytest.mark.asyncio
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
@pytest.mark.asyncio
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
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
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
@pytest.mark.asyncio
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
@pytest.mark.asyncio
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
@pytest.mark.asyncio
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
@pytest.mark.asyncio
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"
@pytest.mark.asyncio
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
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_layers_page_not_found(async_client):
response = await async_client.get("/api/v1/pages/unknown/layers")
assert response.status_code == 404
@pytest.mark.asyncio
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
@pytest.mark.asyncio
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
@pytest.mark.asyncio
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
@pytest.mark.asyncio
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
@pytest.mark.asyncio
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
@pytest.mark.asyncio
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