DRRK/tests/unit/test_services_sqlite.py

154 lines
5.5 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.

"""Сервисы + настоящие репозитории на временном SQLite (схема — миграцией
Alembic, с сидами). Внешние API — фейки на реальных фикстурах MOEX/ЦБ."""
from __future__ import annotations
from dataclasses import replace
from datetime import date
from decimal import Decimal
from pathlib import Path
from types import SimpleNamespace
import pytest
from sqlalchemy import func, select
from app.db.base import run_db
from app.db.models import ExchangeRate, ExternalRequestLog, MarketData
from app.integrations.cbr import parse_dynamic
from app.services import fx_rates, market_data, securities
from tests.unit.fakes import FakeCbr, FakeMoex
ON_DATE = date(2026, 10, 8)
ISIN = "RU000A106169"
def _count(session_local, model) -> int:
async def _q() -> int:
async with session_local() as session:
return (
await session.execute(select(func.count()).select_from(model))
).scalar_one()
return run_db(_q())
def test_full_flow_security_quotes_and_rates(
session_local, fixture_moex: FakeMoex, fixtures_dir: Path
) -> None:
added = run_db(securities.add_by_isin(ISIN, "analyst@rshb.ru", moex=fixture_moex))
rows = run_db(securities.list_securities())
assert added.security_id == 1
assert [(r.isin, r.name, r.currency, r.coupon_frequency) for r in rows] == [
(ISIN, "РЭО-02", "RUB", 4)
]
assert rows[0].initial_face_value == Decimal("1000")
result = run_db(market_data.update(ON_DATE, "system", moex=fixture_moex))
assert (result.updated, result.no_trades, result.errors) == (1, 0, 0)
[quote_row] = run_db(market_data.list_for_date(ON_DATE))
assert quote_row.board == "TQCB"
assert quote_row.clean_price == Decimal("101.42")
assert quote_row.yield_value == Decimal("17.78")
assert quote_row.face_value == Decimal("645.19")
assert quote_row.num_trades == 3
cbr = FakeCbr(parse_dynamic((fixtures_dir / "cbr_dynamic_R01375.xml").read_bytes()))
fx_result = run_db(fx_rates.update(ON_DATE, "system", cbr=cbr))
assert fx_result.saved == 6
recent = run_db(fx_rates.list_recent(limit=3))
assert [r.rate_date for r in recent] == [
date(2026, 10, 8),
date(2026, 10, 7),
date(2026, 10, 6),
]
assert recent[0].rate_per_unit == Decimal("12.7516")
again = run_db(fx_rates.update(ON_DATE, "system", cbr=cbr))
assert again.saved == 0 and len(cbr.calls) == 1
# 2 запроса при добавлении + 1 история + 1 ЦБ
assert _count(session_local, ExternalRequestLog) == 4
def test_market_data_reload_is_upsert(session_local, fixture_moex: FakeMoex) -> None:
run_db(securities.add_by_isin(ISIN, "analyst@rshb.ru", moex=fixture_moex))
run_db(market_data.update(ON_DATE, "analyst@rshb.ru", moex=fixture_moex))
first_quote = fixture_moex._history[ISIN]
fixture_moex._history[ISIN] = replace(first_quote, clean_price=Decimal("100.5"))
result = run_db(market_data.update(ON_DATE, "system", moex=fixture_moex))
async def _rows():
async with session_local() as session:
return (await session.execute(select(MarketData))).scalars().all()
[row] = run_db(_rows())
assert result.updated == 1
assert row.clean_price == Decimal("100.5")
assert row.created_by == "analyst@rshb.ru"
assert row.updated_by == "system"
def test_exchange_rate_reload_is_upsert(session_local, fixtures_dir: Path) -> None:
rates = parse_dynamic((fixtures_dir / "cbr_dynamic_R01375.xml").read_bytes())
run_db(fx_rates.update(date(2026, 10, 7), "system", cbr=FakeCbr(rates)))
# Тот же день повторно (например, после ручной правки последней даты).
async def _drop_last() -> None:
async with session_local() as session:
row = (
await session.execute(
select(ExchangeRate).where(
ExchangeRate.rate_date == date(2026, 10, 7)
)
)
).scalar_one()
await session.delete(row)
await session.commit()
run_db(_drop_last())
run_db(fx_rates.update(ON_DATE, "system", cbr=FakeCbr(rates)))
assert _count(session_local, ExchangeRate) == 6
def test_duplicate_isin_is_rejected_by_service(
session_local, fixture_moex: FakeMoex
) -> None:
run_db(securities.add_by_isin(ISIN, "analyst@rshb.ru", moex=fixture_moex))
with pytest.raises(securities.SecurityServiceError, match="уже добавлена"):
run_db(securities.add_by_isin(ISIN, "analyst@rshb.ru", moex=fixture_moex))
def test_request_log_saves_gravitee_transaction_id(session_local) -> None:
from app.integrations.gateway import RequestRecord
from app.services import request_log
record = RequestRecord(
source="moex",
endpoint="/external/moex-iss/iss/securities.json",
params={"q": ISIN},
http_status=200,
duration_ms=12,
ok=True,
error=None,
transaction_id="0b5c7d3e-tx",
)
gateway = SimpleNamespace(drain_records=lambda: [record])
run_db(request_log.save_request_log(gateway, "system"))
async def _saved() -> list[str | None]:
async with session_local() as session:
return list(
(
await session.execute(
select(ExternalRequestLog.gateway_transaction_id)
)
).scalars()
)
assert run_db(_saved()) == ["0b5c7d3e-tx"]