gendesign/backend/tests/test_weight_profiles.py
lekss361 5aea78c2d8
feat(site-finder): weight profiles service — Pydantic + CRUD (#114 sub-PR 2/4) (#137)
Per #114 sub-PR 2/4. Pydantic v2 models + CRUD service. 14 passing tests.

API: list/get/get_default/create/update/delete_profile + resolve_weights
fallback to system defaults.

Validation: ALLOWED_CATEGORIES guard + weight bounds [-2.0, 3.0].
Default-uniqueness в одной transaction (job_settings.py pattern).

Vault: code/modules/Module_Weight_Profiles_Service.md.

Refs: #114

Co-authored-by: lekss361 <claudestars@proton.me>
2026-05-15 00:29:37 +03:00

215 lines
7.7 KiB
Python
Raw Permalink 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.

"""Unit tests для WeightProfile Pydantic models + resolve_weights.
Mock-based — без реальной БД. Проверяет:
- WeightProfileCreate: отклонение неизвестных категорий
- WeightProfileCreate: отклонение весов за пределами MIN/MAX
- WeightProfileCreate: корректные веса проходят валидацию
- WeightProfileUpdate: weights=None не вызывает ошибку
- resolve_weights: нет user_id и profile_id → системные дефолты
- resolve_weights: user_id задан, default-профиль есть → его веса
- resolve_weights: profile_id задан → его веса
- resolve_weights: профиль не найден → системные дефолты (fallback)
"""
from __future__ import annotations
from unittest.mock import MagicMock
import pytest
from pydantic import ValidationError
from app.services.site_finder.weight_profiles import (
_SYSTEM_POI_WEIGHTS,
WeightProfile,
WeightProfileCreate,
WeightProfileUpdate,
resolve_weights,
)
# ── Validation tests ───────────────────────────────────────────────────────────
def test_validate_unknown_category() -> None:
"""Неизвестная POI-категория → ValidationError."""
with pytest.raises(ValidationError, match="Unknown POI categories"):
WeightProfileCreate(
user_id="user-1",
profile_name="test",
weights={"unknown": 1.0},
)
def test_validate_weight_bounds_too_high() -> None:
"""Вес > MAX_WEIGHT → ValidationError."""
with pytest.raises(ValidationError, match="out of bounds"):
WeightProfileCreate(
user_id="user-1",
profile_name="test",
weights={"school": 99.0},
)
def test_validate_weight_negative_too_low() -> None:
"""Вес < MIN_WEIGHT → ValidationError."""
with pytest.raises(ValidationError, match="out of bounds"):
WeightProfileCreate(
user_id="user-1",
profile_name="test",
weights={"tram_stop": -3.0},
)
def test_validate_valid_weights() -> None:
"""Корректные веса проходят валидацию без ошибок."""
profile = WeightProfileCreate(
user_id="user-1",
profile_name="my profile",
weights={"school": 1.5, "tram_stop": -0.5, "park": 2.9},
)
assert profile.weights["school"] == 1.5
assert profile.weights["tram_stop"] == -0.5
def test_validate_empty_weights() -> None:
"""Пустой dict weights допустим (используются системные дефолты)."""
profile = WeightProfileCreate(
user_id="user-1",
profile_name="empty",
weights={},
)
assert profile.weights == {}
def test_validate_boundary_values() -> None:
"""Граничные значения MIN_WEIGHT и MAX_WEIGHT включительно — допустимы."""
profile = WeightProfileCreate(
user_id="user-1",
profile_name="boundary",
weights={"school": -2.0, "park": 3.0},
)
assert profile.weights["school"] == -2.0
assert profile.weights["park"] == 3.0
def test_validate_update_weights_none() -> None:
"""WeightProfileUpdate с weights=None не вызывает ошибку валидации."""
upd = WeightProfileUpdate(weights=None)
assert upd.weights is None
def test_validate_update_unknown_category() -> None:
"""WeightProfileUpdate с неизвестной категорией → ValidationError."""
with pytest.raises(ValidationError, match="Unknown POI categories"):
WeightProfileUpdate(weights={"bogus_category": 1.0})
# ── resolve_weights tests ──────────────────────────────────────────────────────
def test_resolve_weights_system_default() -> None:
"""Оба аргумента None → возвращаются системные веса."""
db = MagicMock()
result = resolve_weights(db, user_id=None, profile_id=None)
assert result == _SYSTEM_POI_WEIGHTS
# db не должен вызываться вообще
db.execute.assert_not_called()
def test_resolve_weights_system_default_returns_copy() -> None:
"""Возвращается копия словаря, не ссылка на _SYSTEM_POI_WEIGHTS."""
db = MagicMock()
result = resolve_weights(db, user_id=None, profile_id=None)
result["school"] = 999.0
# Оригинал не изменён
assert _SYSTEM_POI_WEIGHTS["school"] == 1.5
def _make_profile_mock(weights: dict[str, float]) -> WeightProfile:
"""Вспомогательная функция: создать WeightProfile с заданными weights."""
from datetime import UTC, datetime
return WeightProfile(
id=1,
user_id="user-1",
profile_name="test",
weights=weights,
is_default=True,
description=None,
created_at=datetime.now(UTC),
updated_at=datetime.now(UTC),
)
def test_resolve_weights_uses_default_profile() -> None:
"""user_id задан, default-профиль найден → его веса."""
custom_weights = {"school": 2.0, "park": 0.5}
profile = _make_profile_mock(custom_weights)
db = MagicMock()
import app.services.site_finder.weight_profiles as wp_module
original = wp_module.get_default_profile
wp_module.get_default_profile = lambda _db, uid: profile
try:
result = resolve_weights(db, user_id="user-1", profile_id=None)
finally:
wp_module.get_default_profile = original
assert result == custom_weights
def test_resolve_weights_uses_specific_profile() -> None:
"""profile_id задан → веса конкретного профиля."""
custom_weights = {"kindergarten": 1.8, "metro_stop": 1.0}
profile = _make_profile_mock(custom_weights)
db = MagicMock()
import app.services.site_finder.weight_profiles as wp_module
original = wp_module.get_profile
wp_module.get_profile = lambda _db, uid, pid: profile
try:
result = resolve_weights(db, user_id="user-1", profile_id=42)
finally:
wp_module.get_profile = original
assert result == custom_weights
def test_resolve_weights_profile_not_found_fallback() -> None:
"""Профиль не найден (get_profile → None) → системные дефолты."""
db = MagicMock()
import app.services.site_finder.weight_profiles as wp_module
original_get = wp_module.get_profile
original_default = wp_module.get_default_profile
wp_module.get_profile = lambda _db, uid, pid: None
wp_module.get_default_profile = lambda _db, uid: None
try:
result = resolve_weights(db, user_id="user-1", profile_id=999)
finally:
wp_module.get_profile = original_get
wp_module.get_default_profile = original_default
assert result == _SYSTEM_POI_WEIGHTS
def test_resolve_weights_empty_profile_weights_fallback() -> None:
"""Профиль найден но weights пустой → системные дефолты."""
profile = _make_profile_mock({}) # пустые weights
db = MagicMock()
import app.services.site_finder.weight_profiles as wp_module
original_default = wp_module.get_default_profile
wp_module.get_default_profile = lambda _db, uid: profile
try:
result = resolve_weights(db, user_id="user-1", profile_id=None)
finally:
wp_module.get_default_profile = original_default
assert result == _SYSTEM_POI_WEIGHTS