"""Integration tests for additive V4 document and relationship API routes.""" from __future__ import annotations import asyncio from collections.abc import Generator from contextlib import contextmanager from pathlib import Path from uuid import UUID from fastapi import FastAPI from fastapi.testclient import TestClient from sqlmodel import select from transcription.api.errors import register_error_handlers from transcription.api.v4_documents import get_document_service from transcription.api.v4_documents import get_people_service from transcription.api.v4_documents import router from transcription.config import Settings from transcription.config import SqliteSettings from transcription.db import create_all from transcription.db.engine import get_database_url from transcription.db.engine import get_engine from transcription.db.models import Document from transcription.db.models import DocumentType from transcription.db.models import Person from transcription.db.session import dispose_session_factory from transcription.db.session import session_scope from transcription.services.documents import DocumentService from transcription.services.people import PeopleService def _seed_document_and_person( *, db_url: str, document_name: str = "API Doc", person_name: str = "API Person" ) -> tuple[UUID, UUID]: async def _seed() -> tuple[UUID, UUID]: async with session_scope(database_url=db_url) as session: document = Document(name=document_name) person = Person(full_name=person_name) session.add(document) session.add(person) await session.commit() await session.refresh(document) await session.refresh(person) return document.id, person.id return asyncio.run(_seed()) async def _document_type_id(*, db_url: str, label: str) -> UUID: async with session_scope(database_url=db_url) as session: document_type = (await session.exec(select(DocumentType).where(DocumentType.label == label))).one() return document_type.id @contextmanager def _v4_api_client(tmp_path: Path, *, db_filename: str) -> Generator[tuple[TestClient, str]]: settings = Settings( openrouter_api_key="test-key", database=SqliteSettings(path=str(tmp_path / db_filename)), environment="test", bootstrap_schema_on_startup=True, upload_dir=tmp_path / "uploads", prompt_dir=tmp_path / "prompts", ) db_url = get_database_url(settings) session_factory = None async def _bootstrap() -> None: nonlocal session_factory from transcription.db.session import get_session_factory session_factory = get_session_factory(database_url=db_url) await create_all(engine=get_engine(db_url)) asyncio.run(_bootstrap()) service = DocumentService(session_factory=session_factory) people_service = PeopleService(session_factory=session_factory) app = FastAPI() register_error_handlers(app) app.include_router(router) app.dependency_overrides[get_document_service] = lambda: service app.dependency_overrides[get_people_service] = lambda: people_service try: with TestClient(app) as client: yield client, db_url finally: asyncio.run(dispose_session_factory(db_url)) def test_list_document_types_returns_seeded_registry(tmp_path): with _v4_api_client(tmp_path, db_filename="api-types.db") as (client, _db_url): response = client.get("/api/v4/document-types") assert response.status_code == 200 payload = response.json() labels = {item["label"] for item in payload} assert {"Letter", "Record", "Memo"}.issubset(labels) def test_list_person_roles_returns_seeded_registry(tmp_path): with _v4_api_client(tmp_path, db_filename="api-roles.db") as (client, _db_url): response = client.get("/api/v4/person-roles") assert response.status_code == 200 payload = response.json() codes = {item["code"] for item in payload} assert {"author", "recipient", "mentioned"}.issubset(codes) def test_set_document_type_by_id_updates_canonical_field(tmp_path): with _v4_api_client(tmp_path, db_filename="api-doc-type.db") as (client, db_url): document_id, _ = _seed_document_and_person(db_url=db_url) type_id = asyncio.run(_document_type_id(db_url=db_url, label="Record")) response = client.put( f"/api/v4/documents/{document_id}/type", json={"document_type_id": str(type_id)}, ) assert response.status_code == 200 payload = response.json() assert payload["document_id"] == str(document_id) assert payload["document_type_id"] == str(type_id) def test_document_type_payload_requires_uuid_only(tmp_path): with _v4_api_client(tmp_path, db_filename="api-doc-type-validation.db") as (client, db_url): document_id, _ = _seed_document_and_person(db_url=db_url) missing = client.put(f"/api/v4/documents/{document_id}/type", json={}) invalid = client.put( f"/api/v4/documents/{document_id}/type", json={"document_type_id": "record"}, ) unexpected = client.put( f"/api/v4/documents/{document_id}/type", json={"document_type_id": str(UUID(int=1)), "ignored": True}, ) assert missing.status_code == 422 assert invalid.status_code == 422 assert unexpected.status_code == 422 def test_document_people_role_aware_write_read_and_delete(tmp_path): with _v4_api_client(tmp_path, db_filename="api-links.db") as (client, db_url): document_id, person_id = _seed_document_and_person(db_url=db_url) create_response = client.post( f"/api/v4/documents/{document_id}/people", json={"person_id": str(person_id), "role_code": "author"}, ) assert create_response.status_code == 200 created = create_response.json() assert created["document_id"] == str(document_id) assert created["person_id"] == str(person_id) assert created["role_code"] == "author" assert created["role_id"] is not None link_id = created["id"] update_response = client.patch( f"/api/v4/document-people/{link_id}", json={"role_code": "recipient"}, ) assert update_response.status_code == 200 updated = update_response.json() assert updated["role_code"] == "recipient" list_response = client.get(f"/api/v4/documents/{document_id}/people") assert list_response.status_code == 200 links = list_response.json()["links"] assert len(links) == 1 assert links[0]["role_code"] == "recipient" delete_response = client.delete(f"/api/v4/document-people/{link_id}") assert delete_response.status_code == 204 list_after_delete = client.get(f"/api/v4/documents/{document_id}/people") assert list_after_delete.status_code == 200 assert list_after_delete.json()["links"] == [] def test_document_person_link_defaults_to_author_when_role_is_omitted(tmp_path): with _v4_api_client(tmp_path, db_filename="api-default-role.db") as (client, db_url): document_id, person_id = _seed_document_and_person(db_url=db_url) response = client.post( f"/api/v4/documents/{document_id}/people", json={"person_id": str(person_id)}, ) assert response.status_code == 200 assert response.json()["role_code"] == "author" def test_duplicate_document_person_link_returns_conflict_envelope(tmp_path): with _v4_api_client(tmp_path, db_filename="api-dup.db") as (client, db_url): document_id, person_id = _seed_document_and_person(db_url=db_url) first = client.post( f"/api/v4/documents/{document_id}/people", json={"person_id": str(person_id), "role_code": "author"}, ) assert first.status_code == 200 second = client.post( f"/api/v4/documents/{document_id}/people", json={"person_id": str(person_id), "role_code": "author"}, ) assert second.status_code == 409 payload = second.json() assert payload["category"] == "conflict_error"