gendesign/tradein-mvp/backend/tests/test_2657_domclick_proxy_pool.py
bot-backend 7b36f86ea6
Some checks failed
Deploy Trade-In / deploy (push) Blocked by required conditions
Deploy Trade-In / changes (push) Successful in 12s
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 3m5s
Deploy Trade-In / build-backend (push) Has been cancelled
fix(tradein/domclick): свип ходит через прокси-пул, а отказ узла не обнуляет прогон (#2657) (#2796)
2026-08-09 17:21:49 +00:00

114 lines
5.6 KiB
Python
Raw Permalink 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.

"""#2657: домклик собирал 0 лотов, потому что ходил мимо прокси-пула.
Прод 06-09.08.2026: `domclick_city_sweep` четыре прогона подряд `failed` с
`buckets=0/6, lots=0, errors=1`. Причина найдена не в QRATOR и не в куках, а в двух
сложившихся дефектах:
1. **Домклик был единственным sweep-источником без `proxy_provider`** (#2160 P4 wiring
gap). `build_browser_fetcher(config, "domclick")` без провайдера → lease не берётся →
сайдкар идёт через статический `SCRAPER_PROXY_URL`. Этот адрес (прод
`scrape_proxies.id=1`, residential, `provider_affinity='domclick'`) с 06.08 отдаёт
`NS_ERROR_PROXY_BAD_GATEWAY` ИМЕННО на `bff-search-web.domclick.ru`. Живая проверка
09.08 через сайдкар: тот же URL через id=1 → 500, через мобильный узел пула
(`id=10`) → 200 и `{"offersCount":704,"snippetsCount":678}` в бакете `st`.
Предпосылка миграции 173 («QRATOR банит все прокси кроме этого residential»)
опровергнута: мобильный узел домкликовский BFF пускает.
2. **Транспортная ошибка убивала весь свип на первом бакете.** `fetch_city` ловил
только `ValueError/TypeError` — ошибки РАЗБОРА. `httpx.HTTPStatusError` от сайдкара
пролетал наружу мимо цикла по `ROOM_BUCKETS`, и прогон заканчивался с `0/6`. Это же
не давало сработать ротации lease после N подряд провалов
(`browser_fetcher._LEASE_ROTATE_AFTER_FAILS`) — до неё не доживали.
Фальсификация на старом коде: `test_scraper_leases_proxy_for_domclick` падает
`TypeError: unexpected keyword argument 'proxy_provider'`,
`test_transport_error_does_not_kill_the_whole_sweep` падает на пробросе
`HTTPStatusError` наружу.
"""
from __future__ import annotations
import os
from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
import httpx
import pytest
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db")
from scraper_kit.orchestration.scheduler import _job_domclick_city_sweep
from scraper_kit.providers.domclick.serp import ROOM_BUCKETS, DomClickScraper
def _config() -> SimpleNamespace:
return SimpleNamespace(
browser_http_endpoint="http://x:9000",
use_proxy_pool_browser=True,
environment="production",
)
@pytest.fixture
def _no_browser() -> Any:
"""Сайдкар не поднимается — нас интересует только то, ЧТО ему передали."""
fetcher = MagicMock()
fetcher.__aenter__ = AsyncMock(return_value=fetcher)
fetcher.__aexit__ = AsyncMock(return_value=None)
with patch("scraper_kit.providers._base.BrowserFetcher", return_value=fetcher) as bf:
yield bf
async def test_scraper_leases_proxy_for_domclick(_no_browser: MagicMock) -> None:
"""Узел берётся из пула под именем "domclick"а не из env сайдкара."""
provider = MagicMock()
scraper = DomClickScraper(_config(), proxy_provider=provider)
with patch.object(DomClickScraper, "_sweep_bucket", AsyncMock(return_value=None)):
await scraper.fetch_city(city_id=4)
assert _no_browser.call_args.kwargs["proxy_provider"] is provider
assert _no_browser.call_args.kwargs["use_pool"] is True
async def test_transport_error_does_not_kill_the_whole_sweep(_no_browser: MagicMock) -> None:
"""Отказ сайдкара на первом бакете не обнуляет прогон — остальные бакеты идут.
Прод-форма отказа: 500 от tradein-browser с NS_ERROR_PROXY_BAD_GATEWAY в теле.
"""
scraper = DomClickScraper(_config())
calls = {"n": 0}
async def _sweep(self: DomClickScraper, **_: object) -> None:
calls["n"] += 1
if calls["n"] == 1:
raise httpx.HTTPStatusError(
"Server error '500 Internal Server Error' | tradein-browser: "
'{"error": "Error: Page.goto: NS_ERROR_PROXY_BAD_GATEWAY"}',
request=MagicMock(),
response=MagicMock(),
)
with patch.object(DomClickScraper, "_sweep_bucket", _sweep):
await scraper.fetch_city(city_id=4)
assert calls["n"] == len(ROOM_BUCKETS)
# Битый бакет НЕ засчитан в охват (#2670) — прогон честно неполон.
assert scraper.buckets_completed == len(ROOM_BUCKETS) - 1
assert scraper.fetch_errors == 1
async def test_scheduler_passes_pool_to_domclick_job() -> None:
"""Планировщик отдаёт домклику ctx.proxy_provider — как всем остальным свипам."""
provider = MagicMock()
ctx = SimpleNamespace(
config=_config(),
matcher=MagicMock(),
shutdown_requested=lambda: False,
proxy_provider=provider,
)
sweep = AsyncMock(return_value=None)
with patch("scraper_kit.orchestration.scheduler.run_domclick_city_sweep", sweep):
await _job_domclick_city_sweep(MagicMock(), 1, {}, ctx) # type: ignore[arg-type]
assert sweep.await_args.kwargs["proxy_provider"] is provider