gendesign/backend/tests/api/v1/test_parcel_snapshot_exclude_dev.py
bot-backend 1302fe63d7
Some checks failed
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Failing after 1m52s
CI / backend-tests (pull_request) Successful in 8m55s
feat(site-finder): snapshot.pdf exclude_dev param — hide competitor by developer name
Optional query param exclude_dev (CSV, case-insensitive) drops competitor
rows by dev_name from the «Конкуренты» block of the parcel snapshot PDF.
Client case: generate a report without a specific developer's offer.
Without the param behavior is unchanged (zero behavior change).
2026-06-18 15:23:28 +03:00

203 lines
7.6 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.

"""Тесты для query-параметра exclude_dev в GET /parcels/{cad}/snapshot.pdf.
Скрывает конкурентов конкретных застройщиков (dev_name) из блока «Конкуренты»
PDF-снимка. Client-кейс: убрать оффер девелопера X из отчёта для девелопера Y.
Юнит-тесты — над чистой функцией `_filter_competitors_by_dev`.
Плюс 1 endpoint smoke (mock DB + patched WeasyPrint-рендер).
"""
from __future__ import annotations
from typing import Any
from unittest.mock import MagicMock
from fastapi.testclient import TestClient
from app.api.v1.parcels import _filter_competitors_by_dev
from app.main import app
def _rows() -> list[dict[str, Any]]:
return [
{"dev_name": "Практика", "comm_name": "ЖК-А", "flat_count": 100, "distance_m": 200.0},
{"dev_name": "Брусника", "comm_name": "ЖК-Б", "flat_count": 200, "distance_m": 400.0},
{"dev_name": "Атлас", "comm_name": "ЖК-В", "flat_count": 50, "distance_m": 800.0},
{"dev_name": None, "comm_name": "ЖК-Г", "flat_count": 30, "distance_m": 900.0},
]
# ── Unit: _filter_competitors_by_dev ──────────────────────────────────────────
def test_filter_none_is_noop() -> None:
"""exclude_dev=None → список идентичен исходному (zero behavior change)."""
rows = _rows()
assert _filter_competitors_by_dev(rows, None) == rows
def test_filter_empty_string_is_noop() -> None:
"""Пустая строка / только запятые → ничего не исключаем."""
rows = _rows()
assert _filter_competitors_by_dev(rows, "") == rows
assert _filter_competitors_by_dev(rows, " , ,") == rows
def test_filter_single_dev() -> None:
"""exclude_dev=Практика → строки этого застройщика исключены, остальные есть."""
kept = _filter_competitors_by_dev(_rows(), "Практика")
names = [r["dev_name"] for r in kept]
assert "Практика" not in names
assert "Брусника" in names
assert "Атлас" in names
def test_filter_case_insensitive() -> None:
"""Регистронезависимо: 'практика' / 'ПРАКТИКА' исключают 'Практика'."""
for variant in ("практика", "ПРАКТИКА", "ПрАкТиКа"):
kept = _filter_competitors_by_dev(_rows(), variant)
assert "Практика" not in [r["dev_name"] for r in kept]
def test_filter_multi_dev() -> None:
"""exclude_dev=Практика,Брусника → обе исключены, прочие остаются."""
kept = _filter_competitors_by_dev(_rows(), "Практика,Брусника")
names = [r["dev_name"] for r in kept]
assert "Практика" not in names
assert "Брусника" not in names
assert "Атлас" in names
assert None in names # dev_name=None не исключается
def test_filter_none_dev_name_not_excluded() -> None:
"""Строка с dev_name=None не падает и не исключается даже при фильтре."""
kept = _filter_competitors_by_dev(_rows(), "Практика")
assert any(r["dev_name"] is None for r in kept)
def test_filter_whitespace_in_csv() -> None:
"""Пробелы вокруг имён в CSV тримятся."""
kept = _filter_competitors_by_dev(_rows(), " Практика , Брусника ")
names = [r["dev_name"] for r in kept]
assert "Практика" not in names
assert "Брусника" not in names
# ── Endpoint smoke: mock DB + patched render ──────────────────────────────────
def _parcel_row() -> MagicMock:
r = MagicMock()
r.__getitem__ = lambda self, k: {
"address": "ул. Тестовая, 1",
"area_m2": 5000.0,
"land_category": "земли населённых пунктов",
"vri": "многоэтажная застройка",
"cadastral_cost": 10_000_000.0,
"last_update": None,
}[k]
return r
def _geom_row() -> MagicMock:
r = MagicMock()
r.__getitem__ = lambda self, k: {"wkt": "POINT(60.605 56.838)"}[k]
return r
def _comp_mapping(dev_name: str | None, obj_id: int) -> dict[str, Any]:
return {
"obj_id": obj_id,
"comm_name": f"ЖК-{obj_id}",
"dev_name": dev_name,
"obj_class": "comfort",
"flat_count": 100,
"distance_m": 100.0 * obj_id,
}
def _make_snapshot_db() -> MagicMock:
"""Порядок execute в parcel_snapshot_pdf:
1. parcel_row → .mappings().first()
2. geom → .mappings().first()
3. poi → .mappings().all()
4. competitors→ .mappings().all()
5. district → .mappings().first()
"""
db = MagicMock()
res_parcel = MagicMock()
res_parcel.mappings.return_value.first.return_value = _parcel_row()
res_geom = MagicMock()
res_geom.mappings.return_value.first.return_value = _geom_row()
res_poi = MagicMock()
res_poi.mappings.return_value.all.return_value = []
res_comp = MagicMock()
res_comp.mappings.return_value.all.return_value = [
_comp_mapping("Практика", 1),
_comp_mapping("Брусника", 2),
]
res_district = MagicMock()
res_district.mappings.return_value.first.return_value = None
db.execute.side_effect = [res_parcel, res_geom, res_poi, res_comp, res_district]
return db
def _override_db(db: MagicMock):
def _get_db_override():
yield db
return _get_db_override
def test_snapshot_endpoint_exclude_dev_filters_render_input(monkeypatch) -> None:
"""С exclude_dev=Практика конкурент с dev_name='Практика' не доходит до рендера."""
captured: dict[str, Any] = {}
def _fake_render(**kwargs: Any) -> bytes:
captured["competitor_rows"] = kwargs.get("competitor_rows")
return b"%PDF-1.4 fake"
monkeypatch.setattr("app.api.v1.parcels.generate_snapshot_pdf", _fake_render)
from app.core.db import get_db
app.dependency_overrides[get_db] = _override_db(_make_snapshot_db())
try:
client = TestClient(app)
resp = client.get("/api/v1/parcels/66:41:0303161:5/snapshot.pdf?exclude_dev=Практика")
assert resp.status_code == 200, resp.text
devs = [r["dev_name"] for r in captured["competitor_rows"]]
assert "Практика" not in devs
assert "Брусника" in devs
finally:
app.dependency_overrides.clear()
def test_snapshot_endpoint_no_param_keeps_all(monkeypatch) -> None:
"""Без exclude_dev все конкуренты доходят до рендера (baseline)."""
captured: dict[str, Any] = {}
def _fake_render(**kwargs: Any) -> bytes:
captured["competitor_rows"] = kwargs.get("competitor_rows")
return b"%PDF-1.4 fake"
monkeypatch.setattr("app.api.v1.parcels.generate_snapshot_pdf", _fake_render)
from app.core.db import get_db
app.dependency_overrides[get_db] = _override_db(_make_snapshot_db())
try:
client = TestClient(app)
resp = client.get("/api/v1/parcels/66:41:0303161:5/snapshot.pdf")
assert resp.status_code == 200, resp.text
devs = [r["dev_name"] for r in captured["competitor_rows"]]
assert "Практика" in devs
assert "Брусника" in devs
finally:
app.dependency_overrides.clear()