gendesign/tradein-mvp/backend/tests/test_avito_serp_date.py
bot-backend dd59ef0516
All checks were successful
CI / changes (pull_request) Successful in 6s
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
feat(avito): capture newbuilding ЖК id/url from SERP card; persist newbuilding_id/url
2026-06-17 23:15:35 +03:00

135 lines
6.4 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.

"""#726 — avito SERP listing_date extraction (sortTimeStamp JSON + DOM fallback).
Offline: парсит фикстуру через AvitoScraper._parse_html, без сети/curl_cffi/БД.
Регрессия на дрейф разметки: дата у lazy-карточек (без DOM item-date) берётся из
per-item JSON sortTimeStamp; у верхних карточек — из DOM data-marker="item-date".
"""
import os
from datetime import date, datetime, timedelta, timezone
from pathlib import Path
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db")
from app.services.scrapers.avito import (
AvitoScraper,
_build_sort_timestamp_map,
)
FIXTURE = Path(__file__).parent / "fixtures" / "avito_serp_sample.html"
# Из фикстуры (sortTimeStamp, epoch-ms).
_TS_LAZY = 1700000000000
_TS_TOP = 1700086400000
# #726: дата берётся в MSK (UTC+3), чтобы совпадать с тем, что Avito показывает на
# сайте (sortTimeStamp — UTC epoch, но отображается по Москве).
_MSK = timezone(timedelta(hours=3))
def _expected(ms: int) -> date:
return datetime.fromtimestamp(ms / 1000, tz=_MSK).date()
def test_sort_timestamp_map_built_from_json() -> None:
html = FIXTURE.read_text(encoding="utf-8")
ts_map = _build_sort_timestamp_map(html)
assert ts_map["8043936560"] == _expected(_TS_LAZY)
assert ts_map["8163084615"] == _expected(_TS_TOP)
def test_parse_html_sets_listing_date_for_all_cards() -> None:
"""Обе карточки получают listing_date != None (DoD #726)."""
html = FIXTURE.read_text(encoding="utf-8")
lots = AvitoScraper()._parse_html(html, "https://www.avito.ru/ekaterinburg/kvartiry")
assert len(lots) == 2
by_url = {lot.source_url: lot for lot in lots}
assert all(lot.listing_date is not None for lot in lots), "every card must carry a date"
# Lazy card (нет DOM item-date) → дата из JSON sortTimeStamp.
lazy = next(lot for lot in lots if "8043936560" in (lot.source_url or ""))
assert lazy.listing_date == _expected(_TS_LAZY)
assert by_url # touch to keep linter calm if structure changes
def test_lazy_card_date_comes_from_json_not_dom() -> None:
"""Карточка без data-marker='item-date' всё равно датируется (JSON primary)."""
html = FIXTURE.read_text(encoding="utf-8")
assert 'data-item-id="8043936560"' in html
# у этой карточки в фикстуре НЕТ DOM item-date — проверяем, что дата не потерялась
lots = AvitoScraper()._parse_html(html, "https://www.avito.ru/x")
lazy = next(lot for lot in lots if "8043936560" in (lot.source_url or ""))
assert lazy.listing_date == _expected(_TS_LAZY)
# ── listing_segment по DOM-маркеру item-development-name ───────────────────────
# Каждый avito-URL квартиры содержит /kvartiry/ (вкл. новостройки), поэтому
# сегмент определяется не по URL, а по наличию data-marker="item-development-name"
# (название ЖК/застройщика рендерится только у карточек новостроек).
def _serp_card(item_id: str, *, with_dev_name: bool, with_zhk_anchor: bool = False) -> str:
dev = '<div data-marker="item-development-name">ЖК «Федерация»</div>' if with_dev_name else ""
zhk = (
'<a href="https://zhk-fed-ekaterinburg.avito.ru?context=H4sIAAAA">ЖК Федерация</a>'
if with_zhk_anchor
else ""
)
return f"""
<div data-marker="item" data-item-id="{item_id}">
<a data-marker="item-title" href="/ekaterinburg/kvartiry/2-k_kvartira_{item_id}">
2-к. квартира, 50 м², 5/20 эт.</a>
<meta itemprop="price" content="9500000"/>
{dev}
{zhk}
</div>
"""
def test_segment_novostroyki_when_development_name_present() -> None:
"""Карточка с data-marker='item-development-name' → listing_segment='novostroyki'."""
html = f"<html><body>{_serp_card('8043003287', with_dev_name=True)}</body></html>"
lots = AvitoScraper()._parse_html(html, "https://www.avito.ru/ekaterinburg/kvartiry")
assert len(lots) == 1
assert lots[0].listing_segment == "novostroyki"
def test_segment_vtorichka_when_no_development_name() -> None:
"""Карточка без маркера ЖК → listing_segment='vtorichka' (даже с /kvartiry/ в URL)."""
html = f"<html><body>{_serp_card('8043936560', with_dev_name=False)}</body></html>"
lots = AvitoScraper()._parse_html(html, "https://www.avito.ru/ekaterinburg/kvartiry")
assert len(lots) == 1
assert lots[0].listing_segment == "vtorichka"
# ── newbuilding (ЖК) id/url из zhk-сабдомена ──────────────────────────────────
def test_newbuilding_captured_from_zhk_anchor() -> None:
"""Новостройка с якорем zhk-*.avito.ru → newbuilding_url без ?context, id = slug."""
card = _serp_card("8043003287", with_dev_name=True, with_zhk_anchor=True)
html = f"<html><body>{card}</body></html>"
lots = AvitoScraper()._parse_html(html, "https://www.avito.ru/ekaterinburg/kvartiry")
assert len(lots) == 1
assert lots[0].newbuilding_url == "https://zhk-fed-ekaterinburg.avito.ru"
assert lots[0].newbuilding_id == "fed-ekaterinburg"
def test_newbuilding_none_when_novostroyka_without_anchor() -> None:
"""Новостройка без zhk-якоря → оба поля None (сегмент по dev-name остаётся)."""
card = _serp_card("8043003287", with_dev_name=True, with_zhk_anchor=False)
html = f"<html><body>{card}</body></html>"
lots = AvitoScraper()._parse_html(html, "https://www.avito.ru/ekaterinburg/kvartiry")
assert len(lots) == 1
assert lots[0].listing_segment == "novostroyki"
assert lots[0].newbuilding_id is None
assert lots[0].newbuilding_url is None
def test_newbuilding_none_for_vtorichka() -> None:
"""Вторичка → newbuilding_id/url оба None."""
html = f"<html><body>{_serp_card('8043936560', with_dev_name=False)}</body></html>"
lots = AvitoScraper()._parse_html(html, "https://www.avito.ru/ekaterinburg/kvartiry")
assert len(lots) == 1
assert lots[0].newbuilding_id is None
assert lots[0].newbuilding_url is None