"""Smoke тесты для avito_detail parser. Загружает minimal HTML fixture и проверяет selector + conversion logic без сетевых запросов. """ # ruff: noqa: E501 # HTML-фикстуры содержат длинные строки разметки import pytest from app.services.scrapers.avito_detail import ( DetailEnrichment, _parse_height_m, parse_detail_html, ) MINIMAL_HTML = """
№ 7986882804 · 18 мая в 13:40 · 1899 просмотров (+ 12 сегодня)
11 990 000 ₽
Дом

1 собственник

Последняя смена собственника 5 мая 2020

Не найдены ограничения и обременения

Совпадают площадь, адрес и этаж

Отличная квартира в новом ЖК. Чкаловская 11-15 мин пешком.
""" SOURCE_URL = "https://www.avito.ru/ekaterinburg/kvartiry/test_7986882804" def test_minimal_parse() -> None: result = parse_detail_html(MINIMAL_HTML, SOURCE_URL) assert isinstance(result, DetailEnrichment) assert result.item_id == "7986882804" assert result.price_rub == 11990000 assert result.rooms == 3 assert result.area_m2 == 75.0 assert result.kitchen_area_m2 == 15.0 assert result.floor == 20 assert result.total_floors == 26 assert result.balcony_loggia == "loggia" assert result.repair_state == "good" # «евро» → good (normalized enum, #621) assert result.sale_type == "free" assert result.mortgage_available is True assert result.house_type == "monolith" assert result.total_floors_house == 26 assert result.lat == 56.797713 assert result.lon == 60.609135 assert result.avito_location_id == 654070 assert result.house_catalog_url is not None and "3171365" in result.house_catalog_url assert result.owners_count == 1 assert result.owners_at_least is False assert result.last_owner_change_date is not None and result.last_owner_change_date.year == 2020 assert result.encumbrances_clean is True assert result.registry_match is True assert result.views_total == 1899 assert result.views_today == 12 def test_metro_extraction() -> None: result = parse_detail_html(MINIMAL_HTML, SOURCE_URL) # "Чкаловская 11-15 мин пешком" → metro_stations assert len(result.metro_stations) >= 1 station = result.metro_stations[0] assert station["name"] == "Чкаловская" assert station["min_from"] == 11 assert station["min_to"] == 15 assert station["mode"] == "walk" def test_owners_at_least_false() -> None: """Один собственник (без 'или больше') → owners_at_least=False.""" result = parse_detail_html(MINIMAL_HTML, SOURCE_URL) assert result.owners_count == 1 assert result.owners_at_least is False def test_owners_at_least_true() -> None: """'3 собственника или больше' → owners_at_least=True.""" html = """
№ 1234567890
5 000 000 ₽

3 собственника или больше

