ceiling_height на detail-странице лежит в offer.building.ceilingHeight,
а не offer.ceilingHeight (всегда null) → было 0% покрытия. repairType
часто пустой, структурное значение приходит как offer.decoration
('preFine'/...) — читаем оба + добавлен preFine->needs_repair и
case-insensitive lookup в нормализаторе. kitchen/description пути уже
верны (offer.kitchenArea/offer.description) — низкое покрытие = мёртвый
CIAN_PROXY_URL, не маппинг. Регресс-тест на реальной карточке 330982715.
188 lines
8.7 KiB
Python
188 lines
8.7 KiB
Python
"""Unit-тесты для repair_state_normalizer (#621, #622).
|
||
|
||
Покрывают:
|
||
- normalize_repair_state(): все known raw → enum, idempotency, None-guard (#621).
|
||
- infer_repair_state_from_text(): русские фразы → enum, приоритет сигналов,
|
||
unknown → None, None-guard (#622).
|
||
"""
|
||
|
||
import logging
|
||
|
||
import pytest
|
||
|
||
from app.services.scrapers.repair_state_normalizer import (
|
||
infer_repair_state_from_text,
|
||
normalize_repair_state,
|
||
)
|
||
|
||
# ── needs_repair ─────────────────────────────────────────────────────────────
|
||
|
||
|
||
@pytest.mark.parametrize("raw", ["required", "without", "no", "rough", "prefine"])
|
||
def test_needs_repair_values(raw: str) -> None:
|
||
assert normalize_repair_state(raw) == "needs_repair"
|
||
|
||
|
||
def test_cian_camelcase_decoration_is_case_insensitive() -> None:
|
||
"""Cian detail отдаёт offer.decoration='preFine' (camelCase) — #1791."""
|
||
assert normalize_repair_state("preFine") == "needs_repair"
|
||
|
||
|
||
# ── standard ─────────────────────────────────────────────────────────────────
|
||
|
||
|
||
def test_cosmetic_maps_to_standard() -> None:
|
||
assert normalize_repair_state("cosmetic") == "standard"
|
||
|
||
|
||
# ── good ─────────────────────────────────────────────────────────────────────
|
||
|
||
|
||
@pytest.mark.parametrize("raw", ["euro", "fine"])
|
||
def test_good_values(raw: str) -> None:
|
||
assert normalize_repair_state(raw) == "good"
|
||
|
||
|
||
# ── excellent ────────────────────────────────────────────────────────────────
|
||
|
||
|
||
@pytest.mark.parametrize("raw", ["designer", "design"])
|
||
def test_excellent_values(raw: str) -> None:
|
||
assert normalize_repair_state(raw) == "excellent"
|
||
|
||
|
||
# ── None guard ───────────────────────────────────────────────────────────────
|
||
|
||
|
||
def test_none_returns_none() -> None:
|
||
assert normalize_repair_state(None) is None
|
||
|
||
|
||
# ── idempotency ──────────────────────────────────────────────────────────────
|
||
|
||
|
||
@pytest.mark.parametrize("enum_val", ["needs_repair", "standard", "good", "excellent"])
|
||
def test_already_normalized_is_passthrough(enum_val: str) -> None:
|
||
"""Уже нормализованное значение возвращается as-is (idempotent при ре-обработке)."""
|
||
assert normalize_repair_state(enum_val) == enum_val
|
||
|
||
|
||
# ── unknown value ────────────────────────────────────────────────────────────
|
||
|
||
|
||
def test_unknown_raw_returns_none(caplog: pytest.LogCaptureFixture) -> None:
|
||
"""Нераспознанное значение → None + WARNING в лог."""
|
||
with caplog.at_level(
|
||
logging.WARNING,
|
||
logger="app.services.scrapers.repair_state_normalizer",
|
||
):
|
||
result = normalize_repair_state("whitebox")
|
||
assert result is None
|
||
assert "whitebox" in caplog.text
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# infer_repair_state_from_text() — извлечение из описания (#622)
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
|
||
# ── excellent ────────────────────────────────────────────────────────────────
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"text",
|
||
[
|
||
"Сделан дизайнерский ремонт с авторской мебелью",
|
||
"Квартира с дизайнерской отделкой премиум-класса",
|
||
"Премиальный ремонт от застройщика",
|
||
"Эксклюзивный ремонт по индивидуальному проекту",
|
||
"Авторский ремонт, всё новое",
|
||
],
|
||
)
|
||
def test_infer_excellent(text: str) -> None:
|
||
assert infer_repair_state_from_text(text) == "excellent"
|
||
|
||
|
||
# ── good ─────────────────────────────────────────────────────────────────────
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"text",
|
||
[
|
||
"Свежий евроремонт, заезжай и живи",
|
||
"Сделан евро-ремонт в 2024 году",
|
||
"Современный ремонт, всё в отличном состоянии — ремонт в отличном",
|
||
"Отличный ремонт, продаётся с мебелью",
|
||
"Качественный ремонт от собственника",
|
||
],
|
||
)
|
||
def test_infer_good(text: str) -> None:
|
||
assert infer_repair_state_from_text(text) == "good"
|
||
|
||
|
||
# ── needs_repair ─────────────────────────────────────────────────────────────
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"text",
|
||
[
|
||
"Квартира без отделки, под чистовую отделку",
|
||
"Сдаётся без ремонта, черновая отделка",
|
||
"Черновой ремонт, нужен ремонт",
|
||
"Требует ремонта, торг уместен",
|
||
"Требуется косметический ремонт",
|
||
"Квартира под ремонт, цена снижена",
|
||
"Предчистовая отделка от застройщика",
|
||
],
|
||
)
|
||
def test_infer_needs_repair(text: str) -> None:
|
||
assert infer_repair_state_from_text(text) == "needs_repair"
|
||
|
||
|
||
# ── standard ─────────────────────────────────────────────────────────────────
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"text",
|
||
[
|
||
"Косметический ремонт, чисто и уютно",
|
||
"Сделана косметическая отделка",
|
||
"Обычный ремонт, всё работает",
|
||
"Квартира в жилом состоянии",
|
||
"Хорошее состояние, заезжай и живи",
|
||
],
|
||
)
|
||
def test_infer_standard(text: str) -> None:
|
||
assert infer_repair_state_from_text(text) == "standard"
|
||
|
||
|
||
# ── приоритет сигналов (порядок excellent > good > needs_repair > standard) ───
|
||
|
||
|
||
def test_infer_excellent_beats_needs_repair() -> None:
|
||
"""Дизайнерский ремонт после черновой → excellent, не needs_repair."""
|
||
text = "Была черновая отделка, теперь сделан дизайнерский ремонт"
|
||
assert infer_repair_state_from_text(text) == "excellent"
|
||
|
||
|
||
def test_infer_good_beats_standard() -> None:
|
||
"""Евроремонт + косметический → good (сильнее)."""
|
||
text = "Косметический ремонт обновлён до евроремонта"
|
||
assert infer_repair_state_from_text(text) == "good"
|
||
|
||
|
||
# ── None / unknown guard ─────────────────────────────────────────────────────
|
||
|
||
|
||
def test_infer_none_returns_none() -> None:
|
||
assert infer_repair_state_from_text(None) is None
|
||
|
||
|
||
def test_infer_empty_returns_none() -> None:
|
||
assert infer_repair_state_from_text("") is None
|
||
|
||
|
||
def test_infer_unknown_text_returns_none() -> None:
|
||
"""Текст без repair-сигналов → None (не фабрикуем)."""
|
||
text = "Светлая квартира рядом с метро, развитая инфраструктура, школа во дворе"
|
||
assert infer_repair_state_from_text(text) is None
|