gendesign/tradein-mvp/backend/tests/test_avito_serp_date.py
bot-backend 22a7a3f086
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
fix(avito): classify novostroyki by item-development-name marker; map переуступка sale_type
2026-06-17 22:24:13 +03:00

96 lines
4.6 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) -> str:
dev = '<div data-marker="item-development-name">ЖК «Федерация»</div>' if with_dev_name 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}
</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"