feat(Ф4): pytest — 22 юнит-теста (contracts + garbage + upload + llm + repo)
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
"""Unit tests — no DB, no network, just pure logic."""
|
||||
import pytest
|
||||
from contracts import ParseResult, ClassifyResult, GroupingResult, CompareOp
|
||||
from services.upload import parse_multipart
|
||||
from services.classify import _is_garbage_by_filename, _is_garbage_by_header
|
||||
|
||||
|
||||
class TestContracts:
|
||||
"""Dataclass creation and validation."""
|
||||
|
||||
def test_parse_result(self):
|
||||
r = ParseResult(status="parsed", element_count=10, elements=[])
|
||||
assert r.status == "parsed"
|
||||
assert r.element_count == 10
|
||||
|
||||
def test_classify_from_llm(self):
|
||||
r = ClassifyResult.from_llm({
|
||||
"doc_type": "contract",
|
||||
"own_number": "03700_1",
|
||||
"doc_date": "2026-02-01",
|
||||
"counterparty": "ЗАО XXX001",
|
||||
})
|
||||
assert r.doc_type == "contract"
|
||||
assert r.own_number == "03700_1"
|
||||
assert r.counterparty == "ЗАО XXX001"
|
||||
|
||||
def test_grouping_result_empty(self):
|
||||
r = GroupingResult(contract_number="03700_1")
|
||||
assert r.contract_number == "03700_1"
|
||||
assert r.documents == []
|
||||
|
||||
def test_compare_op_add(self):
|
||||
op = CompareOp(action="ADD", new_row={"name": "Тест", "price": 100})
|
||||
assert op.action == "ADD"
|
||||
assert op.new_row["price"] == 100
|
||||
|
||||
def test_compare_op_from_llm(self):
|
||||
op = CompareOp.from_llm({
|
||||
"action": "UPDATE",
|
||||
"target_id": "r1",
|
||||
"new_values": {"price": 200},
|
||||
"comment": "change",
|
||||
})
|
||||
assert op.action == "UPDATE"
|
||||
assert op.new_values["price"] == 200
|
||||
assert op.comment == "change"
|
||||
|
||||
|
||||
class TestGarbageFilter:
|
||||
"""Stage 1-2 garbage detection."""
|
||||
|
||||
def test_filename_garbage_invoice(self):
|
||||
assert _is_garbage_by_filename("счет-фактура №123.docx") is True
|
||||
|
||||
def test_filename_garbage_act(self):
|
||||
assert _is_garbage_by_filename("Акт сверки за май.pdf") is True
|
||||
|
||||
def test_filename_not_garbage(self):
|
||||
assert _is_garbage_by_filename("договор-XXX001.docx") is False
|
||||
assert _is_garbage_by_filename("спецификация услуг.pdf") is False
|
||||
|
||||
def test_header_garbage(self):
|
||||
text = "СЧЕТ-ФАКТУРА № 123 от 01.01.2026\nПоставщик: ООО Ромашка"
|
||||
assert _is_garbage_by_header(text) is True
|
||||
|
||||
def test_header_not_garbage(self):
|
||||
text = "ДОГОВОР № 03700_1\nг. Москва\n\nИсполнитель обязуется..."
|
||||
assert _is_garbage_by_header(text) is False
|
||||
|
||||
|
||||
class TestParseMultipart:
|
||||
"""Pure multipart parsing without HTTP."""
|
||||
|
||||
def test_simple_file(self, sample_docx_bytes):
|
||||
boundary = b"----testboundary"
|
||||
body = (
|
||||
b"------testboundary\r\n"
|
||||
b'Content-Disposition: form-data; name="files"; filename="test.docx"\r\n'
|
||||
b"Content-Type: application/octet-stream\r\n\r\n"
|
||||
+ sample_docx_bytes +
|
||||
b"\r\n------testboundary--\r\n"
|
||||
)
|
||||
result = parse_multipart(body, f"multipart/form-data; boundary=----testboundary")
|
||||
assert result["filename"] == "test.docx"
|
||||
assert result["file_data"] == sample_docx_bytes
|
||||
|
||||
def test_with_contract_id(self, sample_docx_bytes):
|
||||
boundary = b"----testboundary"
|
||||
body = (
|
||||
b"------testboundary\r\n"
|
||||
b'Content-Disposition: form-data; name="files"; filename="test.docx"\r\n\r\n'
|
||||
+ sample_docx_bytes +
|
||||
b"\r\n------testboundary\r\n"
|
||||
b'Content-Disposition: form-data; name="contract_id"\r\n\r\n'
|
||||
b"550e8400-e29b-41d4-a716-446655440000"
|
||||
b"\r\n------testboundary--\r\n"
|
||||
)
|
||||
result = parse_multipart(body, f"multipart/form-data; boundary=----testboundary")
|
||||
assert result["contract_id"] == "550e8400-e29b-41d4-a716-446655440000"
|
||||
|
||||
def test_path_traversal_rejected(self):
|
||||
boundary = b"----testboundary"
|
||||
body = (
|
||||
b"------testboundary\r\n"
|
||||
b'Content-Disposition: form-data; name="files"; filename="../etc/passwd"\r\n\r\n'
|
||||
b"evil"
|
||||
b"\r\n------testboundary--\r\n"
|
||||
)
|
||||
with pytest.raises(ValueError, match="invalid filename"):
|
||||
parse_multipart(body, f"multipart/form-data; boundary=----testboundary")
|
||||
Reference in New Issue
Block a user