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 = ( '' '' '10000' "33,3416" ).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'' 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"error", b'1' b"1,0", b'1' b"n/a", ], ) 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"}