gendesign/backend/tests/api/v1/test_admin_cadastre.py
bot-backend 334ac2a315
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
fix(tests): починить и вычистить проверки, которые не исполнялись ни разу
Три инцидента за сутки (#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, причина
   пропуска печатает конкретный ожидаемый путь.

Тесты не удалялись ради зелени: удалены только те, чьё проверяемое поведение
намеренно убрано из продукта, с указанием, что покрывает его замену.
2026-08-06 21:36:16 +05:00

277 lines
9.2 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.

"""Тесты для admin_cadastre.py API endpoints.
Использует FastAPI TestClient с mock DB и mock Celery tasks.
"""
from __future__ import annotations
# 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
from fastapi.testclient import TestClient
from app.main import app
# Токен для тестов (не реальный)
ADMIN_TOKEN = "test-admin-token"
ADMIN_HEADERS = {"X-Admin-Token": ADMIN_TOKEN}
def _make_mock_db(quarters: list[str] | None = None, job_row: dict[str, Any] | None = None):
"""Создать mock db session с преднастроенными ответами."""
db = MagicMock()
# Результат для _resolve_quarters (SELECT cad_number FROM cad_quarters_geom)
quarter_rows = [(q,) for q in (quarters or ["66:41:0303161", "66:41:0303162"])]
# Mock execute chain для разных запросов
def mock_execute(stmt, params=None):
stmt_str = str(stmt)
result = MagicMock()
if "cad_quarters_geom" in stmt_str and "cad_number" in stmt_str:
# _resolve_quarters query
result.all.return_value = quarter_rows
elif "cadastre_jobs" in stmt_str and "RETURNING job_id" in stmt_str:
# _create_job INSERT
result.scalar_one.return_value = 42
elif "cadastre_jobs" in stmt_str and "SELECT" in stmt_str:
# get/list jobs
result.mappings.return_value.all.return_value = [job_row] if job_row else []
result.mappings.return_value.first.return_value = job_row
elif "RETURNING job_id" in stmt_str:
# cancel/resume
result.first.return_value = (42,)
else:
result.all.return_value = []
result.mappings.return_value.all.return_value = []
result.mappings.return_value.first.return_value = None
result.scalar_one.return_value = 1
return result
db.execute = mock_execute
db.commit = MagicMock()
return db
def _mock_get_db(db):
"""FastAPI dependency override factory."""
def _get_db_override():
yield db
return _get_db_override
def _make_sample_job_row() -> dict[str, Any]:
import datetime
return {
"job_id": 42,
"name": "cadastre_pilot",
"job_kind": "pilot",
"scope": "pilot",
"status": "queued",
"triggered_by": "manual",
"rate_ms": 333,
"created_at": datetime.datetime(2026, 5, 15, 10, 0, 0),
"started_at": None,
"finished_at": None,
"heartbeat_at": None,
"targets_total": 50,
"targets_done": 0,
"targets_failed": 0,
"targets_skipped": 0,
"requests_count": 0,
"waf_blocked_count": 0,
"error": None,
"phase_state": None,
"raw_targets": {"quarters": ["66:41:0303161"]},
}
# ── Tests ────────────────────────────────────────────────────────────────────
@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."""
quarters_50 = [f"66:41:{i:07d}" for i in range(50)]
db = _make_mock_db(quarters=quarters_50)
from app.core.db import get_db
app.dependency_overrides[get_db] = _mock_get_db(db)
try:
with patch("app.workers.tasks.scrape_cadastre.enqueue_cadastre_harvest") as mock_task:
mock_task.apply_async = MagicMock()
client = TestClient(app)
response = client.post(
"/api/v1/admin/cadastre/jobs",
json={"scope": "pilot"},
headers=ADMIN_HEADERS,
)
assert response.status_code == 200, response.text
body = response.json()
assert "job_id" in body
assert body["scope"] == "pilot"
assert body["targets_total"] == 50
assert "estimate_minutes" in body
finally:
app.dependency_overrides.clear()
@patch("app.core.config.settings.scrape_admin_token", ADMIN_TOKEN)
def test_create_job_manual_list() -> None:
"""POST /cadastre/jobs scope=manual_list с явным списком."""
db = _make_mock_db()
from app.core.db import get_db
app.dependency_overrides[get_db] = _mock_get_db(db)
try:
with patch("app.workers.tasks.scrape_cadastre.enqueue_cadastre_harvest") as mock_task:
mock_task.apply_async = MagicMock()
client = TestClient(app)
response = client.post(
"/api/v1/admin/cadastre/jobs",
json={
"scope": "manual_list",
"quarters": ["66:41:0303161", "66:41:0303162"],
},
headers=ADMIN_HEADERS,
)
assert response.status_code == 200, response.text
body = response.json()
assert body["targets_total"] == 2
finally:
app.dependency_overrides.clear()
@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."""
db = _make_mock_db()
from app.core.db import get_db
app.dependency_overrides[get_db] = _mock_get_db(db)
try:
client = TestClient(app)
response = client.post(
"/api/v1/admin/cadastre/jobs",
json={"scope": "manual_list"},
headers=ADMIN_HEADERS,
)
assert response.status_code == 400
finally:
app.dependency_overrides.clear()
@patch("app.core.config.settings.scrape_admin_token", ADMIN_TOKEN)
def test_list_jobs_returns_list() -> None:
"""GET /cadastre/jobs → список jobs."""
db = _make_mock_db(job_row=_make_sample_job_row())
# Переопределяем execute чтобы list query вернул список
job_row = _make_sample_job_row()
mock_result = MagicMock()
mock_result.mappings.return_value.all.return_value = [job_row]
db.execute = MagicMock(return_value=mock_result)
from app.core.db import get_db
app.dependency_overrides[get_db] = _mock_get_db(db)
try:
client = TestClient(app)
response = client.get("/api/v1/admin/cadastre/jobs", headers=ADMIN_HEADERS)
assert response.status_code == 200, response.text
body = response.json()
assert isinstance(body, list)
assert len(body) == 1
assert body[0]["job_id"] == 42
assert body[0]["scope"] == "pilot"
assert "progress_pct" in body[0]
finally:
app.dependency_overrides.clear()
@patch("app.core.config.settings.scrape_admin_token", ADMIN_TOKEN)
def test_get_job_not_found_returns_404() -> None:
"""GET /cadastre/jobs/9999 → 404."""
db = MagicMock()
mock_result = MagicMock()
mock_result.mappings.return_value.first.return_value = None
db.execute = MagicMock(return_value=mock_result)
from app.core.db import get_db
app.dependency_overrides[get_db] = _mock_get_db(db)
try:
client = TestClient(app)
response = client.get("/api/v1/admin/cadastre/jobs/9999", headers=ADMIN_HEADERS)
assert response.status_code == 404
finally:
app.dependency_overrides.clear()
@patch("app.core.config.settings.scrape_admin_token", ADMIN_TOKEN)
def test_cancel_job_success() -> None:
"""POST /cadastre/jobs/42/cancel → {cancelled: true}."""
db = MagicMock()
mock_result = MagicMock()
mock_result.first.return_value = (42,)
db.execute = MagicMock(return_value=mock_result)
db.commit = MagicMock()
from app.core.db import get_db
app.dependency_overrides[get_db] = _mock_get_db(db)
try:
client = TestClient(app)
response = client.post(
"/api/v1/admin/cadastre/jobs/42/cancel",
headers=ADMIN_HEADERS,
)
assert response.status_code == 200, response.text
body = response.json()
assert body["cancelled"] is True
assert body["job_id"] == 42
finally:
app.dependency_overrides.clear()
@patch("app.core.config.settings.scrape_admin_token", ADMIN_TOKEN)
def test_cancel_job_not_found_returns_404() -> None:
"""POST /cadastre/jobs/9999/cancel когда job не найден → 404."""
db = MagicMock()
mock_result = MagicMock()
mock_result.first.return_value = None # job не найден / уже cancelled
db.execute = MagicMock(return_value=mock_result)
db.commit = MagicMock()
from app.core.db import get_db
app.dependency_overrides[get_db] = _mock_get_db(db)
try:
client = TestClient(app)
response = client.post(
"/api/v1/admin/cadastre/jobs/9999/cancel",
headers=ADMIN_HEADERS,
)
assert response.status_code == 404
finally:
app.dependency_overrides.clear()