fix(tests): починить и вычистить проверки, которые не исполнялись ни разу
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m5s
CI Trade-In / backend-tests (pull_request) Successful in 3m16s
CI / backend-tests (pull_request) Successful in 15m43s

Три инцидента за сутки (#2722, #2729, #2740) — один класс дефекта: проверка,
которая тихо не исполняется, со временем перестаёт быть верной. Ревизия всех
механизмов пропуска в репозитории нашла ещё четыре таких места.

1. test_gar_flats_loader::test_upsert_and_canon_match_populates_gar_flat_count
   падал ДО первого утверждения: `text()` читает `(?:Россия|...)` в regexp как
   bind-параметр `:Россия` и бросает InvalidRequestError. Тест жил с 2026-06-28
   под skipif «нет БД» и с реальной Postgres не запускался никогда. Перевод на
   exec_driver_sql (драйверный путь не парсит bind-имена) — 23 passed.

2. requires_scrape_cadastre — skipif с причиной «нужно смержить PR #170».
   PR #170 давно в main, модуль на месте, условие не срабатывало никогда:
   мёртвый гард на трёх тестах. Снят.

3. Четыре теста X-Admin-Token (test_admin_cadastre, test_admin_ekburg_permits,
   test_admin_weight_profiles ×2) проверяли гейт, удалённый в #437: endpoint'ы
   не несут verify_admin_token, сам helper в app/core/deps.py уже никем не
   импортируется. 401 недостижим без реверта security-решения — тесты удалены,
   а не «пропущены навсегда». Замена покрыта tests/test_rbac.py (24 теста).

4. test_ekb_ppt_tep_parser искал образец по абсолютному C:/Users/user/... —
   условие пропуска не выполнялось ни на одной машине, кроме одного дев-бокса,
   т.е. 6 тестов были не пропущены, а мертвы. Путь стал repo-relative, причина
   пропуска печатает конкретный ожидаемый путь.

Тесты не удалялись ради зелени: удалены только те, чьё проверяемое поведение
намеренно убрано из продукта, с указанием, что покрывает его замену.
This commit is contained in:
bot-backend 2026-08-06 21:36:16 +05:00
parent c86a5378ef
commit 334ac2a315
5 changed files with 24 additions and 104 deletions

View file

@ -5,32 +5,16 @@
from __future__ import annotations from __future__ import annotations
# scrape_cadastre зависит от app.scrapers.nspd_bulk_client (PR 2/5). # NB: `import app.workers...` здесь запрещён — он пересоздаёт `app` как Python
# Используем importlib.util.find_spec вместо прямого import — иначе # package и перебивает FastAPI instance, привязанный ниже → AttributeError на
# `import app.workers...` пересоздаёт `app` как Python package и # app.dependency_overrides. Патчим воркер строкой, через patch("app.workers...").
# перебивает FastAPI instance, привязанный в строке выше → AttributeError
# на app.dependency_overrides.
import importlib.util
from typing import Any from typing import Any
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import pytest
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from app.main import app from app.main import app
_SCRAPE_CADASTRE_AVAILABLE = (
importlib.util.find_spec("app.workers.tasks.scrape_cadastre") is not None
)
requires_scrape_cadastre = pytest.mark.skipif(
not _SCRAPE_CADASTRE_AVAILABLE,
reason=(
"app.scrapers.nspd_bulk_client отсутствует — нужно смержить main (PR #170). "
"Команда: git rebase origin/main"
),
)
# Токен для тестов (не реальный) # Токен для тестов (не реальный)
ADMIN_TOKEN = "test-admin-token" ADMIN_TOKEN = "test-admin-token"
ADMIN_HEADERS = {"X-Admin-Token": ADMIN_TOKEN} ADMIN_HEADERS = {"X-Admin-Token": ADMIN_TOKEN}
@ -113,7 +97,6 @@ def _make_sample_job_row() -> dict[str, Any]:
# ── Tests ──────────────────────────────────────────────────────────────────── # ── Tests ────────────────────────────────────────────────────────────────────
@requires_scrape_cadastre
@patch("app.core.config.settings.scrape_admin_token", ADMIN_TOKEN) @patch("app.core.config.settings.scrape_admin_token", ADMIN_TOKEN)
def test_create_job_pilot_returns_job_id() -> None: def test_create_job_pilot_returns_job_id() -> None:
"""POST /cadastre/jobs scope=pilot → job_id + targets_total.""" """POST /cadastre/jobs scope=pilot → job_id + targets_total."""
@ -144,7 +127,6 @@ def test_create_job_pilot_returns_job_id() -> None:
app.dependency_overrides.clear() app.dependency_overrides.clear()
@requires_scrape_cadastre
@patch("app.core.config.settings.scrape_admin_token", ADMIN_TOKEN) @patch("app.core.config.settings.scrape_admin_token", ADMIN_TOKEN)
def test_create_job_manual_list() -> None: def test_create_job_manual_list() -> None:
"""POST /cadastre/jobs scope=manual_list с явным списком.""" """POST /cadastre/jobs scope=manual_list с явным списком."""
@ -174,7 +156,6 @@ def test_create_job_manual_list() -> None:
app.dependency_overrides.clear() app.dependency_overrides.clear()
@requires_scrape_cadastre
@patch("app.core.config.settings.scrape_admin_token", ADMIN_TOKEN) @patch("app.core.config.settings.scrape_admin_token", ADMIN_TOKEN)
def test_create_job_manual_list_empty_quarters_returns_400() -> None: def test_create_job_manual_list_empty_quarters_returns_400() -> None:
"""scope=manual_list без quarters → 400.""" """scope=manual_list без quarters → 400."""
@ -294,22 +275,3 @@ def test_cancel_job_not_found_returns_404() -> None:
assert response.status_code == 404 assert response.status_code == 404
finally: finally:
app.dependency_overrides.clear() app.dependency_overrides.clear()
@pytest.mark.skip(
reason=(
"X-Admin-Token gate удалён в #437 (refactor(security): убрать X-Admin-Token — "
"Caddy basic_auth + RBAC middleware достаточны). Admin endpoint'ы больше не "
"несут verify_admin_token dependency; защита — на уровне Caddy/RBAC. В test-mode "
"RBAC bypass'ится (CI-rehab 1/3), поэтому 401/503 здесь больше недостижим без "
"реверта security-решения #437. Тест проверял удалённое поведение."
)
)
def test_create_job_no_token_returns_401() -> None:
"""Без X-Admin-Token → 401/503 (устарело: токен-гейт удалён в #437)."""
client = TestClient(app)
response = client.post(
"/api/v1/admin/cadastre/jobs",
json={"scope": "pilot"},
)
assert response.status_code in (401, 503)

View file

@ -73,19 +73,3 @@ def test_trigger_invalid_year_returns_422(bad_year: int) -> None:
client = TestClient(app) client = TestClient(app)
response = client.post(ENDPOINT, json={"year": bad_year}, headers=ADMIN_HEADERS) response = client.post(ENDPOINT, json={"year": bad_year}, headers=ADMIN_HEADERS)
assert response.status_code == 422, f"year={bad_year} должен возвращать 422" assert response.status_code == 422, f"year={bad_year} должен возвращать 422"
@pytest.mark.skip(
reason=(
"X-Admin-Token gate удалён в #437 (refactor(security): убрать X-Admin-Token — "
"Caddy basic_auth + RBAC middleware достаточны). trigger_ekburg_permits больше не "
"несёт verify_admin_token dependency; защита — на уровне Caddy/RBAC. В test-mode "
"RBAC bypass'ится (CI-rehab 1/3), поэтому 401/503 здесь больше недостижим без "
"реверта security-решения #437. Тест проверял удалённое поведение."
)
)
def test_trigger_no_token_returns_401_or_503() -> None:
"""Без X-Admin-Token → 401 или 503 (устарело: токен-гейт удалён в #437)."""
client = TestClient(app)
response = client.post(ENDPOINT, json={})
assert response.status_code in (401, 503), response.text

View file

@ -202,9 +202,7 @@ class TestParseTable11:
# Реальная зона должна быть в результате # Реальная зона должна быть в результате
assert "Зона жилой застройки" in zone_names assert "Зона жилой застройки" in zone_names
# Артефакты (zone_name пуст, не is_total) отброшены # Артефакты (zone_name пуст, не is_total) отброшены
non_total_no_zone = [ non_total_no_zone = [r for r in result if not r["zone_name"] and not r.get("is_total")]
r for r in result if not r["zone_name"] and not r.get("is_total")
]
assert non_total_no_zone == [] assert non_total_no_zone == []
# Итого-строка осталась # Итого-строка осталась
assert any(r.get("is_total") for r in result) assert any(r.get("is_total") for r in result)
@ -483,15 +481,23 @@ class TestParsePptTepMocked:
# ── Тест на реальном PDF-образце (skip в CI) ───────────────────────────────── # ── Тест на реальном PDF-образце (skip в CI) ─────────────────────────────────
#
_REAL_PDF_PATH = Path( # Путь repo-relative, а не абсолютный `C:/Users/user/source/repos/...`: с
"C:/Users/user/source/repos/gendesign/.playwright-mcp/ekb-docs/ppt2018_22823_poyasnit_PPT.pdf" # Windows-путём условие пропуска не выполнялось НИ НА ОДНОЙ машине кроме одного
# старого дев-бокса, т.е. эти 6 тестов были не «пропущены по среде», а мертвы.
# Образец не в git (untracked `.playwright-mcp/`), поэтому в CI пропуск остаётся —
# но теперь он снимается тем, что файл кладут в репо-каталог на любой ОС.
_REAL_PDF_PATH = (
Path(__file__).resolve().parents[4]
/ ".playwright-mcp"
/ "ekb-docs"
/ "ppt2018_22823_poyasnit_PPT.pdf"
) )
@pytest.mark.skipif( @pytest.mark.skipif(
not _REAL_PDF_PATH.exists(), not _REAL_PDF_PATH.exists(),
reason="Реальный PDF-образец отсутствует (только для локального запуска)", reason=f"Реальный PDF-образец отсутствует ({_REAL_PDF_PATH}) — только локальный запуск",
) )
class TestParsePptTepRealPdf: class TestParsePptTepRealPdf:
"""Интеграционные тесты на реальном образце ppt2018_22823.""" """Интеграционные тесты на реальном образце ppt2018_22823."""

View file

@ -399,41 +399,3 @@ def test_list_profiles_with_system_service(monkeypatch: pytest.MonkeyPatch) -> N
assert len(captured_params) == 1 assert len(captured_params) == 1
assert captured_params[0]["system_user_id"] == SYSTEM_USER_ID assert captured_params[0]["system_user_id"] == SYSTEM_USER_ID
assert captured_params[0]["user_id"] == "user-test" assert captured_params[0]["user_id"] == "user-test"
# ── Auth ───────────────────────────────────────────────────────────────────────
_TOKEN_REMOVED_REASON = (
"App-level X-Admin-Token header удалён 2026-05-23 (см. docstring "
"app/api/v1/admin_weight_profiles.py: Caddy basic_auth PR #426 + RBAC достаточны, "
"двойная auth избыточна). Endpoint больше не несёт verify_admin_token dependency. "
"В test-mode RBAC bypass'ится (CI-rehab 1/3) → 401 здесь недостижим без реверта "
"security-решения. Тест проверял удалённое поведение."
)
@pytest.mark.skip(reason=_TOKEN_REMOVED_REASON)
def test_unauthorized_no_token(monkeypatch: pytest.MonkeyPatch) -> None:
"""Запрос без X-Admin-Token → 401 (устарело: токен-гейт удалён 2026-05-23)."""
monkeypatch.setattr("app.core.config.settings.scrape_admin_token", _ADMIN_TOKEN)
client = TestClient(app)
r = client.get(
"/api/v1/admin/site-finder/weight-profiles",
params={"user_id": "user-1"},
# без headers — нет X-Admin-Token
)
assert r.status_code == 401
@pytest.mark.skip(reason=_TOKEN_REMOVED_REASON)
def test_unauthorized_wrong_token(monkeypatch: pytest.MonkeyPatch) -> None:
"""Неверный X-Admin-Token → 401 (устарело: токен-гейт удалён 2026-05-23)."""
monkeypatch.setattr("app.core.config.settings.scrape_admin_token", _ADMIN_TOKEN)
client = TestClient(app)
r = client.get(
"/api/v1/admin/site-finder/weight-profiles",
params={"user_id": "user-1"},
headers={"X-Admin-Token": "wrong-token"},
)
assert r.status_code == 401

View file

@ -449,8 +449,14 @@ def test_upsert_and_canon_match_populates_gar_flat_count(gar_dir: str) -> None:
db = _live_session() db = _live_session()
assert db is not None assert db is not None
try: try:
db.execute(text(_NORMALIZER_FN)) # exec_driver_sql, НЕ text(): в обеих функциях есть regexp-группы `(?:...)`,
db.execute(text(_CANON_FN)) # а `text()` читает `:Россия` / `:кв` / `:асть` как bind-параметры и падает
# с InvalidRequestError ещё до первого утверждения. Драйверный путь ничего
# не парсит. (Тест жил с 2026-06-28 и до #2741 не исполнялся ни разу — под
# skipif «нет БД» падение было не видно.)
raw = db.connection()
raw.exec_driver_sql(_NORMALIZER_FN)
raw.exec_driver_sql(_CANON_FN)
db.execute( db.execute(
text( text(
"CREATE TEMP TABLE gar_house_flats (" "CREATE TEMP TABLE gar_house_flats ("