gendesign/backend/tests/test_admin_weight_profiles.py
lekss361 c8e510cbbe feat(site-finder): weight profiles endpoints + analyze integration (#114 sub-PR 3/4)
Admin CRUD router + non-breaking analyze_parcel integration. 46 tests pass.

Refs: #114
2026-05-15 00:34:33 +03:00

341 lines
12 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.

"""Tests для admin weight profiles endpoints.
Покрывает:
- GET /api/v1/admin/site-finder/weight-profiles → 200 + list
- POST → 201 + created profile
- GET /{id} → 200 / 404
- PUT /{id} → 200 / 404
- DELETE /{id} → 204 / 404
- 401 при отсутствии X-Admin-Token
- 422 при невалидных weights (неизвестная категория, вес вне диапазона)
Mock-based: get_db переопределяется через dependency override.
"""
from __future__ import annotations
from datetime import UTC, datetime
from unittest.mock import MagicMock
import pytest
from fastapi.testclient import TestClient
from app.core.db import get_db
from app.main import app
from app.services.site_finder.weight_profiles import WeightProfile
_ADMIN_TOKEN = "test-admin-token"
_HEADERS = {"X-Admin-Token": _ADMIN_TOKEN}
_NOW = datetime.now(UTC)
def _make_profile(
profile_id: int = 1,
user_id: str = "user-1",
profile_name: str = "Test",
weights: dict | None = None,
is_default: bool = False,
) -> WeightProfile:
return WeightProfile(
id=profile_id,
user_id=user_id,
profile_name=profile_name,
weights=weights or {"school": 1.5, "park": 1.8},
is_default=is_default,
description=None,
created_at=_NOW,
updated_at=_NOW,
)
@pytest.fixture()
def client_with_token(monkeypatch: pytest.MonkeyPatch) -> TestClient:
"""TestClient с переопределённым SCRAPE_ADMIN_TOKEN."""
monkeypatch.setattr("app.core.config.settings.scrape_admin_token", _ADMIN_TOKEN)
return TestClient(app)
@pytest.fixture()
def mock_db() -> MagicMock:
return MagicMock()
def _override_db(mock: MagicMock):
def _get_db_override():
yield mock
app.dependency_overrides[get_db] = _get_db_override
return mock
def _clear_overrides():
app.dependency_overrides.pop(get_db, None)
# ── GET list ───────────────────────────────────────────────────────────────────
def test_list_empty(client_with_token: TestClient, monkeypatch: pytest.MonkeyPatch) -> None:
"""GET ?user_id= → 200 + пустой список."""
mock = MagicMock()
_override_db(mock)
try:
monkeypatch.setattr(
"app.api.v1.admin_weight_profiles.list_profiles",
lambda db, user_id: [],
)
r = client_with_token.get(
"/api/v1/admin/site-finder/weight-profiles",
params={"user_id": "user-x"},
headers=_HEADERS,
)
assert r.status_code == 200
assert r.json() == []
finally:
_clear_overrides()
def test_list_returns_profiles(
client_with_token: TestClient, monkeypatch: pytest.MonkeyPatch
) -> None:
"""GET ?user_id= → 200 + список профилей."""
profiles = [_make_profile(1, is_default=True), _make_profile(2, profile_name="B")]
mock = MagicMock()
_override_db(mock)
try:
monkeypatch.setattr(
"app.api.v1.admin_weight_profiles.list_profiles",
lambda db, user_id: profiles,
)
r = client_with_token.get(
"/api/v1/admin/site-finder/weight-profiles",
params={"user_id": "user-1"},
headers=_HEADERS,
)
assert r.status_code == 200
body = r.json()
assert len(body) == 2
assert body[0]["id"] == 1
finally:
_clear_overrides()
# ── POST create ────────────────────────────────────────────────────────────────
def test_create_then_get(client_with_token: TestClient, monkeypatch: pytest.MonkeyPatch) -> None:
"""POST создаёт профиль, возвращает его со статусом 201."""
created = _make_profile(42, profile_name="Семейный", weights={"school": 2.0, "park": 1.5})
mock = MagicMock()
_override_db(mock)
try:
monkeypatch.setattr(
"app.api.v1.admin_weight_profiles.create_profile",
lambda db, payload: created,
)
r = client_with_token.post(
"/api/v1/admin/site-finder/weight-profiles",
json={
"user_id": "user-1",
"profile_name": "Семейный",
"weights": {"school": 2.0, "park": 1.5},
"is_default": False,
},
headers=_HEADERS,
)
assert r.status_code == 201
body = r.json()
assert body["id"] == 42
assert body["profile_name"] == "Семейный"
assert body["weights"]["school"] == 2.0
finally:
_clear_overrides()
def test_create_validation_unknown_category(client_with_token: TestClient) -> None:
"""POST с неизвестной POI-категорией → 422 (Pydantic validation)."""
r = client_with_token.post(
"/api/v1/admin/site-finder/weight-profiles",
json={
"user_id": "user-1",
"profile_name": "Bad",
"weights": {"supermarket": 1.0}, # не в ALLOWED_CATEGORIES
},
headers=_HEADERS,
)
assert r.status_code == 422
def test_create_validation_weight_out_of_bounds(client_with_token: TestClient) -> None:
"""POST с весом вне [-2, 3] → 422."""
r = client_with_token.post(
"/api/v1/admin/site-finder/weight-profiles",
json={
"user_id": "user-1",
"profile_name": "Bad",
"weights": {"school": 99.0},
},
headers=_HEADERS,
)
assert r.status_code == 422
# ── GET one ────────────────────────────────────────────────────────────────────
def test_get_profile_found(client_with_token: TestClient, monkeypatch: pytest.MonkeyPatch) -> None:
"""GET /{id}?user_id= → 200."""
profile = _make_profile(7)
mock = MagicMock()
_override_db(mock)
try:
monkeypatch.setattr(
"app.api.v1.admin_weight_profiles.get_profile",
lambda db, user_id, profile_id: profile,
)
r = client_with_token.get(
"/api/v1/admin/site-finder/weight-profiles/7",
params={"user_id": "user-1"},
headers=_HEADERS,
)
assert r.status_code == 200
assert r.json()["id"] == 7
finally:
_clear_overrides()
def test_get_profile_not_found(
client_with_token: TestClient, monkeypatch: pytest.MonkeyPatch
) -> None:
"""GET /{id} несуществующего профиля → 404."""
mock = MagicMock()
_override_db(mock)
try:
monkeypatch.setattr(
"app.api.v1.admin_weight_profiles.get_profile",
lambda db, user_id, profile_id: None,
)
r = client_with_token.get(
"/api/v1/admin/site-finder/weight-profiles/999",
params={"user_id": "user-1"},
headers=_HEADERS,
)
assert r.status_code == 404
finally:
_clear_overrides()
# ── PUT update ─────────────────────────────────────────────────────────────────
def test_update_profile(client_with_token: TestClient, monkeypatch: pytest.MonkeyPatch) -> None:
"""PUT /{id} → 200 + обновлённый профиль."""
updated = _make_profile(3, profile_name="Обновлённый")
mock = MagicMock()
_override_db(mock)
try:
monkeypatch.setattr(
"app.api.v1.admin_weight_profiles.update_profile",
lambda db, user_id, profile_id, payload: updated,
)
r = client_with_token.put(
"/api/v1/admin/site-finder/weight-profiles/3",
params={"user_id": "user-1"},
json={"profile_name": "Обновлённый"},
headers=_HEADERS,
)
assert r.status_code == 200
assert r.json()["profile_name"] == "Обновлённый"
finally:
_clear_overrides()
def test_update_profile_not_found(
client_with_token: TestClient, monkeypatch: pytest.MonkeyPatch
) -> None:
"""PUT /{id} несуществующего → 404."""
mock = MagicMock()
_override_db(mock)
try:
monkeypatch.setattr(
"app.api.v1.admin_weight_profiles.update_profile",
lambda db, user_id, profile_id, payload: None,
)
r = client_with_token.put(
"/api/v1/admin/site-finder/weight-profiles/999",
params={"user_id": "user-1"},
json={"profile_name": "X"},
headers=_HEADERS,
)
assert r.status_code == 404
finally:
_clear_overrides()
# ── DELETE ─────────────────────────────────────────────────────────────────────
def test_delete_success(client_with_token: TestClient, monkeypatch: pytest.MonkeyPatch) -> None:
"""DELETE /{id} → 204."""
mock = MagicMock()
_override_db(mock)
try:
monkeypatch.setattr(
"app.api.v1.admin_weight_profiles.delete_profile",
lambda db, user_id, profile_id: True,
)
r = client_with_token.delete(
"/api/v1/admin/site-finder/weight-profiles/5",
params={"user_id": "user-1"},
headers=_HEADERS,
)
assert r.status_code == 204
finally:
_clear_overrides()
def test_delete_not_found(client_with_token: TestClient, monkeypatch: pytest.MonkeyPatch) -> None:
"""DELETE /{id} несуществующего → 404."""
mock = MagicMock()
_override_db(mock)
try:
monkeypatch.setattr(
"app.api.v1.admin_weight_profiles.delete_profile",
lambda db, user_id, profile_id: False,
)
r = client_with_token.delete(
"/api/v1/admin/site-finder/weight-profiles/999",
params={"user_id": "user-1"},
headers=_HEADERS,
)
assert r.status_code == 404
finally:
_clear_overrides()
# ── Auth ───────────────────────────────────────────────────────────────────────
def test_unauthorized_no_token(monkeypatch: pytest.MonkeyPatch) -> None:
"""Запрос без X-Admin-Token → 401 (или 503 если токен не задан в settings)."""
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
def test_unauthorized_wrong_token(monkeypatch: pytest.MonkeyPatch) -> None:
"""Неверный X-Admin-Token → 401."""
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