Skip to content

Commit 67da983

Browse files
committed
fix: improvements for gaps
1 parent 3b8e8c3 commit 67da983

3 files changed

Lines changed: 54 additions & 20 deletions

File tree

.github/workflows/tests.yml

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,10 @@ jobs:
111111
run: python -m unittest discover tests -v
112112

113113
- name: Run pytest integration suite
114-
# Pytest fixtures (tests/conftest.py) build a temp workspaceStorage
115-
# and exercise the Flask routes via app.test_client(). Scoped to the
116-
# new endpoint file because `pytest tests/` would also re-collect the
117-
# 178 unittest.TestCase subclasses already run in the step above —
118-
# ~2× the CI minutes for zero extra signal.
119-
run: python -m pytest tests/test_api_endpoints.py -v --tb=short
114+
# Pytest fixtures (tests/conftest.py) build a temp workspaceStorage and
115+
# exercise Flask routes via app.test_client(). Only listed files — not
116+
# `pytest tests/` — to avoid re-collecting unittest.TestCase classes above.
117+
run: python -m pytest tests/test_api_endpoints.py tests/test_pdf_export.py -v --tb=short
120118

121119
# ── PyInstaller desktop build (Windows only, once per workflow) ────────
122120
# Closes #44. Builds the onedir bundle and smoke-tests --help so the

tests/conftest.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,15 @@ def workspace_storage() -> Generator[str, None, None]:
123123
os.environ["CLI_CHATS_PATH"] = prior_cli
124124

125125

126+
@pytest.fixture
127+
def pdf_client():
128+
"""Flask test client for routes that do not read workspace storage (e.g. PDF export)."""
129+
app = create_app()
130+
app.config["TESTING"] = True
131+
app.config["EXCLUSION_RULES"] = []
132+
return app.test_client()
133+
134+
126135
@pytest.fixture
127136
def client(workspace_storage: str):
128137
"""Flask test client bound to the temp workspace_storage fixture."""

tests/test_pdf_export.py

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
1-
"""Unit tests for POST /api/generate-pdf (api/pdf.py)."""
1+
"""Unit tests for POST /api/generate-pdf (api/pdf.py). Closes #72."""
22

33
from __future__ import annotations
44

5+
from typing import Any
56
from unittest.mock import patch
67

78
PDF_MAGIC = b"%PDF-"
89

910

10-
def _post_pdf(client, *, markdown: str = "", title: str = "Chat", json_data=None):
11+
def _post_pdf(
12+
client,
13+
*,
14+
markdown: str = "",
15+
title: str = "Chat",
16+
json_data: dict[str, Any] | None = None,
17+
):
1118
if json_data is not None:
1219
return client.post(
1320
"/api/generate-pdf",
@@ -32,7 +39,7 @@ def _assert_pdf_response(response) -> None:
3239

3340

3441
class TestGeneratePdfHappyPath:
35-
def test_normal_conversation_markdown(self, client):
42+
def test_normal_conversation_markdown(self, pdf_client):
3643
md = """# Chat export
3744
3845
## User question
@@ -49,50 +56,70 @@ def fact(n):
4956
5057
---
5158
"""
52-
response = _post_pdf(client, markdown=md, title="Happy conversation")
59+
response = _post_pdf(pdf_client, markdown=md, title="Happy conversation")
5360
_assert_pdf_response(response)
5461
assert (
5562
'attachment; filename="Happy conversation.pdf"'
5663
in response.headers.get("Content-Disposition", "")
5764
)
5865

66+
def test_empty_json_body_uses_defaults(self, pdf_client):
67+
response = _post_pdf(pdf_client, json_data={})
68+
_assert_pdf_response(response)
69+
assert (
70+
'attachment; filename="Chat.pdf"'
71+
in response.headers.get("Content-Disposition", "")
72+
)
73+
74+
def test_unsafe_title_characters_sanitized_in_filename(self, pdf_client):
75+
response = _post_pdf(
76+
pdf_client,
77+
markdown="Hello",
78+
title='bad<>:"/\\|?*name',
79+
)
80+
_assert_pdf_response(response)
81+
assert (
82+
'attachment; filename="bad_________name.pdf"'
83+
in response.headers.get("Content-Disposition", "")
84+
)
85+
5986

6087
class TestGeneratePdfEdgeCases:
61-
def test_empty_markdown(self, client):
62-
response = _post_pdf(client, markdown="", title="Empty chat")
88+
def test_empty_markdown(self, pdf_client):
89+
response = _post_pdf(pdf_client, markdown="", title="Empty chat")
6390
_assert_pdf_response(response)
6491

65-
def test_very_long_content(self, client):
92+
def test_very_long_content(self, pdf_client):
6693
line = "This is a repeated paragraph for length testing. " * 20
6794
md = "\n".join(f"Line {i}: {line}" for i in range(500))
68-
response = _post_pdf(client, markdown=md, title="Long chat")
95+
response = _post_pdf(pdf_client, markdown=md, title="Long chat")
6996
_assert_pdf_response(response)
7097

71-
def test_unicode_and_emoji_content(self, client):
98+
def test_unicode_and_emoji_content(self, pdf_client):
7299
md = (
73100
"Smart quotes: “hello” and ’world’\n"
74101
"Emoji: 🚀🔥 should not break PDF\n"
75102
"Bullet • point\n"
76103
)
77-
response = _post_pdf(client, markdown=md, title="Unicode chat")
104+
response = _post_pdf(pdf_client, markdown=md, title="Unicode chat")
78105
_assert_pdf_response(response)
79106

80107

81108
class TestGeneratePdfErrors:
82-
def test_pdf_engine_failure_returns_500(self, client):
109+
def test_pdf_engine_failure_returns_500(self, pdf_client):
83110
with patch(
84111
"fpdf.fpdf.FPDF.output",
85112
side_effect=RuntimeError("simulated failure"),
86113
):
87-
response = _post_pdf(client, markdown="Hello", title="Fail")
114+
response = _post_pdf(pdf_client, markdown="Hello", title="Fail")
88115
assert response.status_code == 500
89116
assert response.get_json() == {"error": "Failed to generate PDF"}
90117

91-
def test_invalid_export_payload_returns_500(self, client):
118+
def test_invalid_export_payload_returns_500(self, pdf_client):
92119
# Conversation IDs are resolved client-side (tabs API) before markdown is
93120
# POSTed here. A non-string markdown field mimics a corrupted export request.
94121
response = _post_pdf(
95-
client,
122+
pdf_client,
96123
json_data={"markdown": ["not", "a", "string"], "title": "Bad payload"},
97124
)
98125
assert response.status_code == 500

0 commit comments

Comments
 (0)