test_report_export_service.py
7.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
from __future__ import annotations
from datetime import datetime
import pytest
from services.application.report import ReportExportService, ReportExportUnavailableError
from services.application.report import (
ReportArtifactNotFoundError,
ReportJobNotFoundError,
ReportJobNotReadyError,
)
from services.application.report.export_service import ReportExportValidationError
class _FakeTask:
def __init__(self) -> None:
self.markdown_file_path = ""
self.markdown_file_relative_path = ""
self.markdown_file_name = ""
self.persisted = False
def persist_snapshot(self) -> None:
self.persisted = True
class _FakeMarkdownRenderer:
def __init__(self, rendered: str = "# export") -> None:
self.rendered = rendered
self.calls: list[dict[str, object]] = []
def render(self, document_ir, *, ir_file_path):
self.calls.append({"document_ir": document_ir, "ir_file_path": ir_file_path})
return self.rendered
class _FakePDFRenderer:
def __init__(self, payload: bytes = b"%PDF-export") -> None:
self.payload = payload
self.calls: list[dict[str, object]] = []
def render_to_bytes(self, document_ir, *, optimize_layout):
self.calls.append({"document_ir": document_ir, "optimize_layout": optimize_layout})
return self.payload
def _build_service(
*,
export_source_builder=None,
markdown_renderer: _FakeMarkdownRenderer | None = None,
pdf_renderer: _FakePDFRenderer | None = None,
pdf_dependency_checker=None,
safe_filename_builder=None,
) -> ReportExportService:
return ReportExportService(
export_source_builder=export_source_builder or (lambda _task_id: {}),
markdown_renderer_factory=lambda: markdown_renderer or _FakeMarkdownRenderer(),
pdf_renderer_factory=lambda: pdf_renderer or _FakePDFRenderer(),
pdf_dependency_checker=pdf_dependency_checker or (lambda: (True, "ok")),
output_dir_getter=lambda: "/virtual/output",
safe_filename_builder=safe_filename_builder or (lambda text: text.replace(" ", "_")),
now_getter=lambda: datetime(2024, 1, 2, 3, 4, 5),
)
def test_export_markdown_writes_file_and_persists_task(monkeypatch):
task = _FakeTask()
source = {
"task": {"task_id": "report-export"},
"task_query": "museum report",
"task_record": task,
"ir_file_path": "/virtual/report.ir.json",
"document_ir": {"metadata": {"topic": "Museum Topic"}},
}
renderer = _FakeMarkdownRenderer()
writes: list[tuple[str, str, str | None]] = []
monkeypatch.setattr(
"services.application.report.export_service.Path.mkdir",
lambda self, parents=False, exist_ok=False: None,
)
monkeypatch.setattr(
"services.application.report.export_service.Path.write_text",
lambda self, content, encoding=None: writes.append((str(self), content, encoding)) or len(content),
)
monkeypatch.setattr(
"services.application.report.export_service.Path.resolve",
lambda self: self,
)
service = _build_service(
export_source_builder=lambda task_id: source if task_id == "report-export" else None,
markdown_renderer=renderer,
)
descriptor = service.export_markdown("report-export")
assert descriptor == {
"file_path": "\\virtual\\output\\report_Museum_Topic_20240102_030405.md",
"download_name": "report_Museum_Topic_20240102_030405.md",
"mimetype": "text/markdown",
}
assert writes == [(descriptor["file_path"], "# export", "utf-8")]
assert task.persisted is True
assert task.markdown_file_name == descriptor["download_name"]
assert task.markdown_file_path == descriptor["file_path"]
assert renderer.calls == [
{"document_ir": {"metadata": {"topic": "Museum Topic"}}, "ir_file_path": "/virtual/report.ir.json"}
]
def test_export_pdf_returns_response_descriptor():
source = {
"task": {"task_id": "report-export"},
"task_query": "museum report",
"task_record": _FakeTask(),
"ir_file_path": "/virtual/report.ir.json",
"document_ir": {"metadata": {"topic": "Museum Topic"}},
}
renderer = _FakePDFRenderer()
service = _build_service(
export_source_builder=lambda _task_id: source,
pdf_renderer=renderer,
)
descriptor = service.export_pdf("report-export", optimize_layout=False)
assert descriptor == {
"content_bytes": b"%PDF-export",
"download_name": "report_Museum_Topic_20240102_030405.pdf",
"mimetype": "application/pdf",
}
assert renderer.calls == [
{"document_ir": {"metadata": {"topic": "Museum Topic"}}, "optimize_layout": False}
]
def test_export_pdf_from_ir_payload_renders_pdf_and_normalizes_optimize():
renderer = _FakePDFRenderer()
service = _build_service(pdf_renderer=renderer)
descriptor = service.export_pdf_from_ir_payload(
{
"document_ir": {"metadata": {"topic": "Museum Topic"}},
"optimize": "false",
}
)
assert descriptor == {
"content_bytes": b"%PDF-export",
"download_name": "report_Museum_Topic_20240102_030405.pdf",
"mimetype": "application/pdf",
}
assert renderer.calls == [
{"document_ir": {"metadata": {"topic": "Museum Topic"}}, "optimize_layout": False}
]
def test_export_pdf_from_ir_payload_defaults_optimize_to_true():
renderer = _FakePDFRenderer()
service = _build_service(pdf_renderer=renderer)
descriptor = service.export_pdf_from_ir_payload({"document_ir": {"metadata": {"title": "Fallback Topic"}}})
assert descriptor["download_name"] == "report_Fallback_Topic_20240102_030405.pdf"
assert renderer.calls == [
{"document_ir": {"metadata": {"title": "Fallback Topic"}}, "optimize_layout": True}
]
@pytest.mark.parametrize(
("payload", "message"),
[
(["not", "an", "object"], "请求体必须是JSON对象"),
({}, "缺少document_ir参数"),
({"document_ir": {}, "optimize": "sometimes"}, "optimize参数必须是布尔值"),
],
)
def test_export_pdf_from_ir_payload_validates_request_payload(payload, message):
service = _build_service()
with pytest.raises(ReportExportValidationError, match=message):
service.export_pdf_from_ir_payload(payload)
def test_export_pdf_from_ir_payload_raises_unavailable_when_dependency_missing():
service = _build_service(pdf_dependency_checker=lambda: (False, "missing pango"))
with pytest.raises(ReportExportUnavailableError, match="PDF 导出功能不可用") as exc_info:
service.export_pdf_from_ir_payload({"document_ir": {}})
assert exc_info.value.details == {
"details": '请查看根目录 README.md "源码启动"的第二步(PDF 导出依赖)了解安装方法',
"help_url": "https://github.com/666ghj/BettaFish#2-安装-pdf-导出所需系统依赖可选",
"system_message": "missing pango",
}
@pytest.mark.parametrize(
"exc_type",
[ReportJobNotFoundError, ReportJobNotReadyError, ReportArtifactNotFoundError],
)
def test_export_service_propagates_query_side_errors(exc_type):
service = _build_service(
export_source_builder=lambda _task_id: (_ for _ in ()).throw(exc_type()),
)
with pytest.raises(exc_type):
service.export_markdown("report-export")