228 lines
10 KiB
Python
228 lines
10 KiB
Python
"""Тесты парсера реестра КРТ-площадок ЕКБ (#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(
|
||
"""\
|
||
<h2><strong>2022 год</strong></h2>
|
||
<p> <strong>2. КРТ жилой застройки в квартале улицы Донбасской – \
|
||
переулков Черниговского – Суворовского:</strong></p>
|
||
<p><strong><img class="js-has-preview img-float-left" \
|
||
title="2.	КРТ жилой застройки" src="/media/gallery/fe.jpg" width="100%" /></strong></p>
|
||
<p>Площадь территории: 2,79 га;<br />Градостроительный потенциал: 76 650 кв. м;<br />\
|
||
Всего многоквартирных домов: 10;<br />\
|
||
3. Издано Постановление Администрации города Екатеринбурга от 10.03.2022 № 621 \
|
||
«О комплексном развитии территории жилой застройки в квартале улицы Донбасской»;\
|
||
<br />\
|
||
4. Постановление Администрации города Екатеринбурга от 19.05.2022 № 1334 \
|
||
«О проведении аукциона»;<br />\
|
||
5. Аукцион:<br />Начальная стоимость: 641 018,64 рублей;<br />\
|
||
Цена по результатам торгов: 671 018 рублей.<br />\
|
||
6. Заключен договор о комплексном развитии территории жилой застройки от 10.08.2022 \
|
||
№ 1-2/21.7-14 (застройщик – ООО «Брусника. Специализированный застройщик»).\
|
||
<br />\
|
||
7. Постановление Администрации города Екатеринбурга от 28.12.2022 № 4051 \
|
||
«Об утверждении проекта планировки территории».</p>
|
||
<p><strong>Стадия реализации: строительство</strong></p>
|
||
"""
|
||
)
|
||
|
||
|
||
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("<html><body><p>Привет</p></body></html>") == []
|
||
|
||
def test_no_krt_section(self):
|
||
html = "<html><body><h2>Другой раздел</h2><p>Текст</p></body></html>"
|
||
assert parse_krt_page(html) == []
|
||
|
||
|
||
# ── Синтетический блок: площадка без застройщика / договора / цен ─────────────
|
||
# Имитирует площадки «в работе» (только geography + технические параметры).
|
||
# Не требует локального дампа — всегда исполняется в CI.
|
||
_BABUSHKINA_BLOCK = textwrap.dedent(
|
||
"""\
|
||
<h2><strong>2024 год</strong></h2>
|
||
<p> <strong>1. КРТ жилой застройки в районе ул. Бабушкина:</strong></p>
|
||
<p>Площадь территории: 5,10 га;<br />\
|
||
Градостроительный потенциал: 120 000 кв. м;<br />\
|
||
Всего многоквартирных домов: 18;<br />\
|
||
3. Издано Постановление Администрации города Екатеринбурга от 15.01.2024 № 42 \
|
||
«О комплексном развитии территории».</p>
|
||
<p><strong>Стадия реализации: проектирование</strong></p>
|
||
"""
|
||
)
|
||
|
||
|
||
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 в результате"
|