"""Тесты парсера реестра КРТ-площадок ЕКБ (#1060). Все тесты — без живой сети. """ from __future__ import annotations import textwrap import pytest from app.services.scrapers.ekb_krt_registry import parse_krt_page # ── Фикстура: реальный фрагмент блока площадки 2 (Донбасская — Брусника) ──── # Вставлен из krt_page_raw.html (beksburg.rf/дляработы/гиз/крт). # Охватывает обе цены аукциона, договор, застройщика, 2 постановления ПАГЕ. _BRUSNIKA_BLOCK = textwrap.dedent( """\

2022 год

 2. КРТ жилой застройки в квартале улицы Донбасской – \ переулков Черниговского – Суворовского:

Площадь территории: 2,79 га;
Градостроительный потенциал: 76 650 кв. м;
\ Всего многоквартирных домов: 10;
\ 3. Издано Постановление Администрации города Екатеринбурга от 10.03.2022 № 621 \ «О комплексном развитии территории жилой застройки в квартале улицы Донбасской»;\
\ 4. Постановление Администрации города Екатеринбурга от 19.05.2022 № 1334 \ «О проведении аукциона»;
\ 5. Аукцион:
Начальная стоимость: 641 018,64 рублей;
\ Цена по результатам торгов: 671 018 рублей.
\ 6. Заключен договор о комплексном развитии территории жилой застройки от 10.08.2022 \ № 1-2/21.7-14 (застройщик – ООО «Брусника. Специализированный застройщик»).\
\ 7. Постановление Администрации города Екатеринбурга от 28.12.2022 № 4051 \ «Об утверждении проекта планировки территории».

Стадия реализации: строительство

""" ) class TestParseBrusnikaBlock: """Тесты парсинга блока площадки Донбасская (Брусника).""" @pytest.fixture(scope="class") def site(self): sites = parse_krt_page(_BRUSNIKA_BLOCK) assert sites, "parse_krt_page вернул пустой список" return sites[0] def test_krt_kind(self, site): assert site.krt_kind == "жилой застройки" def test_developer(self, site): assert site.developer_name is not None assert "Брусника" in site.developer_name def test_auction_start_price(self, site): """641 018,64 → 641018.64""" assert site.auction_start_price == pytest.approx(641018.64, rel=1e-4) def test_auction_final_price(self, site): """671 018 → 671018.0""" assert site.auction_final_price == pytest.approx(671018.0, rel=1e-4) def test_contract_number(self, site): assert site.contract_number == "1-2/21.7-14" def test_contract_date(self, site): from datetime import date assert site.contract_date == date(2022, 8, 10) def test_paga_decisions_at_least_one(self, site): assert len(site.paga_decisions) >= 1 # первое постановление — 10.03.2022 № 621 dates = [d["date"] for d in site.paga_decisions] assert "10.03.2022" in dates def test_area_ha(self, site): assert site.area_ha == pytest.approx(2.79, rel=1e-3) def test_gradpotential(self, site): # «76 650 кв. м» → 76650.0 assert site.gradpotential_sqm == pytest.approx(76650.0, rel=1e-3) def test_mkd_count(self, site): assert site.mkd_count == 10 def test_site_name_contains_number(self, site): assert "2." in site.site_name assert "Донбасской" in site.site_name def test_raw_block_nonempty(self, site): assert len(site.raw_block) > 50 class TestNumericNormalization: """Тесты числовой нормализации (NBSP, запятая как десятичный разделитель).""" def test_nbsp_price(self): """«641\xa0018,64» → 641018.64""" from app.services.scrapers.ekb_krt_registry import _normalize_num assert _normalize_num("641\xa0018,64") == pytest.approx(641018.64, rel=1e-4) def test_space_price(self): """«671 018» → 671018.0""" from app.services.scrapers.ekb_krt_registry import _normalize_num assert _normalize_num("671 018") == pytest.approx(671018.0) def test_plain_decimal(self): from app.services.scrapers.ekb_krt_registry import _normalize_num assert _normalize_num("3,65") == pytest.approx(3.65) def test_invalid_returns_none(self): from app.services.scrapers.ekb_krt_registry import _normalize_num assert _normalize_num("нет данных") is None class TestEmptyAndGarbage: """Пустой / мусорный HTML → [].""" def test_empty_string(self): assert parse_krt_page("") == [] def test_whitespace_only(self): assert parse_krt_page(" \n\t ") == [] def test_irrelevant_html(self): assert parse_krt_page("

Привет

") == [] def test_no_krt_section(self): html = "

Другой раздел

Текст

" assert parse_krt_page(html) == [] # ── Синтетический блок: площадка без застройщика / договора / цен ───────────── # Имитирует площадки «в работе» (только geography + технические параметры). # Не требует локального дампа — всегда исполняется в CI. _BABUSHKINA_BLOCK = textwrap.dedent( """\

2024 год

 1. КРТ жилой застройки в районе ул. Бабушкина:

Площадь территории: 5,10 га;
\ Градостроительный потенциал: 120 000 кв. м;
\ Всего многоквартирных домов: 18;
\ 3. Издано Постановление Администрации города Екатеринбурга от 15.01.2024 № 42 \ «О комплексном развитии территории».

Стадия реализации: проектирование

""" ) class TestParseBabushkinaBlock: """Синтетический тест: площадка без застройщика, договора и цен аукциона. Не зависит от локального дампа — всегда запускается в CI. """ @pytest.fixture(scope="class") def site(self): sites = parse_krt_page(_BABUSHKINA_BLOCK) assert sites, "parse_krt_page вернул пустой список для синтетического блока" return sites[0] def test_parses_exactly_one_site(self): """Синтетический блок содержит ровно одну площадку.""" assert len(parse_krt_page(_BABUSHKINA_BLOCK)) == 1 def test_developer_is_none(self, site): """Застройщик не указан → None.""" assert site.developer_name is None def test_contract_number_is_none(self, site): """Договор не заключён → None.""" assert site.contract_number is None def test_area_ha_filled(self, site): """Площадь территории заполнена.""" assert site.area_ha == pytest.approx(5.10, rel=1e-3) def test_gradpotential_filled(self, site): """Градостроительный потенциал заполнен.""" assert site.gradpotential_sqm == pytest.approx(120000.0, rel=1e-3) def test_district_filled(self, site): """Район извлечён из названия блока.""" assert site.district is not None assert "Бабушкина" in site.district def test_krt_kind(self, site): assert site.krt_kind == "жилой застройки" class TestMultiSiteCountFromDump: """Проверка количества площадок из реального дампа страницы.""" @pytest.fixture(scope="class") def dump_html(self): """Загружает локальный дамп страницы. Тест пропускается если файл отсутствует (CI без дампа). """ import os dump_path = os.path.join( os.path.dirname(__file__), "../../../../.playwright-mcp/ekb-docs/krt_page_raw.html", ) if not os.path.exists(dump_path): pytest.skip("krt_page_raw.html dump not found — skip real-dump test") with open(dump_path, encoding="utf-8") as fh: return fh.read() def test_site_count_from_dump(self, dump_html): """Из дампа должно распарситься ≥ 14 площадок (реальных на странице 15).""" sites = parse_krt_page(dump_html) assert len(sites) >= 14, f"Expected ≥ 14 sites, got {len(sites)}" def test_no_duplicate_site_names(self, dump_html): sites = parse_krt_page(dump_html) names = [s.site_name for s in sites] assert len(names) == len(set(names)), "Дубликаты site_name в результате"