"""Tests for /api/v1/documents routes (U7).""" from __future__ import annotations import asyncio from pathlib import Path import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from agentkit.documents.db import init_documents_db from agentkit.documents.renderers.excel_renderer import ExcelRenderer from agentkit.documents.renderers.pdf_renderer import PDFRenderer from agentkit.documents.renderers.word_renderer import WordRenderer from agentkit.documents.service import DocumentService from agentkit.server.routes import documents as documents_routes @pytest.fixture def app(tmp_path: Path) -> FastAPI: """Create a test app with DocumentService initialized.""" db_path = tmp_path / "test.db" upload_dir = tmp_path / "uploads" asyncio.run(init_documents_db(db_path)) service = DocumentService(upload_dir=upload_dir, db_path=db_path) service.register_renderer("word", WordRenderer()) service.register_renderer("excel", ExcelRenderer()) service.register_renderer("pdf", PDFRenderer()) app = FastAPI() app.state.document_service = service app.state.server_config = None # No API key configured → allow all app.include_router(documents_routes.router, prefix="/api/v1") return app @pytest.fixture def client(app: FastAPI) -> TestClient: return TestClient(app) # --------------------------------------------------------------------------- # POST /create # --------------------------------------------------------------------------- def test_create_word(client: TestClient) -> None: """POST /create with format=word returns 200 + document metadata.""" resp = client.post( "/api/v1/documents/create", json={ "format": "word", "content": "# Test\n\nParagraph.", "conversation_id": "conv-1", }, ) assert resp.status_code == 200 data = resp.json() assert data["success"] is True assert data["document"]["format"] == "word" assert data["document"]["filename"].endswith(".docx") assert data["document"]["download_url"].startswith("/api/v1/documents/download/") def test_create_pdf(client: TestClient) -> None: """POST /create with format=pdf returns 200.""" resp = client.post( "/api/v1/documents/create", json={ "format": "pdf", "content": "# PDF Test", "conversation_id": "conv-1", }, ) assert resp.status_code == 200 assert resp.json()["document"]["format"] == "pdf" def test_create_excel_json(client: TestClient) -> None: """POST /create with format=excel and JSON content returns 200.""" resp = client.post( "/api/v1/documents/create", json={ "format": "excel", "content": '{"Data": [["A", "B"], ["1", "2"]]}', "conversation_id": "conv-1", }, ) assert resp.status_code == 200 assert resp.json()["document"]["format"] == "excel" def test_create_invalid_format(client: TestClient) -> None: """POST /create with invalid format returns 400.""" resp = client.post( "/api/v1/documents/create", json={ "format": "pptx", "content": "test", "conversation_id": "conv-1", }, ) assert resp.status_code == 400 def test_create_missing_fields(client: TestClient) -> None: """POST /create with missing required fields returns 422.""" resp = client.post( "/api/v1/documents/create", json={"format": "word"}, ) assert resp.status_code == 422 # Pydantic validation error # --------------------------------------------------------------------------- # GET /conversation/{id} # --------------------------------------------------------------------------- def test_list_conversation_documents(client: TestClient) -> None: """GET /conversation/{id} returns documents for that conversation.""" # Create a document first client.post( "/api/v1/documents/create", json={ "format": "word", "content": "# Doc 1", "conversation_id": "conv-list", }, ) client.post( "/api/v1/documents/create", json={ "format": "pdf", "content": "# Doc 2", "conversation_id": "conv-list", }, ) resp = client.get("/api/v1/documents/conversation/conv-list") assert resp.status_code == 200 data = resp.json() assert data["success"] is True assert data["count"] == 2 assert data["conversation_id"] == "conv-list" formats = [d["format"] for d in data["documents"]] assert "word" in formats assert "pdf" in formats def test_list_empty_conversation(client: TestClient) -> None: """GET /conversation/{id} with no documents returns empty list.""" resp = client.get("/api/v1/documents/conversation/no-such-conv") assert resp.status_code == 200 data = resp.json() assert data["count"] == 0 assert data["documents"] == [] # --------------------------------------------------------------------------- # GET /download/{doc_id} # --------------------------------------------------------------------------- def test_download_document(client: TestClient) -> None: """GET /download/{doc_id} returns the file.""" # Create a document create_resp = client.post( "/api/v1/documents/create", json={ "format": "word", "content": "# Downloadable", "conversation_id": "conv-dl", }, ) doc_id = create_resp.json()["document"]["id"] # Download it resp = client.get(f"/api/v1/documents/download/{doc_id}") assert resp.status_code == 200 assert resp.headers["content-type"] == "application/octet-stream" assert len(resp.content) > 0 def test_download_not_found(client: TestClient) -> None: """GET /download/{nonexistent} returns 404.""" resp = client.get("/api/v1/documents/download/nonexistent-id") assert resp.status_code == 404 # --------------------------------------------------------------------------- # POST /upload-template # --------------------------------------------------------------------------- def test_upload_template(client: TestClient, tmp_path: Path) -> None: """POST /upload-template accepts a .docx file and returns stored_name.""" # Create a minimal .docx file from docx import Document template_path = tmp_path / "test_template.docx" doc = Document() doc.add_paragraph("Hello {{name}}!") doc.save(str(template_path)) with open(template_path, "rb") as f: resp = client.post( "/api/v1/documents/upload-template", files={"file": ("test_template.docx", f, "application/vnd.openxmlformats-officedocument.wordprocessingml.document")}, ) assert resp.status_code == 200 data = resp.json() assert data["success"] is True assert data["stored_name"].startswith("template-") assert data["stored_name"].endswith(".docx") def test_upload_template_wrong_format(client: TestClient) -> None: """POST /upload-template with non-.docx returns 400.""" resp = client.post( "/api/v1/documents/upload-template", files={"file": ("test.txt", b"not a docx", "text/plain")}, ) assert resp.status_code == 400 # --------------------------------------------------------------------------- # Service unavailable # --------------------------------------------------------------------------- def test_service_unavailable(tmp_path: Path) -> None: """When document_service is not on app.state, returns 503.""" app = FastAPI() # No document_service set app.include_router(documents_routes.router, prefix="/api/v1") client = TestClient(app) resp = client.post( "/api/v1/documents/create", json={ "format": "word", "content": "test", "conversation_id": "conv-1", }, ) assert resp.status_code == 503