SFM/tests/unit/test_parser.py
Raykov-MS 9968d84e3b fix
2026-08-11 01:09:07 +03:00

227 lines
9.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import xml.etree.ElementTree as ET
from app.pipeline.parser import _extract_direct_fields, parse_xml_content
def _base_xml(operations: str) -> str:
return f"""<?xml version="1.0" encoding="UTF-8"?>
<СообщОперКО xmlns="urn:test">
<СлужЧасть><ИдФайл>ID1</ИдФайл></СлужЧасть>
<ИнформЧасть>
<ИнфБанк><БИК>123</БИК></ИнфБанк>
<СведКО>
{operations}
</СведКО>
</ИнформЧасть>
</СообщОперКО>
"""
def test_parse_xml_with_invalid_name_returns_fatal() -> None:
result = parse_xml_content("broken.xml", "<СообщОперКО/>")
assert result.is_fatal
assert "Некорректное имя XML-файла" in (result.fatal_error or "")
def test_parse_xml_with_malformed_xml_returns_fatal() -> None:
result = parse_xml_content("SKO115FZ_01_123456789_20260616_X00001.xml", "<broken")
assert result.is_fatal
assert "Ошибка XML" in (result.fatal_error or "")
def test_parse_xml_without_inform_part_returns_fatal() -> None:
xml = """<?xml version="1.0" encoding="UTF-8"?><СообщОперКО></СообщОперКО>"""
result = parse_xml_content("SKO115FZ_01_123456789_20260616_X00001.xml", xml)
assert result.is_fatal
assert "ИнформЧасть" in (result.fatal_error or "")
def test_parse_xml_without_participants_creates_single_row() -> None:
xml = _base_xml(
"<Операция><ИдентификаторЗаписи>OP1</ИдентификаторЗаписи><СуммаОпер>100</СуммаОпер></Операция>"
)
result = parse_xml_content("SKO115FZ_01_123456789_20260616_X00001.xml", xml)
assert not result.is_fatal
assert len(result.rows) == 1
assert result.rows[0].record_id == "OP1"
def test_parse_xml_with_namespace_and_two_participants() -> None:
xml = _base_xml(
"""
<Операция>
<ИдентификаторЗаписи>OP2</ИдентификаторЗаписи>
<УчастникОп><Тип>01</Тип></УчастникОп>
<УчастникОп><Тип>02</Тип></УчастникОп>
</Операция>
"""
)
result = parse_xml_content("SKO115FZ_01_123456789_20260616_X00001.xml", xml)
assert not result.is_fatal
assert len(result.rows) == 2
assert result.rows[0].participant_fields["Тип"] == "01"
assert result.rows[1].participant_fields["Тип"] == "02"
def test_parse_xml_operation_error_marks_partial(monkeypatch) -> None:
xml = _base_xml(
"<Операция><ИдентификаторЗаписи>OP3</ИдентификаторЗаписи><УчастникОп><Тип>01</Тип></УчастникОп></Операция>"
)
from app.pipeline import parser
original = parser._extract_direct_fields
def _boom(element, excluded_tags=None): # noqa: ANN001
tag_name = parser._normalize_tag(element.tag)
if tag_name == "УчастникОп":
raise ValueError("bad participant")
return original(element, excluded_tags=excluded_tags)
monkeypatch.setattr(parser, "_extract_direct_fields", _boom)
result = parse_xml_content("SKO115FZ_01_123456789_20260616_X00001.xml", xml)
assert not result.is_fatal
assert result.operation_errors
assert result.operation_errors[0].record_id == "OP3"
def test_parse_xml_with_numeric_suffix_file_name_is_valid() -> None:
xml = _base_xml(
"<Операция><ИдентификаторЗаписи>OP4</ИдентификаторЗаписи><СуммаОпер>100</СуммаОпер></Операция>"
)
result = parse_xml_content("SKO115FZ_01_044525111_20251224_000051.xml", xml)
assert not result.is_fatal
assert len(result.rows) == 1
def test_parse_xml_collects_legal_entity_registration_address_attributes() -> None:
xml = _base_xml(
"""
<Операция>
<ИдентификаторЗаписи>OP5</ИдентификаторЗаписи>
<УчастникОп>
<УчастникЮЛ>
<СведЮЛ>
<АдрРегЮЛ Индекс="675520" КодОКСМ="643" КодСубъектаПоОКАТО="10"
Район="Благовещенский р" Пункт="Чигири с"
Улица="Зеленая ул" Дом="1" />
</СведЮЛ>
</УчастникЮЛ>
</УчастникОп>
</Операция>
"""
)
result = parse_xml_content("SKO115FZ_01_123456789_20260616_X00001.xml", xml)
assert not result.is_fatal
assert len(result.rows) == 1
participant_fields = result.rows[0].participant_fields
assert participant_fields["УчастникЮЛ.СведЮЛ.АдрРегЮЛ.Индекс"] == "675520"
assert participant_fields["УчастникЮЛ.СведЮЛ.АдрРегЮЛ.КодОКСМ"] == "643"
assert participant_fields["УчастникЮЛ.СведЮЛ.АдрРегЮЛ.КодСубъектаПоОКАТО"] == "10"
assert (
participant_fields["УчастникЮЛ.СведЮЛ.АдрРегЮЛ.Район"] == "Благовещенский р"
)
def test_extract_direct_fields_indexes_repeated_siblings_at_every_level() -> None:
participant = ET.fromstring(
"""
<УчастникОп>
<Документ><НомДок>DOC-1</НомДок></Документ>
<Документ><НомДок>DOC-2</НомДок></Документ>
<УчастникЮЛ>
<СведЕИО><ФЛЕИО><ИННФЛИП>111111111111</ИННФЛИП></ФЛЕИО></СведЕИО>
<СведЕИО><ФЛЕИО><ИННФЛИП>222222222222</ИННФЛИП></ФЛЕИО></СведЕИО>
</УчастникЮЛ>
</УчастникОп>
"""
)
fields = _extract_direct_fields(participant)
assert fields["Документ[0].НомДок"] == "DOC-1"
assert fields["Документ[1].НомДок"] == "DOC-2"
assert fields["УчастникЮЛ.СведЕИО[0].ФЛЕИО.ИННФЛИП"] == "111111111111"
assert fields["УчастникЮЛ.СведЕИО[1].ФЛЕИО.ИННФЛИП"] == "222222222222"
assert all("; " not in value for value in fields.values())
def test_extract_direct_fields_keeps_single_sibling_path_unindexed() -> None:
participant = ET.fromstring(
"<УчастникОп><Документ><НомДок>DOC-1</НомДок></Документ></УчастникОп>"
)
fields = _extract_direct_fields(participant)
assert fields == {"Документ.НомДок": "DOC-1"}
def test_parse_xml_expands_documents_and_eio_into_additive_rows() -> None:
xml = _base_xml(
"""
<Операция>
<ИдентификаторЗаписи>OP6</ИдентификаторЗаписи>
<УчастникОп>
<ТипУчастника>1</ТипУчастника>
<Документ><НомДок>DOC-1</НомДок></Документ>
<Документ><НомДок>DOC-2</НомДок></Документ>
<Документ><НомДок>DOC-3</НомДок></Документ>
<УчастникЮЛ>
<СведЕИО><ФЛЕИО><ИННФЛИП>111111111111</ИННФЛИП></ФЛЕИО></СведЕИО>
<СведЕИО><ЮЛЕИО><ИННЮЛ>2222222222</ИННЮЛ></ЮЛЕИО></СведЕИО>
</УчастникЮЛ>
</УчастникОп>
</Операция>
"""
)
result = parse_xml_content("SKO115FZ_01_123456789_20260616_X00001.xml", xml)
assert not result.is_fatal
assert len(result.rows) == 4
base_row, second_document, third_document, second_eio = result.rows
assert base_row.participant_fields["ТипУчастника"] == "1"
assert base_row.participant_fields["Документ.НомДок"] == "DOC-1"
assert (
base_row.participant_fields["УчастникЮЛ.СведЕИО.ФЛЕИО.ИННФЛИП"]
== "111111111111"
)
assert second_document.operation_fields == {}
assert second_document.is_continuation
assert second_document.participant_fields == {"Документ.НомДок": "DOC-2"}
assert third_document.participant_fields == {"Документ.НомДок": "DOC-3"}
assert second_eio.participant_fields == {
"УчастникЮЛ.СведЕИО.ЮЛЕИО.ИННЮЛ": "2222222222"
}
def test_parse_xml_separates_single_eio_and_single_beneficiary() -> None:
xml = _base_xml(
"""
<Операция>
<ИдентификаторЗаписи>OP7</ИдентификаторЗаписи>
<УчастникОп>
<ТипУчастника>1</ТипУчастника>
<УчастникЮЛ>
<СведЕИО><ЮЛЕИО><ИННЮЛ>2222222222</ИННЮЛ></ЮЛЕИО></СведЕИО>
<БенефициарЮЛ>
<ФЛБенефициар><ИННФЛИП>111111111111</ИННФЛИП></ФЛБенефициар>
</БенефициарЮЛ>
</УчастникЮЛ>
</УчастникОп>
</Операция>
"""
)
result = parse_xml_content("SKO115FZ_01_123456789_20260616_X00001.xml", xml)
assert len(result.rows) == 2
assert (
result.rows[0].participant_fields["УчастникЮЛ.СведЕИО.ЮЛЕИО.ИННЮЛ"]
== "2222222222"
)
assert result.rows[1].participant_fields == {
"УчастникЮЛ.БенефициарЮЛ.ФЛБенефициар.ИННФЛИП": "111111111111"
}