"""Shared pytest fixtures для backend-сьюта. NB: RBAC-гейт (app/main.py `rbac_guard`) требует заголовок `X-Authenticated-User` на любом non-public пути — иначе 401. Тесты, бьющие по `app` через TestClient, должны слать этот заголовок ЯВНО на запрос (напр. test_parcel_by_bbox), либо тестировать именно no-auth путь (admin-тесты ждут 401 без заголовка). Глобально заголовок НЕ инжектим — это ломает тесты, проверяющие отказ авторизации. """ from __future__ import annotations import os from pathlib import Path import pytest # Включаем test-mode ДО любого импорта app: rbac_guard (app/main.py) пропускает # запросы при settings.testing=True (юнит-тесты бьют по app мимо Caddy и не несут # X-Authenticated-User → иначе 401 на всём api/v1). Ставим и env (на случай # ре-инстанцирования Settings), и атрибут синглтона напрямую (надёжно независимо # от порядка импортов). RBAC-логика (401/403) покрыта в test_rbac.py — там своя # копия middleware, на settings.testing НЕ смотрит, поэтому остаётся валидной. os.environ.setdefault("TESTING", "1") from app.core.config import settings settings.testing = True @pytest.fixture(autouse=True) def _clear_dependency_overrides(): """Сбрасывать app.dependency_overrides после каждого теста (anti-leak). Тест переопределяет get_db через app.dependency_overrides; без сброса override течёт в следующий тест → ложные коннекты к реальной БД / флейки. Импорт app локальный (ленивый) — не тянем тяжёлый app.main при сборе тестов без него. """ yield try: from app.main import app app.dependency_overrides.clear() except Exception: pass # ── Бюджет пропусков (#2745) ────────────────────────────────────────────────── # # Три инцидента за одни сутки (#2722, #2729, #2740) — один класс дефекта: # проверка, которая тихо не исполняется, со временем перестаёт быть верной, и # узнают об этом ровно тогда, когда на неё понадобилось опереться. `-rs` в CI # делает пропуск ГРОМКИМ; этот хук делает его УЧТЁННЫМ — пропущено может быть # только то, что записано в `tests/skip_allowlist.txt`. Любой новый пропуск (в # том числе возникший сам: отвалилась зависимость, исчезла БД, кто-то дописал # skipif «пока починю») роняет прогон, пока его не внесут в список осознанно. # # Список — НАДмножество: покрывает и CI (где есть Postgres и native-libs), и # ноутбук разработчика (где их нет). Запись, чей пропуск не случился, — не # ошибка; ошибка — пропуск без записи. # # Сюда же попадает xfail: pytest рапортует его как skipped, поэтому xfail без # `strict=True` (зеленеет и когда тест внезапно ПРОШЁЛ) тоже придётся объявить. # # Чего хук НЕ видит: `--deselect` / `-k` / `-m` — они убирают тест до прогона, # отчёта о пропуске не возникает вовсе. Против них работает правило «никаких # deselect'ов в workflow» (см. .forgejo/workflows/ci-tradein.yml). _SKIP_ALLOWLIST_PATH = Path(__file__).parent / "skip_allowlist.txt" _observed_skips: set[str] = set() def _allowed_skips() -> set[str]: if not _SKIP_ALLOWLIST_PATH.exists(): return set() lines = _SKIP_ALLOWLIST_PATH.read_text(encoding="utf-8").splitlines() return {stripped for line in lines if (stripped := line.split("#", 1)[0].strip())} def _record_skip(report) -> None: if report.skipped: # nodeid без «[параметров]»: список фиксирует, какая ПРОВЕРКА может # отсутствовать, а не каждую её параметризацию. _observed_skips.add(report.nodeid.split("[", 1)[0]) def pytest_runtest_logreport(report) -> None: _record_skip(report) def pytest_collectreport(report) -> None: # Модульный pytest.skip(allow_module_level=True) приходит сюда, а не в # logreport, и nodeid у него — путь файла. _record_skip(report) def pytest_sessionfinish(session, exitstatus) -> None: unlisted = sorted(_observed_skips - _allowed_skips()) if not unlisted: return print( f"\nНЕУЧТЁННЫЙ ПРОПУСК ({len(unlisted)}): проверка не исполнилась и не " f"объявлена в {_SKIP_ALLOWLIST_PATH.name}:" ) for nodeid in unlisted: print(f" - {nodeid}") print( "Почини тест либо внеси его в skip_allowlist.txt с причиной — " "пропуск без записи неотличим от пройденной проверки." ) if exitstatus == 0: session.exitstatus = 1