"""Unit tests — no DB, no network, just pure logic.""" import pytest from contracts import ParseResult, ClassifyResult, GroupingResult, CompareOp from compare.upload import parse_multipart from compare.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")