""" result = parse_detail_html(html, "https://www.avito.ru/test_1234567890") assert result.owners_count == 3 assert result.owners_at_least is True def test_missing_domoteka_all_none() -> None: """Если нет domoteka-блока — все 5 полей None.""" html = """
№ 9999999999
3 000 000 ₽ """ result = parse_detail_html(html, "https://www.avito.ru/test_9999999999") assert result.owners_count is None assert result.owners_at_least is None assert result.last_owner_change_date is None assert result.encumbrances_clean is None assert result.registry_match is None def test_invalid_html_raises_value_error() -> None: """HTML без item_id → ValueError.""" html = "

Нет айди

" with pytest.raises(ValueError, match="Cannot extract item_id"): parse_detail_html(html, "https://www.avito.ru/test") def test_house_catalog_url_full() -> None: """house_catalog_url должен быть absolute URL.""" result = parse_detail_html(MINIMAL_HTML, SOURCE_URL) assert result.house_catalog_url is not None assert result.house_catalog_url.startswith("https://www.avito.ru") def test_house_catalog_url_secondary_marker_no_nd_jk() -> None: """#871: вторичка без nd-jk-details-button → fallback на /catalog/houses/ якорь; ?context-трекинг отброшен (verified live: house-link есть в detail SSR).""" html = """
№ 7683141312
4 000 000 ₽ Узнать больше о доме """ result = parse_detail_html(html, "https://www.avito.ru/test_7683141312") assert ( result.house_catalog_url == "https://www.avito.ru/catalog/houses/ekaterinburg/ul_gotvalda_6k1/142792" ) def test_bathroom_type_combined() -> None: html = """
№ 1111111111
4 000 000 ₽
""" result = parse_detail_html(html, "https://www.avito.ru/test_1111111111") assert result.bathroom_type == "combined" assert result.windows_view == "both" assert result.room_layout == "isolated" def test_photo_urls_extracted() -> None: html = """
№ 2222222222
7 000 000 ₽
""" result = parse_detail_html(html, "https://www.avito.ru/test_2222222222") assert len(result.photo_urls) == 2 assert result.photo_urls[0] == "https://img.avito.ru/img1.jpg" def test_source_url_preserved() -> None: result = parse_detail_html(MINIMAL_HTML, SOURCE_URL) assert result.source_url == SOURCE_URL # ── repair_state fallback из описания (#622) ────────────────────────────────── def test_repair_state_structured_wins_over_description() -> None: """Структурное «Ремонт: евро» (good) в приоритете даже при сигнале в тексте.""" html = """
№ 3333333333
6 000 000 ₽
Квартира без отделки, под чистовую
""" result = parse_detail_html(html, "https://www.avito.ru/test_3333333333") assert result.repair_state == "good" # структурное поле, не needs_repair из текста def test_repair_state_inferred_from_description_when_structured_absent() -> None: """Нет блока «Ремонт» → инференс из описания (#622).""" html = """
№ 4444444444
8 000 000 ₽
Сделан свежий евроремонт, заезжай и живи
""" result = parse_detail_html(html, "https://www.avito.ru/test_4444444444") assert result.repair_state == "good" def test_repair_state_none_when_no_signal() -> None: """Нет структурного поля и нет сигнала в тексте → None (не фабрикуем).""" html = """
№ 5555555555
9 000 000 ₽
Светлая квартира рядом с метро
""" result = parse_detail_html(html, "https://www.avito.ru/test_5555555555") assert result.repair_state is None # ── Новые поля: living_area_m2, ceiling_height_m, finishing ────────────────── _NEW_FIELDS_HTML = """
№ 6666666666
8 500 000 ₽
""" def test_new_apartment_fields_populated() -> None: """living_area_m2 / ceiling_height_m / finishing парсятся из params-блока.""" result = parse_detail_html(_NEW_FIELDS_HTML, "https://www.avito.ru/test_6666666666") assert result.living_area_m2 == 12.6 assert result.ceiling_height_m == 2.7 assert result.finishing == "чистовая" def test_new_fields_absent_when_not_in_params() -> None: """Если полей нет в params — None, не дефолт.""" result = parse_detail_html( """
№ 7777777777
4 000 000 ₽
""", "https://www.avito.ru/test_7777777777", ) assert result.living_area_m2 is None assert result.ceiling_height_m is None assert result.finishing is None def test_finishing_stored_lowercase_stripped() -> None: """finishing → lowercase trimmed (' Чистовая ' → 'чистовая').""" html = """
№ 8888888888
5 000 000 ₽
""" result = parse_detail_html(html, "https://www.avito.ru/test_8888888888") assert result.finishing == "чистовая" # ── _parse_height_m unit tests ──────────────────────────────────────────────── def test_parse_height_m_dot() -> None: assert _parse_height_m("2.7\xa0м") == 2.7 def test_parse_height_m_comma() -> None: assert _parse_height_m("2,7\xa0м") == 2.7 def test_parse_height_m_integer() -> None: assert _parse_height_m("3 м") == 3.0 def test_parse_height_m_junk() -> None: assert _parse_height_m("нет данных") is None def test_parse_height_m_empty() -> None: assert _parse_height_m("") is None