generated from john/python-template
Revamped Sources related pages with the help of Gemini, which had a lot to say.
This commit is contained in:
+141
-128
@@ -1,16 +1,60 @@
|
||||
"""Tests for the sources page routes."""
|
||||
"""Tests for the sources page routes and Source model properties."""
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from sqlmodel import select
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from transcription.db import session_scope
|
||||
from transcription.db.models import Document
|
||||
from transcription.db.models import Job
|
||||
from transcription.db.models import JobStatus
|
||||
from transcription.db.models import Source
|
||||
from transcription.db.models import Document, Job, JobSourceStatus, JobStatus, Source
|
||||
|
||||
|
||||
# --- Unit Tests for Model @property Definitions ---
|
||||
|
||||
|
||||
class TestSourceModelProperties:
|
||||
"""Direct unit tests for Source computed properties."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_source_properties_with_no_job_sources(self):
|
||||
source = Source(
|
||||
page_number=1,
|
||||
upload_name="page_one.png",
|
||||
filename="stored_page_one.png",
|
||||
file_path="/tmp/stored_page_one.png",
|
||||
)
|
||||
|
||||
assert source.latest_job_source is None
|
||||
assert source.latest_status is None
|
||||
assert source.latest_error_detail is None
|
||||
assert source.document_name is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_source_properties_with_document_and_job_sources(self, seed_job):
|
||||
job_id = seed_job(
|
||||
filename="source_prop_test.png",
|
||||
status=JobStatus.FAILED,
|
||||
transcription_text=None,
|
||||
error_detail="Timeout during OCR parsing",
|
||||
)
|
||||
|
||||
async with session_scope() as session:
|
||||
job = await session.get(Job, job_id)
|
||||
assert job is not None
|
||||
source = (
|
||||
await session.exec(select(Source).where(Source.document_id == job.document_id))
|
||||
).first()
|
||||
assert source is not None
|
||||
|
||||
# Validate computed properties
|
||||
assert source.document_name is not None
|
||||
assert source.latest_status == JobSourceStatus.FAILED
|
||||
assert source.latest_error_detail == "Timeout during OCR parsing"
|
||||
assert source.latest_job_source is not None
|
||||
|
||||
|
||||
# --- Integration Tests for Page Rendering ---
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@@ -26,26 +70,24 @@ class TestSourcesPageRendering:
|
||||
assert "Sources" in response.text
|
||||
assert "No source file records found." in response.text
|
||||
|
||||
def test_sources_page_lists_seeded_sources(self, app_client):
|
||||
@pytest.mark.asyncio
|
||||
async def test_sources_page_lists_seeded_sources(self, app_client):
|
||||
_, client = app_client
|
||||
|
||||
async def _seed() -> None:
|
||||
async with session_scope() as session:
|
||||
document = Document(name="Source Document", document_type="letter")
|
||||
session.add(document)
|
||||
await session.flush()
|
||||
session.add(
|
||||
Source(
|
||||
document_id=document.id,
|
||||
page_number=1,
|
||||
upload_name="page_one.png",
|
||||
filename="stored_page_one.png",
|
||||
file_path="/tmp/stored_page_one.png",
|
||||
)
|
||||
async with session_scope() as session:
|
||||
document = Document(name="Source Document", document_type="letter")
|
||||
session.add(document)
|
||||
await session.flush()
|
||||
session.add(
|
||||
Source(
|
||||
document_id=document.id,
|
||||
page_number=1,
|
||||
upload_name="page_one.png",
|
||||
filename="stored_page_one.png",
|
||||
file_path="/tmp/stored_page_one.png",
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
asyncio.run(_seed())
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
response = client.get("/ui/sources")
|
||||
|
||||
@@ -53,41 +95,38 @@ class TestSourcesPageRendering:
|
||||
assert "page_one.png" in response.text
|
||||
assert "stored_page_one.png" in response.text
|
||||
|
||||
def test_sources_page_filters_to_document_context(self, app_client):
|
||||
@pytest.mark.asyncio
|
||||
async def test_sources_page_filters_to_document_context(self, app_client):
|
||||
_, client = app_client
|
||||
|
||||
async def _seed() -> str:
|
||||
async with session_scope() as session:
|
||||
target = Document(name="Target", document_type="letter")
|
||||
other = Document(name="Other", document_type="record")
|
||||
session.add(target)
|
||||
session.add(other)
|
||||
await session.flush()
|
||||
async with session_scope() as session:
|
||||
target = Document(name="Target", document_type="letter")
|
||||
other = Document(name="Other", document_type="record")
|
||||
session.add_all([target, other])
|
||||
await session.flush()
|
||||
|
||||
session.add(
|
||||
Source(
|
||||
document_id=target.id,
|
||||
page_number=1,
|
||||
upload_name="target_page.png",
|
||||
filename="target_stored.png",
|
||||
file_path="/tmp/target_stored.png",
|
||||
)
|
||||
session.add(
|
||||
Source(
|
||||
document_id=target.id,
|
||||
page_number=1,
|
||||
upload_name="target_page.png",
|
||||
filename="target_stored.png",
|
||||
file_path="/tmp/target_stored.png",
|
||||
)
|
||||
session.add(
|
||||
Source(
|
||||
document_id=other.id,
|
||||
page_number=1,
|
||||
upload_name="other_page.png",
|
||||
filename="other_stored.png",
|
||||
file_path="/tmp/other_stored.png",
|
||||
)
|
||||
)
|
||||
session.add(
|
||||
Source(
|
||||
document_id=other.id,
|
||||
page_number=1,
|
||||
upload_name="other_page.png",
|
||||
filename="other_stored.png",
|
||||
file_path="/tmp/other_stored.png",
|
||||
)
|
||||
await session.commit()
|
||||
return str(target.id)
|
||||
)
|
||||
await session.commit()
|
||||
target_id = str(target.id)
|
||||
|
||||
document_id = asyncio.run(_seed())
|
||||
|
||||
response = client.get(f"/ui/sources?document_id={document_id}")
|
||||
response = client.get(f"/ui/sources?document_id={target_id}")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Sources: Target" in response.text
|
||||
@@ -105,9 +144,10 @@ class TestSourcesPageRendering:
|
||||
assert "Sources for Job" in response.text
|
||||
assert "Back to Job" in response.text
|
||||
assert "job-page.png" in response.text
|
||||
assert "Job Source Status" in response.text
|
||||
|
||||
def test_sources_page_job_context_shows_job_source_status_and_error_detail(self, app_client, seed_job):
|
||||
def test_sources_page_job_context_shows_job_source_status_and_error_detail(
|
||||
self, app_client, seed_job
|
||||
):
|
||||
_, client = app_client
|
||||
job_id = seed_job(
|
||||
filename="job-failed-page.png",
|
||||
@@ -123,9 +163,18 @@ class TestSourcesPageRendering:
|
||||
assert "failed" in response.text.lower()
|
||||
assert "Provider timed out" in response.text
|
||||
|
||||
def test_source_detail_page_renders_preview_and_revision_box(self, app_client, seed_job):
|
||||
@pytest.mark.asyncio
|
||||
async def test_source_detail_page_renders_preview_and_revision_box(
|
||||
self, app_client, seed_job
|
||||
):
|
||||
_, client = app_client
|
||||
fixture_path = Path(__file__).resolve().parents[1] / "fixtures" / "images" / "valid" / "small_png.png"
|
||||
fixture_path = (
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "fixtures"
|
||||
/ "images"
|
||||
/ "valid"
|
||||
/ "small_png.png"
|
||||
)
|
||||
job_id = seed_job(
|
||||
filename="detail-source.png",
|
||||
transcription_text="original transcription text",
|
||||
@@ -133,101 +182,65 @@ class TestSourcesPageRendering:
|
||||
source_file=fixture_path,
|
||||
)
|
||||
|
||||
async def _get_source_id() -> str:
|
||||
async with session_scope() as session:
|
||||
job = await session.get(Job, job_id)
|
||||
assert job is not None
|
||||
source = (
|
||||
await session.exec(select(Source).where(Source.document_id == job.document_id))
|
||||
).first()
|
||||
assert source is not None
|
||||
return str(source.id)
|
||||
|
||||
source_id = asyncio.run(_get_source_id())
|
||||
async with session_scope() as session:
|
||||
job = await session.get(Job, job_id)
|
||||
assert job is not None
|
||||
source = (
|
||||
await session.exec(select(Source).where(Source.document_id == job.document_id))
|
||||
).first()
|
||||
assert source is not None
|
||||
source_id = str(source.id)
|
||||
|
||||
response = client.get(f"/ui/sources/{source_id}")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Source Page 1: detail-source.png" in response.text
|
||||
assert "Back to Sources" in response.text
|
||||
assert "automated raw transcription" in response.text.lower()
|
||||
assert "original transcription text" in response.text
|
||||
assert "curated human transcription" in response.text.lower()
|
||||
assert "human revision text" in response.text
|
||||
assert "Page Number:" in response.text
|
||||
assert "Stored Filename:" in response.text
|
||||
assert "Delete Source" in response.text
|
||||
|
||||
def test_source_detail_page_displays_job_source_status_and_error_detail(self, app_client, seed_job):
|
||||
_, client = app_client
|
||||
job_id = seed_job(
|
||||
filename="failed-source.png",
|
||||
status=JobStatus.FAILED,
|
||||
transcription_text=None,
|
||||
error_detail="Provider timed out",
|
||||
)
|
||||
|
||||
async def _get_source_id() -> str:
|
||||
async with session_scope() as session:
|
||||
job = await session.get(Job, job_id)
|
||||
assert job is not None
|
||||
source = (
|
||||
await session.exec(select(Source).where(Source.document_id == job.document_id))
|
||||
).first()
|
||||
assert source is not None
|
||||
return str(source.id)
|
||||
|
||||
source_id = asyncio.run(_get_source_id())
|
||||
response = client.get(f"/ui/sources/{source_id}")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "JOB SOURCE OUTCOMES" in response.text
|
||||
assert "Status:" in response.text
|
||||
assert "failed" in response.text.lower()
|
||||
assert "Error Detail:" in response.text
|
||||
assert "Provider timed out" in response.text
|
||||
|
||||
def test_source_delete_page_blocks_when_source_is_job_linked(self, app_client, seed_job):
|
||||
@pytest.mark.asyncio
|
||||
async def test_source_delete_page_blocks_when_source_is_job_linked(
|
||||
self, app_client, seed_job
|
||||
):
|
||||
_, client = app_client
|
||||
job_id = seed_job(filename="linked-source.png", transcription_text="linked text")
|
||||
|
||||
async def _get_source_id() -> str:
|
||||
async with session_scope() as session:
|
||||
job = await session.get(Job, job_id)
|
||||
assert job is not None
|
||||
source = (
|
||||
await session.exec(select(Source).where(Source.document_id == job.document_id))
|
||||
).first()
|
||||
assert source is not None
|
||||
return str(source.id)
|
||||
async with session_scope() as session:
|
||||
job = await session.get(Job, job_id)
|
||||
assert job is not None
|
||||
source = (
|
||||
await session.exec(select(Source).where(Source.document_id == job.document_id))
|
||||
).first()
|
||||
assert source is not None
|
||||
source_id = str(source.id)
|
||||
|
||||
source_id = asyncio.run(_get_source_id())
|
||||
response = client.get(f"/ui/sources/{source_id}/delete")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Delete Source Record" in response.text
|
||||
assert "Delete is only available for unlinked sources." in response.text
|
||||
|
||||
def test_source_delete_page_allows_unlinked_source(self, app_client):
|
||||
@pytest.mark.asyncio
|
||||
async def test_source_delete_page_allows_unlinked_source(self, app_client):
|
||||
_, client = app_client
|
||||
|
||||
async def _seed_unlinked_source() -> str:
|
||||
async with session_scope() as session:
|
||||
document = Document(name="Unlinked Source Doc", document_type="memo")
|
||||
session.add(document)
|
||||
await session.flush()
|
||||
source = Source(
|
||||
document_id=document.id,
|
||||
page_number=1,
|
||||
upload_name="orphan-source.png",
|
||||
filename="orphan-source.png",
|
||||
file_path="/tmp/orphan-source.png",
|
||||
)
|
||||
session.add(source)
|
||||
await session.commit()
|
||||
return str(source.id)
|
||||
async with session_scope() as session:
|
||||
document = Document(name="Unlinked Source Doc", document_type="memo")
|
||||
session.add(document)
|
||||
await session.flush()
|
||||
source = Source(
|
||||
document_id=document.id,
|
||||
page_number=1,
|
||||
upload_name="orphan-source.png",
|
||||
filename="orphan-source.png",
|
||||
file_path="/tmp/orphan-source.png",
|
||||
)
|
||||
session.add(source)
|
||||
await session.commit()
|
||||
source_id = str(source.id)
|
||||
|
||||
source_id = asyncio.run(_seed_unlinked_source())
|
||||
response = client.get(f"/ui/sources/{source_id}/delete")
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
Reference in New Issue
Block a user