generated from john/python-template
UI update complete?
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC
|
||||
from datetime import datetime
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from transcription.db.models import Document
|
||||
from transcription.db.models import Job
|
||||
from transcription.db.models import DocumentPerson
|
||||
from transcription.db.models import DocumentPersonRole
|
||||
from transcription.db.models import Person
|
||||
from transcription.db.models import Source
|
||||
from transcription.services.documents import DocumentDeleteBlockedError
|
||||
from transcription.services.documents import DocumentError
|
||||
from transcription.services.documents import PersonDeleteBlockedError
|
||||
from transcription.services.documents import DocumentService
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_document_detail_allows_missing_sources(default_session_factory):
|
||||
service = DocumentService(session_factory=default_session_factory)
|
||||
|
||||
created = await service.create_document(
|
||||
Document(
|
||||
id=uuid4(),
|
||||
name="detail-doc",
|
||||
document_type="letter",
|
||||
)
|
||||
)
|
||||
|
||||
detail = await service.read_document_detail(created.id)
|
||||
|
||||
assert detail.id == created.id
|
||||
assert detail.sources == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_document_refreshes_updated_timestamp(default_session_factory):
|
||||
service = DocumentService(session_factory=default_session_factory)
|
||||
|
||||
created = await service.create_document(
|
||||
Document(
|
||||
id=uuid4(),
|
||||
name="timestamp-doc",
|
||||
document_type="letter",
|
||||
updated_at=datetime(2000, 1, 1, tzinfo=UTC),
|
||||
)
|
||||
)
|
||||
|
||||
original_updated_at = created.updated_at
|
||||
created.notes = "updated"
|
||||
|
||||
updated = await service.update_document(created)
|
||||
|
||||
assert updated.notes == "updated"
|
||||
assert updated.updated_at >= original_updated_at
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_document_blocks_when_dependencies_exist(default_session_factory):
|
||||
service = DocumentService(session_factory=default_session_factory)
|
||||
|
||||
document = await service.create_document(
|
||||
Document(
|
||||
id=uuid4(),
|
||||
name="blocked-delete",
|
||||
document_type="record",
|
||||
)
|
||||
)
|
||||
|
||||
async with service._session_scope() as session:
|
||||
session.add(
|
||||
Source(
|
||||
document_id=document.id,
|
||||
page_number=1,
|
||||
upload_name="001_page.png",
|
||||
filename="001_page.png",
|
||||
file_path="uploads/001_page.png",
|
||||
)
|
||||
)
|
||||
session.add(Job(document_id=document.id))
|
||||
await session.commit()
|
||||
|
||||
with pytest.raises(DocumentDeleteBlockedError):
|
||||
await service.delete_document(document)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_document_succeeds_when_unlinked(default_session_factory):
|
||||
service = DocumentService(session_factory=default_session_factory)
|
||||
|
||||
document = await service.create_document(
|
||||
Document(
|
||||
id=uuid4(),
|
||||
name="free-delete",
|
||||
document_type="memo",
|
||||
)
|
||||
)
|
||||
|
||||
await service.delete_document(document)
|
||||
|
||||
with pytest.raises(DocumentError):
|
||||
await service.read_document_detail(document.id)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_person_detail_loads_document_links(default_session_factory):
|
||||
service = DocumentService(session_factory=default_session_factory)
|
||||
|
||||
document = await service.create_document(
|
||||
Document(
|
||||
id=uuid4(),
|
||||
name="linked-doc",
|
||||
document_type="letter",
|
||||
)
|
||||
)
|
||||
person = await service.create_person(Person(full_name="Linked Person"))
|
||||
await service.create_document_person(
|
||||
DocumentPerson(
|
||||
document_id=document.id,
|
||||
person_id=person.id,
|
||||
role=DocumentPersonRole.AUTHOR,
|
||||
)
|
||||
)
|
||||
|
||||
detail = await service.read_person_detail(person.id)
|
||||
|
||||
assert detail.id == person.id
|
||||
assert len(detail.document_people) == 1
|
||||
assert detail.document_people[0].document is not None
|
||||
assert detail.document_people[0].document.name == "linked-doc"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_person_refreshes_updated_timestamp(default_session_factory):
|
||||
service = DocumentService(session_factory=default_session_factory)
|
||||
|
||||
created = await service.create_person(
|
||||
Person(
|
||||
full_name="timestamp-person",
|
||||
updated_at=datetime(2000, 1, 1, tzinfo=UTC),
|
||||
)
|
||||
)
|
||||
original_updated_at = created.updated_at
|
||||
created.display_name = "updated"
|
||||
|
||||
updated = await service.update_person(created)
|
||||
|
||||
assert updated.display_name == "updated"
|
||||
assert updated.updated_at >= original_updated_at
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_person_blocks_when_linked_documents_exist(default_session_factory):
|
||||
service = DocumentService(session_factory=default_session_factory)
|
||||
|
||||
document = await service.create_document(
|
||||
Document(
|
||||
id=uuid4(),
|
||||
name="block-person-delete-doc",
|
||||
document_type="record",
|
||||
)
|
||||
)
|
||||
person = await service.create_person(Person(full_name="Blocked Person"))
|
||||
await service.create_document_person(
|
||||
DocumentPerson(
|
||||
document_id=document.id,
|
||||
person_id=person.id,
|
||||
role=DocumentPersonRole.AUTHOR,
|
||||
)
|
||||
)
|
||||
|
||||
with pytest.raises(PersonDeleteBlockedError):
|
||||
await service.delete_person(person)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_person_succeeds_when_unlinked(default_session_factory):
|
||||
service = DocumentService(session_factory=default_session_factory)
|
||||
|
||||
person = await service.create_person(Person(full_name="Free Person"))
|
||||
|
||||
await service.delete_person(person)
|
||||
|
||||
with pytest.raises(DocumentError):
|
||||
await service.read_person_detail(person.id)
|
||||
@@ -9,6 +9,7 @@ from transcription.db.models import JobSourceStatus
|
||||
from transcription.db.models import JobStatus
|
||||
from transcription.db.models import Source
|
||||
from transcription.services.documents import DocumentService
|
||||
from transcription.services.jobs import JobDeleteBlockedError
|
||||
from transcription.services.jobs import JobService
|
||||
|
||||
|
||||
@@ -107,3 +108,111 @@ class TestJobService:
|
||||
next_job = await job_service.read_next_queued_job()
|
||||
assert next_job is not None
|
||||
assert next_job.id == first.id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_job_persists_provider_model_prompt(
|
||||
self,
|
||||
job_service: JobService,
|
||||
document_service: DocumentService,
|
||||
):
|
||||
document = Document(id=uuid4(), name="provider-doc")
|
||||
await document_service.create_document(document=document)
|
||||
|
||||
job = Job(
|
||||
document_id=document.id,
|
||||
provider="openrouter",
|
||||
model="google/gemini-2.5-flash",
|
||||
prompt_name="transcribe_document.md",
|
||||
)
|
||||
await job_service.create_job(job=job)
|
||||
|
||||
fetched = await job_service.read_job(job_id=job.id)
|
||||
assert fetched.provider == "openrouter"
|
||||
assert fetched.model == "google/gemini-2.5-flash"
|
||||
assert fetched.prompt_name == "transcribe_document.md"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_job_resolves_filename_from_linked_source(
|
||||
self,
|
||||
job_service: JobService,
|
||||
document_service: DocumentService,
|
||||
):
|
||||
document = Document(id=uuid4(), name="filename-doc")
|
||||
await document_service.create_document(document=document)
|
||||
|
||||
job = Job(document_id=document.id)
|
||||
await job_service.create_job(job=job)
|
||||
|
||||
async with job_service._session_scope() as session:
|
||||
source = Source(
|
||||
document_id=document.id,
|
||||
page_number=1,
|
||||
upload_name="page_001.png",
|
||||
filename="stored_page_001.png",
|
||||
file_path="/uploads/stored_page_001.png",
|
||||
)
|
||||
session.add(source)
|
||||
await session.flush()
|
||||
|
||||
session.add(
|
||||
JobSource(
|
||||
job_id=job.id,
|
||||
source_id=source.id,
|
||||
status=JobSourceStatus.PENDING,
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
fetched = await job_service.read_job(job_id=job.id)
|
||||
assert fetched.filename == "stored_page_001.png"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_job_with_guardrails_blocks_processing_jobs(
|
||||
self,
|
||||
job_service: JobService,
|
||||
document_service: DocumentService,
|
||||
):
|
||||
document = Document(id=uuid4(), name="processing-delete-doc")
|
||||
await document_service.create_document(document=document)
|
||||
|
||||
job = Job(document_id=document.id, status=JobStatus.PROCESSING)
|
||||
await job_service.create_job(job=job)
|
||||
|
||||
with pytest.raises(JobDeleteBlockedError):
|
||||
await job_service.delete_job_with_guardrails(job_id=job.id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_job_with_guardrails_removes_jobsource_links(
|
||||
self,
|
||||
job_service: JobService,
|
||||
document_service: DocumentService,
|
||||
):
|
||||
document = Document(id=uuid4(), name="delete-job-doc")
|
||||
await document_service.create_document(document=document)
|
||||
|
||||
job = Job(document_id=document.id, status=JobStatus.QUEUED)
|
||||
await job_service.create_job(job=job)
|
||||
|
||||
async with job_service._session_scope() as session:
|
||||
source = Source(
|
||||
document_id=document.id,
|
||||
page_number=1,
|
||||
upload_name="delete-job-source.jpg",
|
||||
filename="stored-delete-job-source.jpg",
|
||||
file_path="/uploads/stored-delete-job-source.jpg",
|
||||
)
|
||||
session.add(source)
|
||||
await session.flush()
|
||||
session.add(
|
||||
JobSource(
|
||||
job_id=job.id,
|
||||
source_id=source.id,
|
||||
status=JobSourceStatus.PENDING,
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
await job_service.delete_job_with_guardrails(job_id=job.id)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
await job_service.read_job(job_id=job.id)
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from sqlmodel import select
|
||||
|
||||
from transcription.config import Settings
|
||||
from transcription.db.models import Document
|
||||
from transcription.db.models import Job
|
||||
from transcription.db.models import JobSource
|
||||
from transcription.db.models import Source
|
||||
from transcription.services.store import UploadError
|
||||
from transcription.services.store import create_job_for_document
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_job_for_document_requires_at_least_one_upload(async_session, tmp_path):
|
||||
document = Document(id=uuid4(), name="needs-upload")
|
||||
async_session.add(document)
|
||||
await async_session.commit()
|
||||
|
||||
settings = Settings(openrouter_api_key="test-key", upload_dir=tmp_path)
|
||||
|
||||
with pytest.raises(UploadError):
|
||||
await create_job_for_document(
|
||||
document_id=document.id,
|
||||
uploads=[],
|
||||
session=async_session,
|
||||
settings=settings,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_job_for_document_sorts_uploads_and_creates_links(async_session, tmp_path):
|
||||
document = Document(id=uuid4(), name="ordered-upload-doc")
|
||||
async_session.add(document)
|
||||
await async_session.commit()
|
||||
|
||||
settings = Settings(openrouter_api_key="test-key", upload_dir=tmp_path)
|
||||
|
||||
result = await create_job_for_document(
|
||||
document_id=document.id,
|
||||
uploads=[
|
||||
("folder/b_page.pdf", b"b"),
|
||||
("folder/A_page.pdf", b"a"),
|
||||
],
|
||||
provider="openrouter",
|
||||
model="test-model",
|
||||
prompt_name="transcribe_document.md",
|
||||
session=async_session,
|
||||
settings=settings,
|
||||
)
|
||||
|
||||
created_job = await async_session.get(Job, result.job_id)
|
||||
assert created_job is not None
|
||||
assert created_job.provider == "openrouter"
|
||||
assert created_job.model == "test-model"
|
||||
assert created_job.prompt_name == "transcribe_document.md"
|
||||
|
||||
sources = (
|
||||
await async_session.exec(
|
||||
select(Source)
|
||||
.where(Source.document_id == document.id)
|
||||
.order_by(Source.page_number) # pyright: ignore[reportArgumentType]
|
||||
)
|
||||
).all()
|
||||
assert [source.upload_name for source in sources] == ["A_page.pdf", "b_page.pdf"]
|
||||
assert all(source.filename.endswith(".pdf") for source in sources)
|
||||
assert all("A_page" not in source.filename and "b_page" not in source.filename for source in sources)
|
||||
|
||||
job_sources = (await async_session.exec(select(JobSource).where(JobSource.job_id == result.job_id))).all()
|
||||
assert len(job_sources) == 2
|
||||
assert set(result.source_ids) == {job_source.source_id for job_source in job_sources}
|
||||
@@ -12,6 +12,8 @@ from transcription.db.models import JobStatus
|
||||
from transcription.db.models import Source
|
||||
from transcription.services.documents import DocumentService
|
||||
from transcription.services.jobs import JobService
|
||||
from transcription.services.transcription import SourceDeleteBlockedError
|
||||
from transcription.services.transcription import TranscriptionNotFoundError
|
||||
from transcription.services.transcription import TranscriptionService
|
||||
|
||||
|
||||
@@ -89,3 +91,66 @@ class TestTranscriptionServiceRevisionUpsert:
|
||||
assert len(revisions) == 1
|
||||
assert revisions[0].id == first.id
|
||||
assert revisions[0].revised_text == "Revision v2"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_source_from_job_context_removes_source_and_single_link(self, default_session_factory):
|
||||
documents = DocumentService(session_factory=default_session_factory)
|
||||
jobs = JobService(session_factory=default_session_factory)
|
||||
transcriptions = TranscriptionService(session_factory=default_session_factory)
|
||||
|
||||
document = Document(id=uuid4(), name="delete-source-success")
|
||||
await documents.create_document(document=document)
|
||||
|
||||
job = Job(document_id=document.id, status=JobStatus.QUEUED)
|
||||
await jobs.create_job(job=job)
|
||||
|
||||
source = Source(
|
||||
document_id=document.id,
|
||||
page_number=1,
|
||||
upload_name="delete.jpg",
|
||||
filename="delete.jpg",
|
||||
file_path="uploads/delete.jpg",
|
||||
)
|
||||
async with transcriptions._session_scope() as session:
|
||||
session.add(source)
|
||||
await session.flush()
|
||||
session.add(JobSource(job_id=job.id, source_id=source.id, status=JobSourceStatus.PENDING))
|
||||
await session.commit()
|
||||
await session.refresh(source)
|
||||
|
||||
await transcriptions.delete_source_from_job_context(job_id=job.id, source_id=source.id)
|
||||
|
||||
with pytest.raises(TranscriptionNotFoundError):
|
||||
await transcriptions.read_source(source.id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_source_from_job_context_blocks_when_other_job_links_exist(self, default_session_factory):
|
||||
documents = DocumentService(session_factory=default_session_factory)
|
||||
jobs = JobService(session_factory=default_session_factory)
|
||||
transcriptions = TranscriptionService(session_factory=default_session_factory)
|
||||
|
||||
document = Document(id=uuid4(), name="delete-source-blocked")
|
||||
await documents.create_document(document=document)
|
||||
|
||||
job_one = Job(document_id=document.id, status=JobStatus.QUEUED)
|
||||
job_two = Job(document_id=document.id, status=JobStatus.QUEUED)
|
||||
await jobs.create_job(job=job_one)
|
||||
await jobs.create_job(job=job_two)
|
||||
|
||||
source = Source(
|
||||
document_id=document.id,
|
||||
page_number=1,
|
||||
upload_name="shared.jpg",
|
||||
filename="shared.jpg",
|
||||
file_path="uploads/shared.jpg",
|
||||
)
|
||||
async with transcriptions._session_scope() as session:
|
||||
session.add(source)
|
||||
await session.flush()
|
||||
session.add(JobSource(job_id=job_one.id, source_id=source.id, status=JobSourceStatus.PENDING))
|
||||
session.add(JobSource(job_id=job_two.id, source_id=source.id, status=JobSourceStatus.PENDING))
|
||||
await session.commit()
|
||||
await session.refresh(source)
|
||||
|
||||
with pytest.raises(SourceDeleteBlockedError):
|
||||
await transcriptions.delete_source_from_job_context(job_id=job_one.id, source_id=source.id)
|
||||
|
||||
@@ -10,8 +10,10 @@ from transcription.db.models import JobSource
|
||||
from transcription.db.models import JobSourceStatus
|
||||
from transcription.db.models import Person
|
||||
from transcription.db.models import Source
|
||||
from transcription.services.documents import DocumentDeleteBlockedError
|
||||
from transcription.services.documents import DocumentService
|
||||
from transcription.services.jobs import JobService
|
||||
from transcription.services.transcription import SourceDeleteBlockedError
|
||||
from transcription.services.transcription import TranscriptionService
|
||||
|
||||
|
||||
@@ -123,3 +125,89 @@ async def test_transcription_service_job_source_crud_uses_caller_session(default
|
||||
await session.commit()
|
||||
|
||||
assert len(await transcriptions.list_job_sources(job_id=job.id)) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_document_detail_loads_linked_person_relationship(default_session_factory):
|
||||
documents = DocumentService(session_factory=default_session_factory)
|
||||
|
||||
document = await documents.create_document(Document(id=uuid4(), name="detail-person-doc"))
|
||||
person = await documents.create_person(Person(full_name="Grace Hopper"))
|
||||
await documents.create_document_person(
|
||||
DocumentPerson(
|
||||
document_id=document.id,
|
||||
person_id=person.id,
|
||||
role=DocumentPersonRole.AUTHOR,
|
||||
)
|
||||
)
|
||||
|
||||
detail = await documents.read_document_detail(document.id)
|
||||
|
||||
assert len(detail.document_people) == 1
|
||||
link = detail.document_people[0]
|
||||
assert link.person is not None
|
||||
assert link.person.full_name == "Grace Hopper"
|
||||
assert link.role == DocumentPersonRole.AUTHOR
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_document_delete_is_blocked_with_source_and_job_dependencies(default_session_factory):
|
||||
documents = DocumentService(session_factory=default_session_factory)
|
||||
jobs = JobService(session_factory=default_session_factory)
|
||||
transcriptions = TranscriptionService(session_factory=default_session_factory)
|
||||
|
||||
document = await documents.create_document(Document(id=uuid4(), name="blocked-by-deps"))
|
||||
job = await jobs.create_job(Job(document_id=document.id))
|
||||
source = await transcriptions.create_source(
|
||||
Source(
|
||||
document_id=document.id,
|
||||
page_number=1,
|
||||
upload_name="blocked.jpg",
|
||||
filename="blocked.jpg",
|
||||
file_path="uploads/blocked.jpg",
|
||||
)
|
||||
)
|
||||
await transcriptions.create_job_source(
|
||||
JobSource(
|
||||
job_id=job.id,
|
||||
source_id=source.id,
|
||||
status=JobSourceStatus.PENDING,
|
||||
)
|
||||
)
|
||||
|
||||
with pytest.raises(DocumentDeleteBlockedError) as exc_info:
|
||||
await documents.delete_document(document)
|
||||
|
||||
message = exc_info.value.message
|
||||
assert "Sources" in message
|
||||
assert "Jobs" in message
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_source_delete_blocks_when_linked_to_multiple_jobs(default_session_factory):
|
||||
documents = DocumentService(session_factory=default_session_factory)
|
||||
jobs = JobService(session_factory=default_session_factory)
|
||||
transcriptions = TranscriptionService(session_factory=default_session_factory)
|
||||
|
||||
document = await documents.create_document(Document(id=uuid4(), name="multi-job-source-doc"))
|
||||
job_one = await jobs.create_job(Job(document_id=document.id))
|
||||
job_two = await jobs.create_job(Job(document_id=document.id))
|
||||
|
||||
source = await transcriptions.create_source(
|
||||
Source(
|
||||
document_id=document.id,
|
||||
page_number=1,
|
||||
upload_name="shared-page.jpg",
|
||||
filename="shared-page.jpg",
|
||||
file_path="uploads/shared-page.jpg",
|
||||
)
|
||||
)
|
||||
await transcriptions.create_job_source(
|
||||
JobSource(job_id=job_one.id, source_id=source.id, status=JobSourceStatus.PENDING)
|
||||
)
|
||||
await transcriptions.create_job_source(
|
||||
JobSource(job_id=job_two.id, source_id=source.id, status=JobSourceStatus.PENDING)
|
||||
)
|
||||
|
||||
with pytest.raises(SourceDeleteBlockedError):
|
||||
await transcriptions.delete_source_from_job_context(job_id=job_one.id, source_id=source.id)
|
||||
|
||||
@@ -71,13 +71,18 @@ class TestProviderSettings:
|
||||
with pytest.raises(ValidationError):
|
||||
_make_settings(provider="not-a-provider")
|
||||
|
||||
def test_optional_fields_default_to_none(self):
|
||||
"""provider_model, openrouter_http_referer, and openrouter_app_title are None when unset."""
|
||||
def test_optional_provider_header_fields_default_to_none(self):
|
||||
"""openrouter_http_referer and openrouter_app_title are None when unset."""
|
||||
settings = _make_settings()
|
||||
assert settings.provider_model is None
|
||||
assert settings.openrouter_http_referer is None
|
||||
assert settings.openrouter_app_title is None
|
||||
|
||||
def test_provider_model_accepts_env_default(self, monkeypatch):
|
||||
"""provider_model is sourced when provided through environment configuration."""
|
||||
monkeypatch.setenv("PROVIDER_MODEL", "google/gemini-2.5-flash")
|
||||
settings = Settings(openrouter_api_key="test-key-abc123")
|
||||
assert settings.provider_model == "google/gemini-2.5-flash"
|
||||
|
||||
|
||||
class TestPathSettings:
|
||||
"""Verify filesystem path field types."""
|
||||
|
||||
@@ -22,10 +22,12 @@ from transcription.db import create_all
|
||||
from transcription.db import initialize_database_runtime
|
||||
from transcription.db import session_scope
|
||||
from transcription.db.models import Document
|
||||
from transcription.db.models import DocumentPerson
|
||||
from transcription.db.models import Job
|
||||
from transcription.db.models import JobSource
|
||||
from transcription.db.models import JobSourceStatus
|
||||
from transcription.db.models import JobStatus
|
||||
from transcription.db.models import Person
|
||||
from transcription.db.models import Source
|
||||
|
||||
RevisionSeed = str
|
||||
@@ -58,9 +60,12 @@ def clear_ui_database(app_client: tuple[FastAPI, TestClient]) -> None:
|
||||
|
||||
async def _clear() -> None:
|
||||
async with session_scope() as session:
|
||||
await session.exec(delete(JobSource))
|
||||
await session.exec(delete(DocumentPerson))
|
||||
await session.exec(delete(Source))
|
||||
await session.exec(delete(Job))
|
||||
await session.exec(delete(Document))
|
||||
await session.exec(delete(Person))
|
||||
await session.commit()
|
||||
|
||||
asyncio.run(_clear())
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
"""Tests for the documents page routes."""
|
||||
|
||||
import asyncio
|
||||
from datetime import UTC
|
||||
from datetime import date
|
||||
from datetime import datetime
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from transcription.db import session_scope
|
||||
from transcription.db.models import Document
|
||||
from transcription.db.models import DocumentPerson
|
||||
from transcription.db.models import DocumentPersonRole
|
||||
from transcription.db.models import Job
|
||||
from transcription.db.models import Person
|
||||
from transcription.db.models import Source
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestDocumentsPageRendering:
|
||||
"""Verify document list/detail routes render expected read states."""
|
||||
|
||||
def test_documents_page_renders_empty_state(self, app_client):
|
||||
"""GET /ui/documents renders empty-state text when no records exist."""
|
||||
_, client = app_client
|
||||
|
||||
response = client.get("/ui/documents")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Documents" in response.text
|
||||
assert "No documents yet." in response.text
|
||||
|
||||
def test_documents_page_lists_seeded_documents(self, app_client):
|
||||
"""GET /ui/documents lists seeded document cards."""
|
||||
_, client = app_client
|
||||
|
||||
async def _seed_document() -> None:
|
||||
async with session_scope() as session:
|
||||
session.add(Document(name="Seeded Document", document_type="letter"))
|
||||
await session.commit()
|
||||
|
||||
asyncio.run(_seed_document())
|
||||
|
||||
response = client.get("/ui/documents")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Seeded Document" in response.text
|
||||
assert "Type: letter" in response.text
|
||||
|
||||
def test_document_detail_page_renders_metadata_and_empty_related_sections(self, app_client):
|
||||
"""GET /ui/documents/{document_id} shows metadata and related empty states."""
|
||||
_, client = app_client
|
||||
|
||||
async def _seed_document() -> str:
|
||||
async with session_scope() as session:
|
||||
document = Document(
|
||||
name="Zenna Letter",
|
||||
document_type="letter",
|
||||
document_date=date(1885, 7, 13),
|
||||
document_date_raw="c. 1885",
|
||||
location_created="Ohio",
|
||||
notes="Family archive",
|
||||
archive_identifier="BOX-1-FOLDER-2",
|
||||
)
|
||||
session.add(document)
|
||||
await session.commit()
|
||||
await session.refresh(document)
|
||||
return str(document.id)
|
||||
|
||||
document_id = asyncio.run(_seed_document())
|
||||
|
||||
response = client.get(f"/ui/documents/{document_id}")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Zenna Letter" in response.text
|
||||
assert "Document type: letter" in response.text
|
||||
assert "Exact date: 1885-07-13" in response.text
|
||||
assert "Approximate date: c. 1885" in response.text
|
||||
assert "Location created: Ohio" in response.text
|
||||
assert "Archive identifier: BOX-1-FOLDER-2" in response.text
|
||||
assert "Notes: Family archive" in response.text
|
||||
assert "Created at (read-only):" in response.text
|
||||
assert "Updated at (read-only):" in response.text
|
||||
assert "No linked people yet." in response.text
|
||||
assert "No sources added yet." in response.text
|
||||
assert "No jobs created yet." in response.text
|
||||
assert "Add sources" in response.text
|
||||
assert "Create job" in response.text
|
||||
assert "View sources" in response.text
|
||||
assert "View jobs" in response.text
|
||||
assert "Edit document" in response.text
|
||||
assert "Delete document" in response.text
|
||||
|
||||
def test_document_detail_page_renders_related_people_sources_and_jobs(self, app_client):
|
||||
"""GET /ui/documents/{document_id} shows related records when present."""
|
||||
_, client = app_client
|
||||
|
||||
async def _seed_related() -> str:
|
||||
async with session_scope() as session:
|
||||
document = Document(name="Roster", document_type="record")
|
||||
person = Person(full_name="Jane Doe")
|
||||
session.add(document)
|
||||
session.add(person)
|
||||
await session.flush()
|
||||
|
||||
session.add(
|
||||
DocumentPerson(
|
||||
document_id=document.id,
|
||||
person_id=person.id,
|
||||
role=DocumentPersonRole.AUTHOR,
|
||||
)
|
||||
)
|
||||
session.add(
|
||||
Source(
|
||||
document_id=document.id,
|
||||
page_number=1,
|
||||
upload_name="001_page.png",
|
||||
filename="stored_001_page.png",
|
||||
file_path="/tmp/stored_001_page.png",
|
||||
)
|
||||
)
|
||||
session.add(
|
||||
Job(
|
||||
document_id=document.id,
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
await session.refresh(document)
|
||||
return str(document.id)
|
||||
|
||||
document_id = asyncio.run(_seed_related())
|
||||
|
||||
response = client.get(f"/ui/documents/{document_id}")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Jane Doe (author)" in response.text
|
||||
assert "Page 1: 001_page.png" in response.text
|
||||
assert "queued -" in response.text
|
||||
|
||||
def test_document_jobs_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()
|
||||
session.add(Job(document_id=target.id))
|
||||
session.add(Job(document_id=other.id))
|
||||
await session.commit()
|
||||
await session.refresh(target)
|
||||
return str(target.id)
|
||||
|
||||
document_id = asyncio.run(_seed())
|
||||
response = client.get(f"/ui/documents/{document_id}/jobs")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Jobs for Target" in response.text
|
||||
assert "Jobs for Other" not in response.text
|
||||
|
||||
def test_document_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()
|
||||
|
||||
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",
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
await session.refresh(target)
|
||||
return str(target.id)
|
||||
|
||||
document_id = asyncio.run(_seed())
|
||||
response = client.get(f"/ui/documents/{document_id}/sources")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Sources for Target" in response.text
|
||||
assert "target_page.png" in response.text
|
||||
assert "other_page.png" not in response.text
|
||||
|
||||
def test_document_detail_page_rejects_invalid_id(self, app_client):
|
||||
"""GET /ui/documents/{document_id} shows validation feedback for malformed IDs."""
|
||||
_, client = app_client
|
||||
|
||||
response = client.get("/ui/documents/not-a-uuid")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Invalid document id" in response.text
|
||||
|
||||
def test_document_detail_page_handles_missing_document(self, app_client):
|
||||
"""GET /ui/documents/{document_id} shows not-found state for unknown IDs."""
|
||||
_, client = app_client
|
||||
|
||||
response = client.get(f"/ui/documents/{uuid4()}")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Document not found" in response.text
|
||||
|
||||
def test_document_edit_page_renders_expected_fields(self, app_client):
|
||||
"""GET /ui/documents/{document_id}/edit renders editable fields and save controls."""
|
||||
_, client = app_client
|
||||
|
||||
async def _seed_document() -> str:
|
||||
async with session_scope() as session:
|
||||
document = Document(
|
||||
name="Editable Document",
|
||||
document_type="memo",
|
||||
document_date_raw="c. 1900",
|
||||
)
|
||||
session.add(document)
|
||||
await session.commit()
|
||||
await session.refresh(document)
|
||||
return str(document.id)
|
||||
|
||||
document_id = asyncio.run(_seed_document())
|
||||
|
||||
response = client.get(f"/ui/documents/{document_id}/edit")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Edit document" in response.text
|
||||
assert "Document name and document type are required." in response.text
|
||||
assert "Document name" in response.text
|
||||
assert "Document type" in response.text
|
||||
assert "Exact date (YYYY-MM-DD)" in response.text
|
||||
assert "Approximate date" in response.text
|
||||
assert "Document location" in response.text
|
||||
assert "Archive identifier" in response.text
|
||||
assert "Notes" in response.text
|
||||
assert "Save changes" in response.text
|
||||
|
||||
def test_document_delete_page_shows_confirmation_when_unlinked(self, app_client):
|
||||
"""GET /ui/documents/{document_id}/delete renders permanent-action confirmation if unlinked."""
|
||||
_, client = app_client
|
||||
|
||||
async def _seed_document() -> str:
|
||||
async with session_scope() as session:
|
||||
document = Document(name="Safe Delete", document_type="letter")
|
||||
session.add(document)
|
||||
await session.commit()
|
||||
await session.refresh(document)
|
||||
return str(document.id)
|
||||
|
||||
document_id = asyncio.run(_seed_document())
|
||||
|
||||
response = client.get(f"/ui/documents/{document_id}/delete")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Delete document" in response.text
|
||||
assert "This action permanently deletes the document." in response.text
|
||||
assert "Delete document permanently" in response.text
|
||||
|
||||
def test_document_delete_page_shows_blocked_state_when_dependencies_exist(self, app_client):
|
||||
"""GET /ui/documents/{document_id}/delete explains blocked deletion with dependency categories."""
|
||||
_, client = app_client
|
||||
|
||||
async def _seed_related() -> str:
|
||||
async with session_scope() as session:
|
||||
document = Document(name="Blocked Delete", document_type="record")
|
||||
session.add(document)
|
||||
await session.flush()
|
||||
|
||||
session.add(
|
||||
Source(
|
||||
document_id=document.id,
|
||||
page_number=1,
|
||||
upload_name="001_page.png",
|
||||
filename="stored_001_page.png",
|
||||
file_path="/tmp/stored_001_page.png",
|
||||
)
|
||||
)
|
||||
session.add(Job(document_id=document.id))
|
||||
await session.commit()
|
||||
await session.refresh(document)
|
||||
return str(document.id)
|
||||
|
||||
document_id = asyncio.run(_seed_related())
|
||||
|
||||
response = client.get(f"/ui/documents/{document_id}/delete")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Delete is blocked because related records exist." in response.text
|
||||
assert "Dependencies present: Sources, Jobs" in response.text
|
||||
assert "Go to Jobs" in response.text
|
||||
|
||||
def test_job_create_page_preselects_document_query_param(self, app_client):
|
||||
"""GET /ui/jobs/new?document_id=... includes the selected document in rendered state."""
|
||||
_, client = app_client
|
||||
|
||||
async def _seed_document() -> str:
|
||||
async with session_scope() as session:
|
||||
document = Document(
|
||||
name="Preselected Document",
|
||||
document_type="letter",
|
||||
created_at=datetime.now(UTC),
|
||||
updated_at=datetime.now(UTC),
|
||||
)
|
||||
session.add(document)
|
||||
await session.commit()
|
||||
await session.refresh(document)
|
||||
return str(document.id)
|
||||
|
||||
document_id = asyncio.run(_seed_document())
|
||||
|
||||
response = client.get(f"/ui/jobs/new?document_id={document_id}")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Preselected Document" in response.text
|
||||
@@ -1,10 +1,13 @@
|
||||
"""Tests for the jobs page route."""
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from transcription.db import session_scope
|
||||
from transcription.db.models import Document
|
||||
from transcription.db.models import JobStatus
|
||||
|
||||
|
||||
@@ -18,8 +21,39 @@ class TestPageRendering:
|
||||
response = client.get("/ui/jobs")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Create job" in response.text
|
||||
assert "No jobs yet." in response.text
|
||||
|
||||
def test_job_create_page_requires_existing_documents(self, app_client):
|
||||
"""GET /ui/jobs/new shows guidance when no Documents exist."""
|
||||
_, client = app_client
|
||||
|
||||
response = client.get("/ui/jobs/new")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Create job" in response.text
|
||||
assert "No documents available. Create a Document before creating a Job." in response.text
|
||||
|
||||
def test_job_create_page_lists_available_documents(self, app_client):
|
||||
"""GET /ui/jobs/new renders document choices when Documents exist."""
|
||||
_, client = app_client
|
||||
|
||||
async def _seed_document() -> None:
|
||||
async with session_scope() as session:
|
||||
session.add(Document(name="Seeded Document"))
|
||||
await session.commit()
|
||||
|
||||
asyncio.run(_seed_document())
|
||||
|
||||
response = client.get("/ui/jobs/new")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Create job" in response.text
|
||||
assert "Seeded Document" in response.text
|
||||
assert "Files are processed alphabetically by original filename." in response.text
|
||||
assert "No files uploaded yet." in response.text
|
||||
assert "Upload folder" in response.text
|
||||
|
||||
def test_jobs_page_lists_seeded_jobs(self, app_client, seed_job):
|
||||
"""GET /ui/jobs lists seeded jobs from the in-memory database."""
|
||||
_, client = app_client
|
||||
@@ -54,6 +88,14 @@ class TestPageRendering:
|
||||
assert "original text" in response.text
|
||||
assert "Document preview" in response.text
|
||||
assert "/uploads/detail.pdf" in response.text
|
||||
assert "Provider:" in response.text
|
||||
assert "Model:" in response.text
|
||||
assert "Prompt:" in response.text
|
||||
assert "Retry count:" in response.text
|
||||
assert "Last updated:" in response.text
|
||||
assert "Delete job" in response.text
|
||||
assert "Delete source" in response.text
|
||||
assert "This permanently deletes the source from this job context." in response.text
|
||||
|
||||
def test_job_detail_page_rejects_invalid_id(self, app_client):
|
||||
"""GET /ui/jobs/{job_id} shows validation feedback for malformed IDs."""
|
||||
@@ -105,3 +147,24 @@ class TestPageRendering:
|
||||
assert "Revision Editor" in response.text
|
||||
assert "Update revision" in response.text
|
||||
assert "hello" in response.text
|
||||
|
||||
def test_job_delete_page_shows_confirmation_when_not_processing(self, app_client, seed_job):
|
||||
_, client = app_client
|
||||
job_id = seed_job(filename="delete-ready.pdf", status=JobStatus.TRANSCRIBED)
|
||||
|
||||
response = client.get(f"/ui/jobs/{job_id}/delete")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Delete job" in response.text
|
||||
assert "This action permanently deletes the job." in response.text
|
||||
assert "Delete job permanently" in response.text
|
||||
|
||||
def test_job_delete_page_shows_blocked_state_when_processing(self, app_client, seed_job):
|
||||
_, client = app_client
|
||||
job_id = seed_job(filename="delete-blocked.pdf", status=JobStatus.PROCESSING)
|
||||
|
||||
response = client.get(f"/ui/jobs/{job_id}/delete")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Delete is blocked while the job is processing." in response.text
|
||||
assert "Wait for processing to complete, then retry delete." in response.text
|
||||
|
||||
@@ -11,8 +11,12 @@ class TestPageRegistration:
|
||||
"""Mounted UI routes respond successfully when the full app is created."""
|
||||
_, client = app_client
|
||||
|
||||
upload_response = client.get("/ui/upload")
|
||||
upload_response = client.get("/ui/upload", follow_redirects=False)
|
||||
documents_response = client.get("/ui/documents")
|
||||
people_response = client.get("/ui/people")
|
||||
jobs_response = client.get("/ui/jobs")
|
||||
|
||||
assert upload_response.status_code == 200
|
||||
assert upload_response.status_code == 307
|
||||
assert documents_response.status_code == 200
|
||||
assert people_response.status_code == 200
|
||||
assert jobs_response.status_code == 200
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
"""Tests for the people page routes."""
|
||||
|
||||
import asyncio
|
||||
from datetime import date
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from transcription.db import session_scope
|
||||
from transcription.db.models import Document
|
||||
from transcription.db.models import DocumentPerson
|
||||
from transcription.db.models import DocumentPersonRole
|
||||
from transcription.db.models import Person
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestPeoplePageRendering:
|
||||
"""Verify people routes render expected CRUD read states."""
|
||||
|
||||
def test_people_page_renders_empty_state(self, app_client):
|
||||
_, client = app_client
|
||||
|
||||
response = client.get("/ui/people")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "People" in response.text
|
||||
assert "Create new person" in response.text
|
||||
assert "No people yet." in response.text
|
||||
|
||||
def test_people_page_lists_seeded_people(self, app_client):
|
||||
_, client = app_client
|
||||
|
||||
async def _seed_person() -> None:
|
||||
async with session_scope() as session:
|
||||
session.add(Person(full_name="Ada Lovelace", display_name="Ada"))
|
||||
await session.commit()
|
||||
|
||||
asyncio.run(_seed_person())
|
||||
|
||||
response = client.get("/ui/people")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Ada Lovelace" in response.text
|
||||
assert "Display name: Ada" in response.text
|
||||
|
||||
def test_person_create_page_renders_fields(self, app_client):
|
||||
_, client = app_client
|
||||
|
||||
response = client.get("/ui/people/new")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Create person" in response.text
|
||||
assert "Full name is required." in response.text
|
||||
assert "Birth date (YYYY-MM-DD)" in response.text
|
||||
assert "Death date (YYYY-MM-DD)" in response.text
|
||||
assert "Biography" in response.text
|
||||
assert "Save person" in response.text
|
||||
|
||||
def test_person_detail_page_renders_metadata_and_empty_links(self, app_client):
|
||||
_, client = app_client
|
||||
|
||||
async def _seed_person() -> str:
|
||||
async with session_scope() as session:
|
||||
person = Person(
|
||||
full_name="Grace Hopper",
|
||||
display_name="Grace",
|
||||
maiden_name="Murray",
|
||||
birth_date=date(1906, 12, 9),
|
||||
birth_date_raw="1906",
|
||||
birth_place="New York",
|
||||
death_date=date(1992, 1, 1),
|
||||
death_date_raw="1992",
|
||||
death_place="Arlington",
|
||||
biography="Computer pioneer",
|
||||
portrait_path="/images/grace.jpg",
|
||||
)
|
||||
session.add(person)
|
||||
await session.commit()
|
||||
await session.refresh(person)
|
||||
return str(person.id)
|
||||
|
||||
person_id = asyncio.run(_seed_person())
|
||||
|
||||
response = client.get(f"/ui/people/{person_id}")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Grace Hopper" in response.text
|
||||
assert "Display name: Grace" in response.text
|
||||
assert "Maiden name: Murray" in response.text
|
||||
assert "Birth date: 1906-12-09" in response.text
|
||||
assert "Death date: 1992-01-01" in response.text
|
||||
assert "Biography: Computer pioneer" in response.text
|
||||
assert "Portrait path: /images/grace.jpg" in response.text
|
||||
assert "Created at (read-only):" in response.text
|
||||
assert "Updated at (read-only):" in response.text
|
||||
assert "No linked documents yet." in response.text
|
||||
assert "Link this person from a Document workflow." in response.text
|
||||
|
||||
def test_person_detail_page_renders_linked_documents(self, app_client):
|
||||
_, client = app_client
|
||||
|
||||
async def _seed_links() -> str:
|
||||
async with session_scope() as session:
|
||||
person = Person(full_name="Linked Person")
|
||||
document = Document(name="Linked Document", document_type="letter")
|
||||
session.add(person)
|
||||
session.add(document)
|
||||
await session.flush()
|
||||
|
||||
session.add(
|
||||
DocumentPerson(
|
||||
document_id=document.id,
|
||||
person_id=person.id,
|
||||
role=DocumentPersonRole.AUTHOR,
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
await session.refresh(person)
|
||||
return str(person.id)
|
||||
|
||||
person_id = asyncio.run(_seed_links())
|
||||
|
||||
response = client.get(f"/ui/people/{person_id}")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Linked Document (author)" in response.text
|
||||
|
||||
def test_person_detail_page_handles_invalid_id(self, app_client):
|
||||
_, client = app_client
|
||||
|
||||
response = client.get("/ui/people/not-a-uuid")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Invalid person id" in response.text
|
||||
|
||||
def test_person_detail_page_handles_missing_person(self, app_client):
|
||||
_, client = app_client
|
||||
|
||||
response = client.get(f"/ui/people/{uuid4()}")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Person not found" in response.text
|
||||
|
||||
def test_person_edit_page_renders_expected_fields(self, app_client):
|
||||
_, client = app_client
|
||||
|
||||
async def _seed_person() -> str:
|
||||
async with session_scope() as session:
|
||||
person = Person(full_name="Editable Person", display_name="EP")
|
||||
session.add(person)
|
||||
await session.commit()
|
||||
await session.refresh(person)
|
||||
return str(person.id)
|
||||
|
||||
person_id = asyncio.run(_seed_person())
|
||||
|
||||
response = client.get(f"/ui/people/{person_id}/edit")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Edit person" in response.text
|
||||
assert "Full name is required." in response.text
|
||||
assert "Full name" in response.text
|
||||
assert "Save changes" in response.text
|
||||
|
||||
def test_person_delete_page_shows_confirmation_when_unlinked(self, app_client):
|
||||
_, client = app_client
|
||||
|
||||
async def _seed_person() -> str:
|
||||
async with session_scope() as session:
|
||||
person = Person(full_name="Safe Delete")
|
||||
session.add(person)
|
||||
await session.commit()
|
||||
await session.refresh(person)
|
||||
return str(person.id)
|
||||
|
||||
person_id = asyncio.run(_seed_person())
|
||||
|
||||
response = client.get(f"/ui/people/{person_id}/delete")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Delete person" in response.text
|
||||
assert "This action permanently deletes the person." in response.text
|
||||
assert "Delete person permanently" in response.text
|
||||
|
||||
def test_person_delete_page_shows_blocked_state_when_linked_documents_exist(self, app_client):
|
||||
_, client = app_client
|
||||
|
||||
async def _seed_links() -> str:
|
||||
async with session_scope() as session:
|
||||
person = Person(full_name="Blocked Delete")
|
||||
document = Document(name="Linked Document", document_type="record")
|
||||
session.add(person)
|
||||
session.add(document)
|
||||
await session.flush()
|
||||
|
||||
session.add(
|
||||
DocumentPerson(
|
||||
document_id=document.id,
|
||||
person_id=person.id,
|
||||
role=DocumentPersonRole.AUTHOR,
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
await session.refresh(person)
|
||||
return str(person.id)
|
||||
|
||||
person_id = asyncio.run(_seed_links())
|
||||
|
||||
response = client.get(f"/ui/people/{person_id}/delete")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Delete is blocked because linked documents exist." in response.text
|
||||
assert "Linked documents: 1" in response.text
|
||||
assert "Go to Documents" in response.text
|
||||
@@ -16,21 +16,17 @@ class TestPageRendering:
|
||||
assert response.headers["location"] == "/ui"
|
||||
|
||||
def test_ui_redirects_to_upload(self, app_client):
|
||||
"""GET /ui redirects to the upload page."""
|
||||
"""GET /ui redirects to the jobs page."""
|
||||
_, client = app_client
|
||||
response = client.get("/ui", follow_redirects=False)
|
||||
|
||||
assert response.status_code == 307
|
||||
assert response.headers["location"] == "/ui/upload"
|
||||
assert response.headers["location"] == "/ui/jobs"
|
||||
|
||||
def test_upload_page_renders_expected_controls(self, app_client):
|
||||
"""GET /ui/upload returns the page shell and upload controls."""
|
||||
"""GET /ui/upload redirects to the job-create flow."""
|
||||
_, client = app_client
|
||||
response = client.get("/ui/upload")
|
||||
response = client.get("/ui/upload", follow_redirects=False)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "VibeScribe" in response.text
|
||||
assert "Upload Document" in response.text
|
||||
assert "Select document file" in response.text
|
||||
assert "Upload" in response.text
|
||||
assert "Jobs" in response.text
|
||||
assert response.status_code == 307
|
||||
assert response.headers["location"] == "/ui/jobs/new"
|
||||
|
||||
Reference in New Issue
Block a user