fix: close independent verification findings on document parsing
Zip construction and member reads now share one narrow boundary so corrupt BZIP2, LZMA and version fields cannot leak native exceptions. Non-success statuses are read before classification, text-bearing blocks require lines, the size cap and reasoning contract are pinned by tests that fail under mutation, and the per-page timing claim is corrected to the observed range.
This commit is contained in:
@@ -704,27 +704,83 @@ class TestParseDocument:
|
||||
document=_PDF_BODY, source=_source(), call_id="c1"
|
||||
)
|
||||
|
||||
async def test_oversized_declared_content_length_is_rejected(self, monkeypatch):
|
||||
monkeypatch.setattr(monkey_ocr, "_MAX_PACKAGE_BYTES", 100)
|
||||
resp = httpx.Response(200, content=b"x" * 10, headers={"content-length": "101"})
|
||||
with pytest.raises(ResultInvalidError):
|
||||
@staticmethod
|
||||
def _oversized_valid_package():
|
||||
"""**合法**的 ZIP 但整体超过上限: 去掉上限检查时它能被正常解析,用例才会转红。
|
||||
|
||||
(独立验证发现: 旧用例用非 ZIP 垃圾字节,去掉上限后同样抛 ResultInvalidError——
|
||||
走的是 BadZipFile 路径,上限从未被测到。)
|
||||
"""
|
||||
return _doc_package(
|
||||
[_doc_page([_text_block("SYNTH-A")])], extra=[("images/big.jpg", b"x" * 5000)]
|
||||
)
|
||||
|
||||
async def _assert_cap_rejects_before_parsing(self, monkeypatch, resp):
|
||||
monkeypatch.setattr(monkey_ocr, "_MAX_PACKAGE_BYTES", 2000)
|
||||
parsed = []
|
||||
|
||||
def spy(package):
|
||||
parsed.append(len(package))
|
||||
return [], [(1.0, 1.0)], ""
|
||||
|
||||
monkeypatch.setattr(monkey_ocr, "parse_document_package", spy)
|
||||
with pytest.raises(ResultInvalidError, match="体积超过上限"):
|
||||
await self._transport(zip_resp=resp).parse_document(
|
||||
document=_PDF_BODY, source=_source(), call_id="c1"
|
||||
)
|
||||
assert parsed == [] # 解析函数根本没被调用
|
||||
|
||||
async def test_oversized_streamed_body_is_rejected_by_cumulative_count(self, monkeypatch):
|
||||
"""没有 Content-Length(分块传输)时靠累计字节兜底: 多个小块合起来超限。"""
|
||||
monkeypatch.setattr(monkey_ocr, "_MAX_PACKAGE_BYTES", 100)
|
||||
async def test_oversized_valid_package_is_rejected_by_declared_content_length(
|
||||
self, monkeypatch
|
||||
):
|
||||
resp = httpx.Response(200, content=self._oversized_valid_package()) # 自动带 Content-Length
|
||||
await self._assert_cap_rejects_before_parsing(monkeypatch, resp)
|
||||
|
||||
async def test_oversized_valid_package_is_rejected_by_cumulative_count(self, monkeypatch):
|
||||
"""没有 Content-Length(分块传输)时靠累计字节兜底。"""
|
||||
package = self._oversized_valid_package()
|
||||
|
||||
async def chunks():
|
||||
for _ in range(3):
|
||||
yield b"x" * 60
|
||||
for offset in range(0, len(package), 700):
|
||||
yield package[offset : offset + 700]
|
||||
|
||||
resp = httpx.Response(200, content=chunks())
|
||||
with pytest.raises(ResultInvalidError):
|
||||
await self._transport(zip_resp=resp).parse_document(
|
||||
document=_PDF_BODY, source=_source(), call_id="c1"
|
||||
)
|
||||
await self._assert_cap_rejects_before_parsing(
|
||||
monkeypatch, httpx.Response(200, content=chunks())
|
||||
)
|
||||
|
||||
async def test_package_under_the_cap_still_parses(self, monkeypatch):
|
||||
"""对照: 上限只拦超限的,不误伤正常包。"""
|
||||
monkeypatch.setattr(monkey_ocr, "_MAX_PACKAGE_BYTES", 10_000)
|
||||
result = await self._transport(
|
||||
zip_resp=httpx.Response(200, content=self._oversized_valid_package())
|
||||
).parse_document(document=_PDF_BODY, source=_source(), call_id="c1")
|
||||
assert [e.text for e in result.elements] == ["SYNTH-A"]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("status", "exc_type"),
|
||||
[
|
||||
(401, SourceDeadError),
|
||||
(404, RequestRejectedError),
|
||||
(500, TransientError),
|
||||
(302, RequestRejectedError), # 3xx 不是 `is_error`,但 raise_for_status 会抛
|
||||
],
|
||||
)
|
||||
async def test_streamed_error_body_is_read_before_classification(self, status, exc_type):
|
||||
"""真正的流式响应(异步迭代器 body)不会被预缓冲: 不先 `aread()`,摘要读到的是空串。
|
||||
|
||||
(独立验证发现: `httpx.Response(content=bytes)` 已预缓冲,删掉 `aread()` 后旧用例仍绿;
|
||||
且旧条件 `is_error` 漏掉 3xx。)
|
||||
"""
|
||||
body = '{"detail": "synthetic upstream reason"}'
|
||||
|
||||
async def streamed():
|
||||
yield body.encode()
|
||||
|
||||
t = self._transport(zip_resp=httpx.Response(status, content=streamed()))
|
||||
with pytest.raises(exc_type) as ei:
|
||||
await t.parse_document(document=_PDF_BODY, source=_source(), call_id="c1")
|
||||
assert ei.value.status_code == status
|
||||
assert ei.value.body_text == body
|
||||
|
||||
async def test_bad_package_is_result_invalid(self):
|
||||
t = self._transport(zip_resp=httpx.Response(200, content=b"not a zip"))
|
||||
|
||||
Reference in New Issue
Block a user