Files
sales-trainer/backend/tests/test_upload_security.py
Macky 9fd748154d feat: UX/SAAS 12-point redesign
- auth: self-registration (role=user); first-created-user becomes super_admin
- roles: super_admin may promote others; regular admin cannot see super_admin accounts
- products: user-created private groups; admin 'สินค้าขององค์กร' (shared) with hidden/public; users can create groups
- analytics: team + per-user weak areas, close-rate-by-difficulty buckets, 30-day default, weekly trend, trainee table, active users; dashboard redesigned
- files: docx + xlsx upload support (python-docx + openpyxl)
- ui: tabs การฝึก→ผลการฝึก→ภาพรวม; admin lands on ภาพรวม / user on การฝึก; guide in topbar
- consolidate: weak-areas merged into Results (10/page), my-personas merged into Training
- copy: บุคคลต้นแบบ→persona everywhere; clearer add-product form (A/B/C, upload-or-fill)
- report: remove ดูรายงาน UI entry (endpoint kept)

Backend 348 tests pass; frontend build + vitest clean.
2026-08-21 12:28:22 +07:00

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.exe"
source.write_bytes(b"not supported")
with pytest.raises(ParseError, match="unsupported document type"):
parse_document(source)