All checks were successful
Deploy Trade-In / changes (push) Successful in 5s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / test (push) Successful in 30s
Deploy Trade-In / build-backend (push) Successful in 41s
Deploy Trade-In / deploy (push) Successful in 36s
Co-authored-by: lekss361 <lekss361@gendsgn.local> Co-committed-by: lekss361 <lekss361@gendsgn.local>
243 lines
10 KiB
Python
243 lines
10 KiB
Python
"""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_detail_html
|
||
|
||
MINIMAL_HTML = """
|
||
<html><body>
|
||
<div data-marker="item-view/item-id">№ 7986882804 · 18 мая в 13:40 · 1899 просмотров (+ 12 сегодня)</div>
|
||
<span itemprop="price" content="11990000">11 990 000 ₽</span>
|
||
<div data-marker="item-map-wrapper" data-map-lat="56.797713" data-map-lon="60.609135" data-location-id="654070"></div>
|
||
<div data-marker="item-view/item-params">
|
||
<ul>
|
||
<li>Количество комнат: 3</li>
|
||
<li>Общая площадь: 75 м²</li>
|
||
<li>Площадь кухни: 15 м²</li>
|
||
<li>Этаж: 20 из 26</li>
|
||
<li>Балкон или лоджия: лоджия</li>
|
||
<li>Ремонт: евро</li>
|
||
<li>Способ продажи: свободная</li>
|
||
<li>Условия продажи: возможна ипотека</li>
|
||
</ul>
|
||
<ul>
|
||
<li>Тип дома: монолитный</li>
|
||
<li>Этажей в доме: 26</li>
|
||
</ul>
|
||
</div>
|
||
<a data-marker="nd-jk-details-button" href="/catalog/houses/ekaterinburg/ul_postovskogo_17a/3171365">Дом</a>
|
||
<div data-marker="domoteka-entry-block">
|
||
<p data-marker="TeaserData.item">1 собственник</p>
|
||
<p data-marker="TeaserData.item">Последняя смена собственника 5 мая 2020</p>
|
||
<p data-marker="TeaserData.item">Не найдены ограничения и обременения</p>
|
||
<p data-marker="TeaserData.item">Совпадают площадь, адрес и этаж</p>
|
||
</div>
|
||
<div data-marker="item-view/item-description">Отличная квартира в новом ЖК. Чкаловская 11-15 мин пешком.</div>
|
||
</body></html>
|
||
"""
|
||
|
||
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 = """
|
||
<html><body>
|
||
<div data-marker="item-view/item-id">№ 1234567890</div>
|
||
<span itemprop="price" content="5000000">5 000 000 ₽</span>
|
||
<div data-marker="domoteka-entry-block">
|
||
<p data-marker="TeaserData.item">3 собственника или больше</p>
|
||
</div>
|
||
</body></html>
|
||
"""
|
||
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 = """
|
||
<html><body>
|
||
<div data-marker="item-view/item-id">№ 9999999999</div>
|
||
<span itemprop="price" content="3000000">3 000 000 ₽</span>
|
||
</body></html>
|
||
"""
|
||
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 = "<html><body><p>Нет айди</p></body></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 = """
|
||
<html><body>
|
||
<div data-marker="item-view/item-id">№ 7683141312</div>
|
||
<span itemprop="price" content="4000000">4 000 000 ₽</span>
|
||
<a href="/catalog/houses/ekaterinburg/ul_gotvalda_6k1/142792?context=H4sIabc">
|
||
Узнать больше о доме</a>
|
||
</body></html>
|
||
"""
|
||
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 = """
|
||
<html><body>
|
||
<div data-marker="item-view/item-id">№ 1111111111</div>
|
||
<span itemprop="price" content="4000000">4 000 000 ₽</span>
|
||
<div data-marker="item-view/item-params">
|
||
<ul>
|
||
<li>Санузел: совмещенный</li>
|
||
<li>Окна: во двор и на улицу</li>
|
||
<li>Тип комнат: изолированные</li>
|
||
</ul>
|
||
</div>
|
||
</body></html>
|
||
"""
|
||
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 = """
|
||
<html><body>
|
||
<div data-marker="item-view/item-id">№ 2222222222</div>
|
||
<span itemprop="price" content="7000000">7 000 000 ₽</span>
|
||
<div data-marker="image-preview/item"><img src="https://img.avito.ru/img1.jpg"/></div>
|
||
<div data-marker="image-preview/item"><img src="https://img.avito.ru/img2.jpg"/></div>
|
||
</body></html>
|
||
"""
|
||
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 = """
|
||
<html><body>
|
||
<div data-marker="item-view/item-id">№ 3333333333</div>
|
||
<span itemprop="price" content="6000000">6 000 000 ₽</span>
|
||
<div data-marker="item-view/item-params">
|
||
<ul><li>Ремонт: евро</li></ul>
|
||
</div>
|
||
<div data-marker="item-view/item-description">Квартира без отделки, под чистовую</div>
|
||
</body></html>
|
||
"""
|
||
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 = """
|
||
<html><body>
|
||
<div data-marker="item-view/item-id">№ 4444444444</div>
|
||
<span itemprop="price" content="8000000">8 000 000 ₽</span>
|
||
<div data-marker="item-view/item-params">
|
||
<ul><li>Количество комнат: 2</li></ul>
|
||
</div>
|
||
<div data-marker="item-view/item-description">Сделан свежий евроремонт, заезжай и живи</div>
|
||
</body></html>
|
||
"""
|
||
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 = """
|
||
<html><body>
|
||
<div data-marker="item-view/item-id">№ 5555555555</div>
|
||
<span itemprop="price" content="9000000">9 000 000 ₽</span>
|
||
<div data-marker="item-view/item-description">Светлая квартира рядом с метро</div>
|
||
</body></html>
|
||
"""
|
||
result = parse_detail_html(html, "https://www.avito.ru/test_5555555555")
|
||
assert result.repair_state is None
|