Files
sales-trainer/backend/tests/test_upload_security.py

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)