"""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