gendesign/tradein-mvp/backend/tests/scrapers/test_domclick.py

240 lines
8.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.

"""Unit-тесты для DomClick scraper (#796).
Тестируем:
- Парсинг title-паттерна (rooms/area/floor) из текста карточки
- Парсинг цены из отдельного DOM-элемента
- Извлечение source_id из href
- Полный цикл _parse_html -> list[ScrapedLot] на mocked HTML
"""
from __future__ import annotations
import asyncio
import pytest
from app.services.scrapers.domclick import (
DomClickScraper,
_extract_area,
_extract_floor,
_extract_price_from_element,
_extract_rooms,
_extract_source_id,
)
# ── Rooms / studio ───────────────────────────────────────────────────────────
@pytest.mark.parametrize(
"text, expected",
[
("2-комн. квартира 53,5 м² 5/15 эт.", 2),
("1-к. квартира 32,1 м² 3/9 эт.", 1),
("3-комн. кв. 78 м² 10/12 эт.", 3),
("4-комн. квартира 95 м² 2/5 эт.", 4),
("студия 24 м² 1/10 эт.", 0),
("Студия 30,5 м² 8/22 эт.", 0),
("Нет комнат — просто текст", None),
],
)
def test_extract_rooms(text: str, expected: int | None) -> None:
assert _extract_rooms(text) == expected
# ── Area ─────────────────────────────────────────────────────────────────────
@pytest.mark.parametrize(
"text, expected",
[
("2-комн. квартира 53,5 м² 5/15 эт.", 53.5),
("студия 24 м² 1/10 эт.", 24.0),
("78м2 на этаже", 78.0),
("площадь 101.7 м²", 101.7),
("Нет площади", None),
],
)
def test_extract_area(text: str, expected: float | None) -> None:
assert _extract_area(text) == expected
# ── Floor ─────────────────────────────────────────────────────────────────────
@pytest.mark.parametrize(
"text, floor, total",
[
("2-комн. квартира 53,5 м² 5/15 эт.", 5, 15),
("студия 24 м² 1/10 эт.", 1, 10),
("12/24 эт.", 12, 24),
("нет этажа", None, None),
],
)
def test_extract_floor(text: str, floor: int | None, total: int | None) -> None:
assert _extract_floor(text) == (floor, total)
# ── Price (per-element) ───────────────────────────────────────────────────────
# Цена парсится из отдельного DOM-элемента (не из суммарного card_text),
# поэтому числа адреса не загрязняют парсинг цены.
@pytest.mark.parametrize(
"text, expected",
[
("5 200 000 ₽", 5_200_000),
("12 345 678 ₽", 12_345_678),
("3 990 000 руб.", 3_990_000),
("от 4 500 000 ₽", 4_500_000),
("без цены", None),
("0 ₽", None),
],
)
def test_extract_price_from_element(text: str, expected: int | None) -> None:
assert _extract_price_from_element(text) == expected
# ── source_id from href ───────────────────────────────────────────────────────
@pytest.mark.parametrize(
"href, expected",
[
("/card/sale__flat__2075671636", "2075671636"),
("https://domclick.ru/card/sale__flat__123456789", "123456789"),
("/card/sale__flat__1", "1"),
("/card/something_else", None),
("", None),
],
)
def test_extract_source_id(href: str, expected: str | None) -> None:
assert _extract_source_id(href) == expected
# ── _parse_html -> list[ScrapedLot] ──────────────────────────────────────────
# Цена в каждой карточке вынесена в отдельный <span> с символом рубля,
# чтобы не сливаться с адресным номером дома при конкатенации card_text.
_MOCK_HTML = """\
<!DOCTYPE html>
<html>
<body>
<!-- Карточка 1: комнатность, площадь, этаж, адрес, цена в отдельных span -->
<a href="/card/sale__flat__2075671636">
<span>2-комн. квартира 53,5 м² 5/15 эт.</span>
<span>ул. Малышева, 1</span>
<span>5 200 000 ₽</span>
</a>
<!-- Карточка 2: студия -->
<a href="/card/sale__flat__1111111111">
<span>Студия 28 м² 3/10 эт.</span>
<span>пр-т Ленина, 50</span>
<span>3 100 000 ₽</span>
</a>
<!-- Карточка 3: без цены -> должна быть отфильтрована -->
<a href="/card/sale__flat__9999999999">
<span>1-комн. квартира 32 м² 2/9 эт.</span>
</a>
<!-- Не карточка (без sale__flat__) -> должна быть отфильтрована -->
<a href="/card/sale__apartment__123">
<span>что-то другое</span>
</a>
<!-- Дубликат первой карточки -> один раз на страницу -->
<a href="/card/sale__flat__2075671636">
<span>2-комн. квартира 53,5 м² 5/15 эт.</span>
<span>5 200 000 ₽</span>
</a>
</body>
</html>
"""
def test_parse_html_returns_lots() -> None:
scraper = DomClickScraper()
lots = scraper._parse_html(_MOCK_HTML)
# Карточки 1 и 2 парсятся; карточка 3 (без цены) отфильтрована; дубль — дедуплицирован
assert len(lots) == 2
def test_parse_html_card1_fields() -> None:
scraper = DomClickScraper()
lots = scraper._parse_html(_MOCK_HTML)
card1 = next(lot for lot in lots if lot.source_id == "2075671636")
assert card1.source == "domclick"
assert card1.source_url == "https://domclick.ru/card/sale__flat__2075671636"
assert card1.rooms == 2
assert card1.area_m2 == 53.5
assert card1.floor == 5
assert card1.total_floors == 15
assert card1.price_rub == 5_200_000
assert card1.lat is None
assert card1.lon is None
assert card1.listing_segment == "vtorichka"
def test_parse_html_studio() -> None:
scraper = DomClickScraper()
lots = scraper._parse_html(_MOCK_HTML)
studio = next(lot for lot in lots if lot.source_id == "1111111111")
assert studio.rooms == 0 # студия
assert studio.area_m2 == 28.0
assert studio.price_rub == 3_100_000
def test_parse_html_no_price_filtered() -> None:
scraper = DomClickScraper()
lots = scraper._parse_html(_MOCK_HTML)
source_ids = {lot.source_id for lot in lots}
assert "9999999999" not in source_ids
def test_parse_html_dedup_same_href() -> None:
"""Дубликат href на одной странице должен давать одну карточку."""
scraper = DomClickScraper()
lots = scraper._parse_html(_MOCK_HTML)
ids_2075 = [lot for lot in lots if lot.source_id == "2075671636"]
assert len(ids_2075) == 1
def test_scraper_source_name() -> None:
scraper = DomClickScraper()
assert scraper.name == "domclick"
assert scraper.source == "domclick"
assert scraper.base_url == "https://domclick.ru"
assert scraper.request_delay_sec == 8.0
def test_fetch_around_raises() -> None:
scraper = DomClickScraper()
with pytest.raises(NotImplementedError, match="geo-radius"):
asyncio.run(scraper.fetch_around(56.8, 60.6))
def test_build_url_no_rooms() -> None:
scraper = DomClickScraper()
url = scraper._build_url(city_id=4, rooms=None, page=1)
assert "city_id=4" in url
assert "rooms" not in url
assert "p=1" in url
def test_build_url_with_rooms() -> None:
scraper = DomClickScraper()
url = scraper._build_url(city_id=4, rooms=2, page=3)
assert "rooms=2" in url
assert "p=3" in url
def test_parse_failures_counter() -> None:
"""parse_failures счётчик начинается с 0."""
scraper = DomClickScraper()
assert scraper.parse_failures == 0