gendesign/tradein-mvp/backend/tests/test_repair_state_normalizer.py
bot-backend 2d3ca78b0b
All checks were successful
CI / changes (pull_request) Successful in 5s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
fix(scrapers): cian_detail — ceiling/repair актуальные пути state (#1791)
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.
2026-06-19 19:57:18 +03:00

188 lines
8.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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