DRRK/tests/unit/test_cbr.py

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"}