From 334ac2a315aa513b568ab22c80f9b1a60c2d7be7 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Thu, 6 Aug 2026 21:36:16 +0500 Subject: [PATCH] =?UTF-8?q?fix(tests):=20=D0=BF=D0=BE=D1=87=D0=B8=D0=BD?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B8=20=D0=B2=D1=8B=D1=87=D0=B8=D1=81?= =?UTF-8?q?=D1=82=D0=B8=D1=82=D1=8C=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=BA=D0=B8,=20=D0=BA=D0=BE=D1=82=D0=BE=D1=80=D1=8B=D0=B5=20?= =?UTF-8?q?=D0=BD=D0=B5=20=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D0=BD=D1=8F=D0=BB?= =?UTF-8?q?=D0=B8=D1=81=D1=8C=20=D0=BD=D0=B8=20=D1=80=D0=B0=D0=B7=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Три инцидента за сутки (#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, причина пропуска печатает конкретный ожидаемый путь. Тесты не удалялись ради зелени: удалены только те, чьё проверяемое поведение намеренно убрано из продукта, с указанием, что покрывает его замену. --- backend/tests/api/v1/test_admin_cadastre.py | 44 ++----------------- .../tests/api/v1/test_admin_ekburg_permits.py | 16 ------- .../scrapers/test_ekb_ppt_tep_parser.py | 20 ++++++--- backend/tests/test_admin_weight_profiles.py | 38 ---------------- .../backend/tests/test_gar_flats_loader.py | 10 ++++- 5 files changed, 24 insertions(+), 104 deletions(-) diff --git a/backend/tests/api/v1/test_admin_cadastre.py b/backend/tests/api/v1/test_admin_cadastre.py index 3df39895..189ecba1 100644 --- a/backend/tests/api/v1/test_admin_cadastre.py +++ b/backend/tests/api/v1/test_admin_cadastre.py @@ -5,32 +5,16 @@ from __future__ import annotations -# scrape_cadastre зависит от app.scrapers.nspd_bulk_client (PR 2/5). -# Используем importlib.util.find_spec вместо прямого import — иначе -# `import app.workers...` пересоздаёт `app` как Python package и -# перебивает FastAPI instance, привязанный в строке выше → AttributeError -# на app.dependency_overrides. -import importlib.util +# NB: `import app.workers...` здесь запрещён — он пересоздаёт `app` как Python +# package и перебивает FastAPI instance, привязанный ниже → AttributeError на +# app.dependency_overrides. Патчим воркер строкой, через patch("app.workers..."). from typing import Any from unittest.mock import MagicMock, patch -import pytest from fastapi.testclient import TestClient 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_HEADERS = {"X-Admin-Token": ADMIN_TOKEN} @@ -113,7 +97,6 @@ def _make_sample_job_row() -> dict[str, Any]: # ── Tests ──────────────────────────────────────────────────────────────────── -@requires_scrape_cadastre @patch("app.core.config.settings.scrape_admin_token", ADMIN_TOKEN) def test_create_job_pilot_returns_job_id() -> None: """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() -@requires_scrape_cadastre @patch("app.core.config.settings.scrape_admin_token", ADMIN_TOKEN) def test_create_job_manual_list() -> None: """POST /cadastre/jobs scope=manual_list с явным списком.""" @@ -174,7 +156,6 @@ def test_create_job_manual_list() -> None: app.dependency_overrides.clear() -@requires_scrape_cadastre @patch("app.core.config.settings.scrape_admin_token", ADMIN_TOKEN) def test_create_job_manual_list_empty_quarters_returns_400() -> None: """scope=manual_list без quarters → 400.""" @@ -294,22 +275,3 @@ def test_cancel_job_not_found_returns_404() -> None: assert response.status_code == 404 finally: 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) diff --git a/backend/tests/api/v1/test_admin_ekburg_permits.py b/backend/tests/api/v1/test_admin_ekburg_permits.py index 7c75f376..1c98bac3 100644 --- a/backend/tests/api/v1/test_admin_ekburg_permits.py +++ b/backend/tests/api/v1/test_admin_ekburg_permits.py @@ -73,19 +73,3 @@ def test_trigger_invalid_year_returns_422(bad_year: int) -> None: client = TestClient(app) response = client.post(ENDPOINT, json={"year": bad_year}, headers=ADMIN_HEADERS) 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 diff --git a/backend/tests/services/scrapers/test_ekb_ppt_tep_parser.py b/backend/tests/services/scrapers/test_ekb_ppt_tep_parser.py index 292f3653..662e76a7 100644 --- a/backend/tests/services/scrapers/test_ekb_ppt_tep_parser.py +++ b/backend/tests/services/scrapers/test_ekb_ppt_tep_parser.py @@ -202,9 +202,7 @@ class TestParseTable11: # Реальная зона должна быть в результате assert "Зона жилой застройки" in zone_names # Артефакты (zone_name пуст, не is_total) отброшены - non_total_no_zone = [ - r for r in result if not r["zone_name"] and not r.get("is_total") - ] + non_total_no_zone = [r for r in result if not r["zone_name"] and not r.get("is_total")] assert non_total_no_zone == [] # Итого-строка осталась assert any(r.get("is_total") for r in result) @@ -483,15 +481,23 @@ class TestParsePptTepMocked: # ── Тест на реальном PDF-образце (skip в CI) ───────────────────────────────── - -_REAL_PDF_PATH = Path( - "C:/Users/user/source/repos/gendesign/.playwright-mcp/ekb-docs/ppt2018_22823_poyasnit_PPT.pdf" +# +# Путь repo-relative, а не абсолютный `C:/Users/user/source/repos/...`: с +# 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( not _REAL_PDF_PATH.exists(), - reason="Реальный PDF-образец отсутствует (только для локального запуска)", + reason=f"Реальный PDF-образец отсутствует ({_REAL_PDF_PATH}) — только локальный запуск", ) class TestParsePptTepRealPdf: """Интеграционные тесты на реальном образце ppt2018_22823.""" diff --git a/backend/tests/test_admin_weight_profiles.py b/backend/tests/test_admin_weight_profiles.py index da53c58d..13316e56 100644 --- a/backend/tests/test_admin_weight_profiles.py +++ b/backend/tests/test_admin_weight_profiles.py @@ -399,41 +399,3 @@ def test_list_profiles_with_system_service(monkeypatch: pytest.MonkeyPatch) -> N assert len(captured_params) == 1 assert captured_params[0]["system_user_id"] == SYSTEM_USER_ID 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 diff --git a/tradein-mvp/backend/tests/test_gar_flats_loader.py b/tradein-mvp/backend/tests/test_gar_flats_loader.py index e98fc4c5..62b2e9a3 100644 --- a/tradein-mvp/backend/tests/test_gar_flats_loader.py +++ b/tradein-mvp/backend/tests/test_gar_flats_loader.py @@ -449,8 +449,14 @@ def test_upsert_and_canon_match_populates_gar_flat_count(gar_dir: str) -> None: db = _live_session() assert db is not None try: - db.execute(text(_NORMALIZER_FN)) - db.execute(text(_CANON_FN)) + # exec_driver_sql, НЕ text(): в обеих функциях есть regexp-группы `(?:...)`, + # а `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( text( "CREATE TEMP TABLE gar_house_flats ("