gendesign/tradein-mvp/backend/tests/test_migration_165_remove_n1_source.py

95 lines
4.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.

"""Static guards for migration 165 (issue #2204 — полное выключение источника n1).
Прод применяет data/sql построчно строго (ON_ERROR_STOP). Полный DB-прогон
требует живой БД; здесь фиксируем структурные инварианты, которые ГАРАНТИРУЮТ
идемпотентность и НЕдеструктивность к самим listings-строкам по построению:
1. Транзакционность (BEGIN/COMMIT) — атомарный апдейт.
2. НЕдеструктивность к данным: строки listings source='n1' НЕ удаляются, только
деактивируются (is_active=false) — исторические объявления остаются в БД.
Никакого `DELETE FROM listings` в коде.
3. Идемпотентность по построению:
- страховочный UPDATE ограничен `AND is_active` -> повторный прогон no-op;
- `DELETE ... WHERE source IN (...)` / `WHERE source = 'n1'` -> повторный
прогон удаляет 0 строк.
4. Скоуп чистки конфига: обе n1-строки расписаний (n1_city_sweep +
deactivate_stale_n1) и per-source knob scraper_settings 'n1'.
Комментарий-заголовок в прозе объясняет намерение (в т.ч. фразу «Никакого
DELETE FROM listings»), поэтому destructive-проверки смотрят на ИСПОЛНЯЕМЫЙ SQL
(`--`-комменты вырезаны), а не на прозу.
"""
from __future__ import annotations
import re
from pathlib import Path
_SQL_DIR = Path(__file__).resolve().parents[1] / "data" / "sql"
_MIGRATION_165 = _SQL_DIR / "165_remove_n1_source.sql"
def _sql() -> str:
return _MIGRATION_165.read_text(encoding="utf-8")
def _executable_sql() -> str:
"""SQL без комментариев — только исполняемые строки (`--`-комменты вырезаны).
В этой миграции `--` не встречается внутри строковых литералов, поэтому
split по первому `--` безопасно отделяет код от построчного комментария.
"""
lines = []
for raw in _sql().splitlines():
code = raw.split("--", 1)[0]
if code.strip():
lines.append(code)
return "\n".join(lines)
def _flat(text: str) -> str:
return re.sub(r"\s+", " ", text).strip().lower()
def test_migration_165_exists() -> None:
assert _MIGRATION_165.exists(), f"missing migration: {_MIGRATION_165}"
def test_migration_165_is_transactional() -> None:
sql = _sql()
assert "BEGIN;" in sql
assert "COMMIT;" in sql
def test_migration_165_does_not_delete_listings() -> None:
"""Строки listings source='n1' НЕ удаляются — только деактивируются."""
flat = _flat(_executable_sql())
assert "delete from listings" not in flat
# Деактивация вместо удаления.
assert "update listings" in flat
assert "set is_active = false" in flat
def test_migration_165_listings_update_idempotent_and_scoped() -> None:
"""UPDATE ограничен source='n1' AND is_active -> повторный прогон no-op."""
flat = _flat(_executable_sql())
assert "where source = 'n1'" in flat
assert "and is_active" in flat
def test_migration_165_deletes_both_n1_schedules() -> None:
flat = _flat(_executable_sql())
assert "delete from scrape_schedules" in flat
assert "'n1_city_sweep'" in flat
assert "'deactivate_stale_n1'" in flat
def test_migration_165_deletes_n1_scraper_settings() -> None:
flat = _flat(_executable_sql())
assert "delete from scraper_settings" in flat
assert "where source = 'n1'" in flat
def test_migration_165_no_psycopg_trap() -> None:
"""Никаких :param::type — psycopg v3 требует CAST(... AS type)."""
assert not re.search(r":\w+::", _sql())