from __future__ import annotations from datetime import UTC from datetime import datetime from pathlib import Path 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 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, tmp_path): service = DocumentService(session_factory=default_session_factory) service.settings.upload_dir = tmp_path document = await service.create_document( Document( id=uuid4(), name="free-delete", document_type="memo", ) ) document_dir = service.settings.upload_dir / "documents" / str(document.id) document_dir.mkdir(parents=True, exist_ok=True) (document_dir / "leftover.txt").write_text("orphan", encoding="utf-8") await service.delete_document(document) assert not document_dir.exists() with pytest.raises(DocumentError): await service.read_document_detail(document.id) @pytest.mark.asyncio async def test_delete_document_removes_populated_storage_tree(default_session_factory, tmp_path): service = DocumentService(session_factory=default_session_factory) service.settings.upload_dir = tmp_path document = await service.create_document( Document( id=uuid4(), name="tree-delete", document_type="memo", ) ) document_dir = service.settings.upload_dir / "documents" / str(document.id) (document_dir / "page-1.jpg").parent.mkdir(parents=True, exist_ok=True) (document_dir / "page-1.jpg").write_bytes(b"one") (document_dir / "page-2.jpg").write_bytes(b"two") (document_dir / "nested" / "manifest.json").parent.mkdir(parents=True, exist_ok=True) (document_dir / "nested" / "manifest.json").write_text('{"ok": true}', encoding="utf-8") assert document_dir.exists() await service.delete_document(document) assert not document_dir.exists() @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_removes_links_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, ) ) await service.delete_person(person) links = await service.list_document_people(person_id=person.id) assert links == [] with pytest.raises(DocumentError): await service.read_person_detail(person.id) @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)