251 lines
7.8 KiB
Python
251 lines
7.8 KiB
Python
"""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
|