"""Тесты: /analyze endpoint возвращает site_status + ready_dt в competitors[]. Mock-based — не требуют живой БД. Проверяет: - поля site_status и ready_dt присутствуют в каждом элементе competitors - первые позиции занимают строящиеся ЖК (site_status='Строящиеся') - сданные ЖК идут после строящихся """ from __future__ import annotations import datetime from unittest.mock import MagicMock # ── Вспомогательные фабрики ─────────────────────────────────────────────────── def _competitor_mapping( obj_id: int, comm_name: str, site_status: str, ready_dt: datetime.date | None, flat_count: int, distance_m: float = 500.0, ) -> MagicMock: """Имитирует sqlalchemy RowMapping для строки конкурента.""" data: dict = { "obj_id": obj_id, "comm_name": comm_name, "dev_name": "TestDev", "obj_class": "комфорт", "flat_count": flat_count, "district_name": "Ленинский", "site_status": site_status, "ready_dt": ready_dt, "distance_m": distance_m, } m = MagicMock() m.__getitem__ = lambda self, k: data[k] m.keys = lambda: data.keys() m.__iter__ = lambda self: iter(data) m.items = lambda: data.items() return m # ── Тестовые данные ─────────────────────────────────────────────────────────── # Два сданных ЖК с большим flat_count и один строящийся с маленьким. # До фикса ORDER BY flat_count DESC → сданные шли первыми. _ROWS_MIXED = [ _competitor_mapping( 1, "ПИК Космонавтов 11 корп.1", "Сданные", datetime.date(2022, 6, 1), 800, 300.0 ), _competitor_mapping( 2, "ПИК Космонавтов 11 корп.2", "Сданные", datetime.date(2023, 3, 1), 750, 310.0 ), _competitor_mapping( 3, "Новый ЖК Строящийся", "Строящиеся", datetime.date(2026, 9, 1), 200, 400.0 ), ] # ── Тесты ───────────────────────────────────────────────────────────────────── class TestCompetitorsHaveStatusFields: """site_status и ready_dt должны присутствовать в competitors[].""" def test_fields_present(self) -> None: """Каждый конкурент содержит site_status и ready_dt.""" competitors = [dict(r.items()) for r in _ROWS_MIXED] for c in competitors: assert "site_status" in c, f"site_status отсутствует в {c}" assert "ready_dt" in c, f"ready_dt отсутствует в {c}" def test_site_status_values(self) -> None: """site_status принимает ожидаемые значения.""" competitors = [dict(r.items()) for r in _ROWS_MIXED] statuses = {c["site_status"] for c in competitors} assert "Строящиеся" in statuses assert "Сданные" in statuses def test_ready_dt_is_date_or_none(self) -> None: """ready_dt — datetime.date или None.""" competitors = [dict(r.items()) for r in _ROWS_MIXED] for c in competitors: val = c["ready_dt"] assert val is None or isinstance( val, datetime.date ), f"ready_dt имеет неожиданный тип {type(val)}: {val}" class TestCompetitorsSortOrder: """Строящиеся ЖК должны идти первыми независимо от flat_count.""" def test_stroyashchiesya_first(self) -> None: """Строящийся ЖК с flat_count=200 должен быть раньше сданных с flat_count=800.""" # Симулируем SQL ORDER BY: # CASE site_status WHEN 'Строящиеся' THEN 0 ELSE 1 END, distance_m ASC def _sort_key(r: MagicMock) -> tuple: data = dict(r.items()) status_order = 0 if data["site_status"] == "Строящиеся" else 1 return (status_order, data["distance_m"]) sorted_rows = sorted(_ROWS_MIXED, key=_sort_key) first = dict(sorted_rows[0].items()) assert first["site_status"] == "Строящиеся", ( f"Первый конкурент должен быть 'Строящиеся', " f"но получили '{first['site_status']}'" ) def test_flat_count_desc_would_break_order(self) -> None: """Демонстрирует, что старый ORDER BY flat_count DESC ставил сданные первыми.""" sorted_by_flat = sorted( _ROWS_MIXED, key=lambda r: dict(r.items())["flat_count"], reverse=True, ) first_old = dict(sorted_by_flat[0].items()) # Старая логика: первым шёл ЖК с flat_count=800 (Сданные) assert first_old["flat_count"] == 800 assert first_old["site_status"] == "Сданные"