139 lines
3.9 KiB
Python
139 lines
3.9 KiB
Python
"""Sprint 4.7 upload/parser hardening contract tests."""
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
from app.config import Config
|
|
from app.services.file_parser import ParseError, parse_document, parse_pdf, parse_text
|
|
|
|
|
|
def test_text_parser_uses_charset_normalizer_fallback(tmp_path, monkeypatch):
|
|
source = tmp_path / "brief.txt"
|
|
source.write_bytes(b"legacy bytes")
|
|
calls = []
|
|
|
|
class BestMatch:
|
|
encoding = "latin-1"
|
|
|
|
def from_bytes(raw):
|
|
calls.append(raw)
|
|
return SimpleNamespace(best=lambda: BestMatch())
|
|
|
|
monkeypatch.setitem(sys.modules, "charset_normalizer", SimpleNamespace(from_bytes=from_bytes))
|
|
monkeypatch.setattr(Config, "UPLOAD_TEXT_MAX_BYTES", 1024)
|
|
|
|
assert parse_text(source) == "legacy bytes"
|
|
assert calls == [] # UTF-8 succeeds without invoking detection.
|
|
|
|
legacy = "café".encode("latin-1")
|
|
source.write_bytes(legacy)
|
|
assert parse_text(source) == "café"
|
|
assert calls == [legacy]
|
|
|
|
|
|
def test_text_parser_rejects_oversized_document(tmp_path, monkeypatch):
|
|
source = tmp_path / "brief.txt"
|
|
source.write_bytes(b"x" * 11)
|
|
monkeypatch.setattr(Config, "UPLOAD_TEXT_MAX_BYTES", 10)
|
|
|
|
with pytest.raises(ParseError, match="text document too large"):
|
|
parse_text(source)
|
|
|
|
|
|
def test_text_parser_preserves_thai_utf8(tmp_path):
|
|
source = tmp_path / "thai.txt"
|
|
source.write_text("ระบบฝึกทักษะการขาย", encoding="utf-8")
|
|
|
|
assert parse_text(source) == "ระบบฝึกทักษะการขาย"
|
|
|
|
|
|
def test_pdf_parser_rejects_page_bomb(monkeypatch, tmp_path):
|
|
class FakeDocument:
|
|
page_count = 3
|
|
|
|
def __iter__(self):
|
|
return iter(())
|
|
|
|
def close(self):
|
|
pass
|
|
|
|
monkeypatch.setitem(sys.modules, "fitz", SimpleNamespace(open=lambda _path: FakeDocument()))
|
|
monkeypatch.setattr(Config, "UPLOAD_MAX_PDF_PAGES", 2)
|
|
|
|
with pytest.raises(ParseError, match="too many pages"):
|
|
parse_pdf(tmp_path / "bomb.pdf")
|
|
|
|
|
|
def test_malformed_pdf_returns_controlled_parse_error(tmp_path):
|
|
pytest.importorskip("fitz")
|
|
source = tmp_path / "malformed.pdf"
|
|
source.write_bytes(b"not a PDF")
|
|
|
|
with pytest.raises(ParseError, match="cannot open PDF"):
|
|
parse_pdf(source)
|
|
|
|
|
|
def test_pdf_parser_rejects_excessive_extracted_text(monkeypatch, tmp_path):
|
|
class FakePage:
|
|
rect = SimpleNamespace(x0=0, y0=0, x1=10, y1=100)
|
|
|
|
def get_text(self, *args, **kwargs):
|
|
return "0123456789"
|
|
|
|
class FakeDocument:
|
|
page_count = 1
|
|
|
|
def __iter__(self):
|
|
return iter((FakePage(),))
|
|
|
|
def close(self):
|
|
pass
|
|
|
|
monkeypatch.setitem(
|
|
sys.modules,
|
|
"fitz",
|
|
SimpleNamespace(open=lambda _path: FakeDocument(), Rect=lambda *args: args),
|
|
)
|
|
monkeypatch.setattr(Config, "UPLOAD_MAX_EXTRACTED_CHARS", 5)
|
|
|
|
with pytest.raises(ParseError, match="extracted text too large"):
|
|
parse_pdf(tmp_path / "large.pdf")
|
|
|
|
|
|
def test_pdf_parser_rejects_page_without_usable_geometry(monkeypatch, tmp_path):
|
|
calls = []
|
|
|
|
class FakePage:
|
|
rect = None
|
|
|
|
def get_text(self, *args, **kwargs):
|
|
calls.append((args, kwargs))
|
|
return "must not be materialized"
|
|
|
|
class FakeDocument:
|
|
page_count = 1
|
|
|
|
def __iter__(self):
|
|
return iter((FakePage(),))
|
|
|
|
def close(self):
|
|
pass
|
|
|
|
monkeypatch.setitem(sys.modules, "fitz", SimpleNamespace(open=lambda _path: FakeDocument()))
|
|
|
|
with pytest.raises(ParseError, match="page geometry unavailable"):
|
|
parse_pdf(tmp_path / "no-geometry.pdf")
|
|
assert calls == []
|
|
|
|
|
|
def test_parser_rejects_unsupported_extension(tmp_path):
|
|
source = tmp_path / "brief.docx"
|
|
source.write_bytes(b"not supported")
|
|
|
|
with pytest.raises(ParseError, match="unsupported document type"):
|
|
parse_document(source)
|