"""Tests for POI weighted score service (B6). Юнит-тесты для чистой функции — без DB. """ from app.services.site_finder.poi_score import ( CATEGORY_WEIGHTS, PoiScoreResponse, _category_weight, compute_poi_weighted_top7, ) # ── unit: _category_weight ───────────────────────────────────────────────────── def test_category_weight_metro(): """Метро имеет наибольший вес из всех категорий.""" metro_w = _category_weight("metro_stop") for cat in CATEGORY_WEIGHTS: if cat != "metro_stop" and cat != "default": assert metro_w >= _category_weight( cat ), f"metro_stop weight {metro_w} должен быть >= {cat} weight {_category_weight(cat)}" def test_category_weight_unknown_returns_default(): w = _category_weight("unknown_category_xyz") assert w == CATEGORY_WEIGHTS["default"] def test_category_weight_all_positive(): """Все веса в CATEGORY_WEIGHTS должны быть положительными (B6 — ranking, не штраф).""" for cat, w in CATEGORY_WEIGHTS.items(): assert w > 0, f"Вес {cat}={w} должен быть > 0" # ── unit: weight formula ratio ───────────────────────────────────────────────── def test_weight_formula_ratio(): """Ближний объект той же категории должен иметь больший вес.""" cat = "school" cw = _category_weight(cat) w_near = (1.0 / (100.0 + 100.0)) * cw # 100м w_far = (1.0 / (1000.0 + 100.0)) * cw # 1000м assert w_near > w_far def test_weight_formula_category_dominates_at_equal_distance(): """При одинаковом расстоянии метро должно быть впереди автобусной остановки.""" dist = 500.0 w_metro = (1.0 / (dist + 100.0)) * _category_weight("metro_stop") w_bus = (1.0 / (dist + 100.0)) * _category_weight("bus_stop") assert w_metro > w_bus # ── unit: compute_poi_weighted_top7 with mock DB ─────────────────────────────── class _MockMappings: def __init__(self, data: list[dict]) -> None: self._data = data def all(self) -> list[dict]: return self._data # type: ignore[return-value] class _MockResult: def __init__(self, data: list[dict]) -> None: self._data = data def mappings(self) -> "_MockMappings": return _MockMappings(self._data) class _MockDb: """Минимальный мок SQLAlchemy Session для тестирования без БД.""" def __init__(self, rows: list[dict]) -> None: self._rows = rows def execute(self, *_args: object, **_kwargs: object) -> _MockResult: return _MockResult(self._rows) def _make_row(name: str, category: str, distance_m: float) -> dict: return { "name": name, "category": category, "tags": {}, "distance_m": distance_m, } def test_top7_returns_at_most_7(): rows = [_make_row(f"POI {i}", "school", float(i * 50)) for i in range(1, 20)] db = _MockDb(rows) result = compute_poi_weighted_top7(db, "66:41:0204016:10", 56.838, 60.605) assert isinstance(result, PoiScoreResponse) assert len(result.top_poi) <= 7 def test_top7_sorted_by_weight_desc(): rows = [ _make_row("Дальняя школа", "school", 1500.0), _make_row("Метро", "metro_stop", 300.0), _make_row("Близкая школа", "school", 100.0), ] db = _MockDb(rows) result = compute_poi_weighted_top7(db, "66:41:0204016:10", 56.838, 60.605) weights = [item.weight for item in result.top_poi] assert weights == sorted(weights, reverse=True), "top_poi должны быть по weight DESC" def test_metro_beats_school_at_equal_distance(): """Метро в 300м должно быть на первом месте перед школой в 300м (равное расстояние).""" rows = [ _make_row("Школа №1", "school", 300.0), _make_row("Метро Чкаловская", "metro_stop", 300.0), ] db = _MockDb(rows) result = compute_poi_weighted_top7(db, "66:41:0204016:10", 56.838, 60.605) assert ( result.top_poi[0].category == "metro_stop" ), "При равном расстоянии метро (category_weight=6.0) должно быть выше школы (5.0)" def test_metro_first_when_close(): """Метро в 50м должно быть на первом месте перед школой в 300м.""" rows = [ _make_row("Школа №1", "school", 300.0), _make_row("Метро Чкаловская", "metro_stop", 50.0), ] db = _MockDb(rows) result = compute_poi_weighted_top7(db, "66:41:0204016:10", 56.838, 60.605) assert result.top_poi[0].category == "metro_stop", ( "Метро (weight=6.0) в 50м должно быть впереди школы (weight=5.0) в 300м — " f"metro_weight={(1/(50+100))*6:.5f} vs school_weight={(1/(300+100))*5:.5f}" ) def test_empty_db_returns_empty_top_poi(): db = _MockDb([]) result = compute_poi_weighted_top7(db, "66:41:0204016:10", 56.838, 60.605) assert result.top_poi == [] assert result.cad_num == "66:41:0204016:10" assert result.radius_m == 2000 def test_address_built_from_tags(): rows = [ { "name": "Магазин", "category": "shop_small", "tags": {"addr:street": "ул. Ленина", "addr:housenumber": "10"}, "distance_m": 200.0, } ] db = _MockDb(rows) result = compute_poi_weighted_top7(db, "test", 56.838, 60.605) assert result.top_poi[0].address == "ул. Ленина, 10" def test_address_none_when_no_tags(): rows = [_make_row("Парк", "park", 400.0)] db = _MockDb(rows) result = compute_poi_weighted_top7(db, "test", 56.838, 60.605) assert result.top_poi[0].address is None