Skip to content

Commit 3b8e8c3

Browse files
committed
add: initial implementation of unit test for pdf export
1 parent adb75a4 commit 3b8e8c3

1 file changed

Lines changed: 99 additions & 0 deletions

File tree

tests/test_pdf_export.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""Unit tests for POST /api/generate-pdf (api/pdf.py)."""
2+
3+
from __future__ import annotations
4+
5+
from unittest.mock import patch
6+
7+
PDF_MAGIC = b"%PDF-"
8+
9+
10+
def _post_pdf(client, *, markdown: str = "", title: str = "Chat", json_data=None):
11+
if json_data is not None:
12+
return client.post(
13+
"/api/generate-pdf",
14+
json=json_data,
15+
content_type="application/json",
16+
)
17+
return client.post(
18+
"/api/generate-pdf",
19+
json={"markdown": markdown, "title": title},
20+
content_type="application/json",
21+
)
22+
23+
24+
def _assert_pdf_response(response) -> None:
25+
assert response.status_code == 200
26+
assert response.content_type.startswith("application/pdf")
27+
data = response.data
28+
assert len(data) > 0
29+
assert data.startswith(PDF_MAGIC)
30+
# Trailing %%EOF is a minimal structural check (see tests/web-ui-qa-checklist.md).
31+
assert b"%%EOF" in data[-1024:]
32+
33+
34+
class TestGeneratePdfHappyPath:
35+
def test_normal_conversation_markdown(self, client):
36+
md = """# Chat export
37+
38+
## User question
39+
40+
Please explain **recursion** in Python.
41+
42+
- Base case
43+
- Recursive step
44+
45+
```python
46+
def fact(n):
47+
return 1 if n < 2 else n * fact(n - 1)
48+
```
49+
50+
---
51+
"""
52+
response = _post_pdf(client, markdown=md, title="Happy conversation")
53+
_assert_pdf_response(response)
54+
assert (
55+
'attachment; filename="Happy conversation.pdf"'
56+
in response.headers.get("Content-Disposition", "")
57+
)
58+
59+
60+
class TestGeneratePdfEdgeCases:
61+
def test_empty_markdown(self, client):
62+
response = _post_pdf(client, markdown="", title="Empty chat")
63+
_assert_pdf_response(response)
64+
65+
def test_very_long_content(self, client):
66+
line = "This is a repeated paragraph for length testing. " * 20
67+
md = "\n".join(f"Line {i}: {line}" for i in range(500))
68+
response = _post_pdf(client, markdown=md, title="Long chat")
69+
_assert_pdf_response(response)
70+
71+
def test_unicode_and_emoji_content(self, client):
72+
md = (
73+
"Smart quotes: “hello” and ’world’\n"
74+
"Emoji: 🚀🔥 should not break PDF\n"
75+
"Bullet • point\n"
76+
)
77+
response = _post_pdf(client, markdown=md, title="Unicode chat")
78+
_assert_pdf_response(response)
79+
80+
81+
class TestGeneratePdfErrors:
82+
def test_pdf_engine_failure_returns_500(self, client):
83+
with patch(
84+
"fpdf.fpdf.FPDF.output",
85+
side_effect=RuntimeError("simulated failure"),
86+
):
87+
response = _post_pdf(client, markdown="Hello", title="Fail")
88+
assert response.status_code == 500
89+
assert response.get_json() == {"error": "Failed to generate PDF"}
90+
91+
def test_invalid_export_payload_returns_500(self, client):
92+
# Conversation IDs are resolved client-side (tabs API) before markdown is
93+
# POSTed here. A non-string markdown field mimics a corrupted export request.
94+
response = _post_pdf(
95+
client,
96+
json_data={"markdown": ["not", "a", "string"], "title": "Bad payload"},
97+
)
98+
assert response.status_code == 500
99+
assert response.get_json() == {"error": "Failed to generate PDF"}

0 commit comments

Comments
 (0)