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

83 lines
3.9 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 164 (issue #2235).
Прод применяет data/sql построчно строго (ON_ERROR_STOP). Полный DB-прогон
идемпотентности требует живой БД; здесь мы фиксируем структурные инварианты,
которые ГАРАНТИРУЮТ идемпотентность и НЕдеструктивность по построению:
1. Транзакционность (BEGIN/COMMIT) — атомарный апдейт.
2. Идемпотентность: апдейт ограничен строками, чей source_url ещё НЕ канонический
(`source_url <> '…offer/'||source_id||'/'`) И входит в группу с >1 строкой
(CTE `shared`, HAVING count(*)>1). После первого прогона таких строк нет →
повторный прогон = no-op.
3. НЕдеструктивность: миграция канонизирует source_url, а НЕ гасит строки —
`is_active = false` в файле быть НЕ должно (защита от регресса к
деструктивному «схлопыванию», отвергнутому по данным: дубли — это РАЗНЫЕ
офферы, не дубли-строки).
4. Скоуп — только yandex.
"""
from __future__ import annotations
import re
from pathlib import Path
_SQL_DIR = Path(__file__).resolve().parents[1] / "data" / "sql"
_MIGRATION_164 = _SQL_DIR / "164_yandex_url_canonicalize_active_dups.sql"
def _sql() -> str:
return _MIGRATION_164.read_text(encoding="utf-8")
def _executable_sql() -> str:
"""SQL без комментариев — только исполняемые строки (`--`-комменты вырезаны).
Заголовок миграции в прозе объясняет, ПОЧЕМУ отвергнуто «схлопывание»
(is_active=false), поэтому destructive-проверки должны смотреть на код, а не
на комментарии. В этой миграции `--` не встречается внутри строковых литералов.
"""
lines = []
for raw in _MIGRATION_164.read_text(encoding="utf-8").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()
def test_migration_164_exists() -> None:
assert _MIGRATION_164.exists(), f"missing migration: {_MIGRATION_164}"
def test_migration_164_is_transactional() -> None:
sql = _sql()
assert "BEGIN;" in sql
assert "COMMIT;" in sql
def test_migration_164_is_idempotent_by_construction() -> None:
"""Апдейт исключает уже-канонические строки → повторный прогон no-op."""
flat = _flat(_executable_sql())
# Группировка дублей (HAVING count(*) > 1) + пропуск уже-канонических строк.
assert "having count(*) > 1" in flat.lower()
assert "source_url <> 'https://realty.yandex.ru/offer/'" in flat
def test_migration_164_is_non_destructive() -> None:
"""Канонизация source_url, НЕ гашение строк: никакого is_active=false в коде."""
flat = _flat(_executable_sql()).lower()
assert "is_active = false" not in flat
assert "is_active=false" not in flat
# Мутируется именно source_url.
assert "set source_url = 'https://realty.yandex.ru/offer/'" in flat
def test_migration_164_scoped_to_yandex_active() -> None:
flat = _flat(_executable_sql())
assert "l.source = 'yandex'" in flat
assert "l.is_active" in flat
# Только числовой yandex offerId идёт в канонический URL.
assert "l.source_id ~ '^[0-9]+$'" in flat