115 lines
3.8 KiB
Python
115 lines
3.8 KiB
Python
from __future__ import annotations
|
|
|
|
import xml.etree.ElementTree as ET
|
|
from datetime import date
|
|
from decimal import Decimal
|
|
from pathlib import Path
|
|
from unittest.mock import Mock
|
|
|
|
import pytest
|
|
from pydantic import SecretStr
|
|
|
|
from app.integrations.cbr import CbrClient, CbrDataError, parse_dynamic
|
|
from app.integrations.gateway import GatewayClient
|
|
|
|
|
|
def test_parse_dynamic_from_fixture(fixtures_dir: Path) -> None:
|
|
rates = parse_dynamic((fixtures_dir / "cbr_dynamic_R01375.xml").read_bytes())
|
|
|
|
assert [r.rate_date for r in rates] == [
|
|
date(2026, 10, 1),
|
|
date(2026, 10, 2),
|
|
date(2026, 10, 3),
|
|
date(2026, 10, 6),
|
|
date(2026, 10, 7),
|
|
date(2026, 10, 8),
|
|
]
|
|
last = rates[-1]
|
|
assert last.nominal == 1
|
|
assert last.value == Decimal("12.7516") # запятая -> точка
|
|
assert last.rate_per_unit == Decimal("12.7516")
|
|
assert all(isinstance(r.value, Decimal) for r in rates)
|
|
|
|
|
|
def test_parse_dynamic_matches_daily_fixture(fixtures_dir: Path) -> None:
|
|
"""Сверка двух реальных ответов ЦБ: курс CNY на 08.10 из XML_daily
|
|
(windows-1251, кириллица в Name) равен курсу из XML_dynamic."""
|
|
daily = ET.fromstring((fixtures_dir / "cbr_daily.xml").read_bytes())
|
|
cny = next(v for v in daily.iter("Valute") if v.get("ID") == "R01375")
|
|
rates = parse_dynamic((fixtures_dir / "cbr_dynamic_R01375.xml").read_bytes())
|
|
|
|
assert cny.findtext("CharCode") == "CNY"
|
|
assert cny.findtext("Name") == "Юань"
|
|
assert rates[-1].value == Decimal(cny.findtext("Value").replace(",", "."))
|
|
|
|
|
|
def test_parse_dynamic_divides_by_nominal_and_decodes_cp1251() -> None:
|
|
xml = (
|
|
'<?xml version="1.0" encoding="windows-1251"?>'
|
|
'<ValCurs ID="R01150" name="Динамика">'
|
|
'<Record Date="08.10.2026" Id="R01150"><Nominal>10000</Nominal>'
|
|
"<Value>33,3416</Value></Record></ValCurs>"
|
|
).encode("cp1251")
|
|
|
|
[rate] = parse_dynamic(xml)
|
|
|
|
assert rate.nominal == 10000
|
|
assert rate.value == Decimal("33.3416")
|
|
assert rate.rate_per_unit == Decimal("0.00333416")
|
|
|
|
|
|
def test_parse_dynamic_empty_period_returns_empty_list() -> None:
|
|
xml = b'<?xml version="1.0" encoding="windows-1251"?><ValCurs ID="R01375"/>'
|
|
|
|
assert parse_dynamic(xml) == []
|
|
|
|
|
|
def test_parse_dynamic_daily_format_has_no_records(fixtures_dir: Path) -> None:
|
|
assert parse_dynamic((fixtures_dir / "cbr_daily.xml").read_bytes()) == []
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"content",
|
|
[
|
|
b"Unauthorized",
|
|
b"<html><body>error</body></html>",
|
|
b'<ValCurs><Record Date="31.02.2026"><Nominal>1</Nominal>'
|
|
b"<Value>1,0</Value></Record></ValCurs>",
|
|
b'<ValCurs><Record Date="08.10.2026"><Nominal>1</Nominal>'
|
|
b"<Value>n/a</Value></Record></ValCurs>",
|
|
],
|
|
)
|
|
def test_parse_dynamic_bad_content_raises(content: bytes) -> None:
|
|
with pytest.raises(CbrDataError):
|
|
parse_dynamic(content)
|
|
|
|
|
|
def test_client_builds_cbr_query(fixtures_dir: Path) -> None:
|
|
session = Mock()
|
|
session.get.return_value = Mock(
|
|
status_code=200,
|
|
content=(fixtures_dir / "cbr_dynamic_R01375.xml").read_bytes(),
|
|
)
|
|
client = CbrClient(
|
|
GatewayClient(
|
|
source="cbr",
|
|
base_url="https://gateway.test",
|
|
path_prefix="/external/cbr",
|
|
api_key=SecretStr("cbr-key"),
|
|
session=session,
|
|
)
|
|
)
|
|
|
|
rates = client.get_rates(date(2026, 10, 1), date(2026, 10, 8))
|
|
|
|
assert len(rates) == 6
|
|
assert session.get.call_args.args[0] == (
|
|
"https://gateway.test/external/cbr/scripts/XML_dynamic.asp"
|
|
)
|
|
assert session.get.call_args.kwargs["params"] == {
|
|
"date_req1": "01/10/2026",
|
|
"date_req2": "08/10/2026",
|
|
"VAL_NM_RQ": "R01375",
|
|
}
|
|
assert session.get.call_args.kwargs["headers"] == {"X-Rshb-Api-Key": "cbr-key"}
|