"""Smoke-тесты Celery-обвязки точек выхода газосети (#2119 Фаза B2, PR-4). - sync_gas_outlet_points зарегистрирована (module в include-списке celery_app); - beat содержит gas-outlet-points-sync-weekly (еженедельно, вторник); - таск дважды дёргает лоадер: load_gas_outlet_points → geocode_gas_outlets(apply=True), каждый шаг graceful (сбой одного не роняет второй) — mock без сети/БД. """ from __future__ import annotations from unittest.mock import patch def test_task_registered_in_celery_include() -> None: """Модуль connection_capacity_sync в include-списке celery_app.""" from app.workers.celery_app import celery_app assert "app.workers.tasks.connection_capacity_sync" in celery_app.conf.include def test_beat_schedule_includes_gas_outlet_sync_weekly_tuesday() -> None: """gas-outlet-points-sync-weekly зарегистрирована еженедельно во вторник.""" from app.workers import beat_schedule # build_beat_schedule читает DB → мокаем _build_beat_schedule_from_db, # чтобы тест не зависел от живого Postgres. with patch.object(beat_schedule, "_build_beat_schedule_from_db", return_value={}): schedule = beat_schedule.build_beat_schedule() assert ( "gas-outlet-points-sync-weekly" in schedule ), f"gas-outlet-points-sync-weekly отсутствует в beat: {sorted(schedule.keys())}" entry = schedule["gas-outlet-points-sync-weekly"] assert entry["task"] == "tasks.connection_capacity_sync.sync_gas_outlet_points" # crontab day_of_week = вторник (2). assert 2 in entry["schedule"].day_of_week def test_sync_gas_outlet_points_load_then_geocode() -> None: """Таск: load_gas_outlet_points → geocode_gas_outlets(apply=True), оба результата в ответе.""" from app.workers.tasks import connection_capacity_sync as ccs load_result = {"rows": 1037, "inserted": 10, "updated": 1027} geo_result = {"candidates": 5, "geocoded": 3, "applied": True} with ( patch( "app.services.site_finder.ekgas_outlet_loader.load_gas_outlet_points", return_value=load_result, ), patch( "app.services.site_finder.ekgas_outlet_loader.geocode_gas_outlets", return_value=geo_result, ) as mock_geo, patch.object(ccs, "_log_breadcrumb"), ): out = ccs.sync_gas_outlet_points() assert out["load"] == load_result assert out["geocode"] == geo_result # геокод вызван С apply=True (реальная запись geom на проде). assert mock_geo.call_args.kwargs.get("apply") is True def test_sync_gas_outlet_points_geocode_failure_graceful() -> None: """Сбой geocode-шага не роняет таск: load-результат сохраняется, geocode → error-словарь.""" from app.workers.tasks import connection_capacity_sync as ccs load_result = {"rows": 1037, "inserted": 0, "updated": 1037} with ( patch( "app.services.site_finder.ekgas_outlet_loader.load_gas_outlet_points", return_value=load_result, ), patch( "app.services.site_finder.ekgas_outlet_loader.geocode_gas_outlets", side_effect=RuntimeError("DaData quota exhausted"), ), patch.object(ccs, "_log_breadcrumb"), ): out = ccs.sync_gas_outlet_points() assert out["load"] == load_result # шаг 1 не откатывается сбоем шага 2 assert "error" in out["geocode"] assert "DaData" in out["geocode"]["error"] def test_sync_gas_outlet_points_load_failure_still_attempts_geocode() -> None: """Сбой load-шага не мешает попытке geocode (шаги независимы, каждый graceful).""" from app.workers.tasks import connection_capacity_sync as ccs geo_result = {"candidates": 0, "geocoded": 0, "applied": True} with ( patch( "app.services.site_finder.ekgas_outlet_loader.load_gas_outlet_points", side_effect=RuntimeError("ekgas catalog 503"), ), patch( "app.services.site_finder.ekgas_outlet_loader.geocode_gas_outlets", return_value=geo_result, ), patch.object(ccs, "_log_breadcrumb"), ): out = ccs.sync_gas_outlet_points() assert "error" in out["load"] assert out["geocode"] == geo_result