gendesign/backend/tests/workers/test_cbr_macro_sync.py
Light1YT c78c6ec2d0
All checks were successful
Deploy / build-frontend (push) Has been skipped
Deploy / deploy (push) Successful in 1m10s
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Successful in 1m24s
Deploy / build-worker (push) Successful in 2m53s
feat(site-finder): live CBR key-rate scraper → macro_indicator (#945 PR B) (#964)
2026-06-02 19:05:57 +00:00

137 lines
5.1 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.

"""Тесты Celery-таски cbr_macro_sync (mock session + mock fetch, без живой сети).
Покрывают: контракт upsert-SQL (indicator_type='key_rate', region='rf',
source='cbr', ON CONFLICT, CAST not ::), счётчики fetched/upserted, проброс
ошибки fetch'а.
"""
from __future__ import annotations
from datetime import date
from decimal import Decimal
from typing import Any
from unittest.mock import MagicMock, patch
def _make_mock_db() -> tuple[MagicMock, list[dict[str, Any]]]:
"""Mock DB session с поддержкой db.begin_nested() context manager.
Возвращает (db, captured_params) — captured содержит params каждого
db.execute(..., params).
"""
captured: list[dict[str, Any]] = []
def execute_router(stmt: Any, params: Any = None) -> MagicMock:
if params is not None:
captured.append(params)
return MagicMock()
db = MagicMock()
db.execute = execute_router
# begin_nested() → context manager (SAVEPOINT)
db.begin_nested.return_value.__enter__ = MagicMock(return_value=None)
db.begin_nested.return_value.__exit__ = MagicMock(return_value=False)
return db, captured
def test_upsert_sql_contract() -> None:
"""Smoke: UPSERT_KEY_RATE_SQL содержит обязательные литералы контракта
macro_indicator и НЕ использует :x::type (psycopg v3)."""
from app.workers.tasks.cbr_macro_sync import UPSERT_KEY_RATE_SQL
sql = str(UPSERT_KEY_RATE_SQL)
upper = sql.upper()
assert "INTO MACRO_INDICATOR" in upper
# литералы контракта — в SQL они lowercase
assert "'key_rate'" in sql
assert "'rf'" in sql
assert "'cbr'" in sql
assert "'daily'" in sql
# ON CONFLICT по полному PK + обновление value/updated_at
assert "ON CONFLICT (INDICATOR_TYPE, REGION, OBS_DATE) DO UPDATE" in upper
assert "VALUE = EXCLUDED.VALUE" in upper
assert "UPDATED_AT = NOW()" in upper
# psycopg v3: CAST(:x AS type), НИКОГДА :x::type
assert "CAST(:D AS DATE)" in upper
assert "CAST(:V AS NUMERIC)" in upper
assert "::" not in sql
def test_task_upserts_each_row_with_correct_params() -> None:
"""Каждая (date, rate) → один execute с bind-параметрами :d и :v."""
from app.workers.tasks import cbr_macro_sync as task_mod
db, captured = _make_mock_db()
fake_rows = [
(date(2023, 8, 14), Decimal("8.5")),
(date(2023, 8, 15), Decimal("12.0")),
]
with (
patch.object(task_mod, "SessionLocal", return_value=db),
patch.object(task_mod, "fetch_key_rate", return_value=fake_rows) as mock_fetch,
):
result = task_mod.cbr_macro_sync.run()
mock_fetch.assert_called_once()
assert result["fetched"] == 2
assert result["upserted"] == 2
# параметры каждого upsert
assert captured == [
{"d": date(2023, 8, 14), "v": Decimal("8.5")},
{"d": date(2023, 8, 15), "v": Decimal("12.0")},
]
db.commit.assert_called()
db.close.assert_called_once()
def test_task_passes_date_range_to_fetch() -> None:
"""from_date / to_date (ISO-строки) парсятся в date и прокидываются в fetch."""
from app.workers.tasks import cbr_macro_sync as task_mod
db, _ = _make_mock_db()
with (
patch.object(task_mod, "SessionLocal", return_value=db),
patch.object(task_mod, "fetch_key_rate", return_value=[]) as mock_fetch,
):
result = task_mod.cbr_macro_sync.run(from_date="2020-01-01", to_date="2020-12-31")
mock_fetch.assert_called_once_with(from_date=date(2020, 1, 1), to_date=date(2020, 12, 31))
assert result["from_date"] == "2020-01-01"
assert result["to_date"] == "2020-12-31"
assert result["fetched"] == 0
assert result["upserted"] == 0
def test_task_reraises_fetch_failure() -> None:
"""Ошибка fetch'а пробрасывается (surfaces в Celery/GlitchTip), не глотается.
DB-сессия при этом закрывается."""
from app.workers.tasks import cbr_macro_sync as task_mod
db, _ = _make_mock_db()
with (
patch.object(task_mod, "SessionLocal", return_value=db),
patch.object(task_mod, "fetch_key_rate", side_effect=RuntimeError("CBR down")),
):
try:
task_mod.cbr_macro_sync.run()
raise AssertionError("expected RuntimeError to propagate")
except RuntimeError as e:
assert "CBR down" in str(e)
db.close.assert_called_once()
def test_task_registered_in_beat_weekly() -> None:
"""Задача зарегистрирована в beat как еженедельная (понедельник)."""
from app.workers.beat_schedule import build_beat_schedule
schedule = build_beat_schedule()
assert "cbr-macro-sync-weekly" in schedule
entry = schedule["cbr-macro-sync-weekly"]
assert entry["task"] == "tasks.cbr_macro_sync.cbr_macro_sync"
# crontab day_of_week = понедельник (1)
assert 1 in entry["schedule"].day_of_week