gendesign/tradein-mvp/backend/tests/test_cian_views_parse.py
bot-backend 339713c1aa
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 3m6s
Deploy Trade-In / build-backend (push) Successful in 1m35s
Deploy Trade-In / deploy (push) Successful in 1m45s
fix(tradein/yandex): просмотры режутся по разделителю тысяч + Циан молчит о непонятой фразе (#2669) (#2773)
2026-08-07 08:44:48 +00:00

101 lines
5.2 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

"""#2669: Cian отдаёт просмотры фразой — разбор обязан доставать из неё числа.
Замер на проде до правки: `listings.views_total` пуст у ВСЕХ 21 799 cian-строк
(0 из 1 571 detail-обогащённых), при том что у avito 12 350/12 352, у domklik
6 296/6 296, у yandex 1 146/1 210 — потеря ровно одна и только у Cian.
Корень: `_parse_views` звала `int()` по всей фразе
`"146 просмотров, 8 за сегодня"` → ValueError → None (except в самой функции).
Тесты гоняют настоящий сохранённый ответ (fixtures/cian_flat_330982715.html)
через настоящий `fetch_detail` + перебирают формы фразы.
"""
from __future__ import annotations
import logging
import os
from unittest.mock import AsyncMock, MagicMock
import pytest
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db")
from scraper_kit.providers.cian.detail import _parse_views, fetch_detail
_FIXTURE = os.path.join(os.path.dirname(__file__), "fixtures", "cian_flat_330982715.html")
# Дословно из фикстуры: "stats":{"totalViewsFormattedString":"146 просмотров, 8 за сегодня"}
_FIXTURE_PHRASE = "146 просмотров, 8 за сегодня"
def _fixture_html() -> str:
with open(_FIXTURE, encoding="utf-8") as fh:
return fh.read()
def test_fixture_still_carries_the_phrase() -> None:
"""Страховка: тесты ниже бессмысленны, если фикстура перестала содержать фразу."""
assert f'"totalViewsFormattedString":"{_FIXTURE_PHRASE}"' in _fixture_html()
@pytest.mark.parametrize(
("phrase", "expected"),
[
(_FIXTURE_PHRASE, (146, 8)), # форма из сохранённого ответа
("1 просмотр", (1, None)), # единственное число, хвоста нет
("2 просмотра", (2, None)),
("0 просмотров", (0, None)), # ноль — это 0, а не «нет данных»
("1 234 просмотра", (1234, None)), # разделитель тысяч — обычный пробел
("1\xa0234 просмотра", (1234, None)), # ... и NBSP
("12345 просмотров, 1\xa0234 за сегодня", (12345, 1234)), # narrow NBSP
("1 234", (1234, None)), # голое число (форма из старого докстринга)
("", (None, None)),
(None, (None, None)),
("просмотров нет", (None, None)), # без цифр — пусто, а не 0
("нет данных", (None, None)), # ответ площадки «нечего показать»
],
)
def test_parse_views_forms(phrase: str | None, expected: tuple[int | None, int | None]) -> None:
assert _parse_views(phrase) == expected
def test_unparsed_phrase_is_logged_with_itself(caplog: pytest.LogCaptureFixture) -> None:
"""Провал разбора обязан быть отличим от «Циан не прислал» (эпик #2674).
Прошлая пропажа держалась месяцами ровно потому, что в журнале не было ни
строки: пустая колонка одинаково выглядела и при сломанном разборе, и при
молчании площадки. Поэтому в warning уходит САМА фраза.
"""
with caplog.at_level(logging.WARNING, logger="scraper_kit.providers.cian.detail"):
assert _parse_views("нет данных") == (None, None)
assert "нет данных" in caplog.text, "неразобранная фраза не видна в журнале"
def test_absent_phrase_is_not_logged(caplog: pytest.LogCaptureFixture) -> None:
"""Циан не прислал ключ вовсе — это не поломка разбора, шуметь не о чем."""
with caplog.at_level(logging.WARNING, logger="scraper_kit.providers.cian.detail"):
assert _parse_views(None) == (None, None)
assert caplog.text == ""
def test_no_warning_when_today_tail_absent(caplog: pytest.LogCaptureFixture) -> None:
"""«0 просмотров» без хвоста «за сегодня» — штатная форма, не повод для warning."""
with caplog.at_level(logging.WARNING, logger="scraper_kit.providers.cian.detail"):
assert _parse_views("0 просмотров") == (0, None)
assert caplog.text == ""
async def test_fetch_detail_fills_views_from_real_saved_page() -> None:
"""Настоящая сохранённая страница → views_total/views_today непусты (#2669)."""
fetcher = MagicMock()
fetcher.fetch = AsyncMock(return_value=_fixture_html())
result = await fetch_detail("https://ekb.cian.ru/sale/flat/330982715/", browser_fetcher=fetcher)
assert result is not None
assert result.views_total == 146, "views_total снова теряется — колонка останется пустой"
assert result.views_today == 8