gendesign/backend/tests/services/scrapers/test_stealth_throttle_proxy.py
bot-backend 3421b21632
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 9m31s
Deploy / build-worker (push) Successful in 11m57s
Deploy / deploy (push) Successful in 3m51s
fix(scrapers): разблокировать domrf_kn flats — изоляция flats от poison-extras + throttle (#1945) (#2050)
2026-06-28 13:03:43 +00:00

125 lines
5 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-тесты для KN-loader anti-ban рычагов в scrapers/stealth (#1945).
DOM.РФ WAF банит VPS-IP по volume/rate на full sweep (~17k запросов при
concurrency=8). Лечим: (1) per-instance throttle (concurrency + шире jitter)
для KN-sweep, НЕ трогая модульный дефолт остальных скраперов; (2) опциональный
прокси (default off → поведение без изменений).
Тесты проверяют:
• parse_proxy_url корректно разбирает http://user:pass@host:port;
• BrowserSession прокидывает concurrency/jitter/proxy в self-поля;
• дефолтное (None) поведение НЕ меняется (concurrency=8, jitter 6001500, без прокси).
"""
from __future__ import annotations
import pytest
from app.services.scrapers.stealth import (
_BROWSER_CONCURRENCY,
_DEFAULT_JITTER_MAX_MS,
_DEFAULT_JITTER_MIN_MS,
BrowserSession,
parse_proxy_url,
)
class TestParseProxyUrl:
"""parse_proxy_url → Playwright proxy-dict {server, username?, password?}."""
def test_none_returns_none(self) -> None:
assert parse_proxy_url(None) is None
def test_empty_string_returns_none(self) -> None:
assert parse_proxy_url("") is None
def test_full_creds_url(self) -> None:
got = parse_proxy_url("http://user:pass@host.example:8080")
assert got == {
"server": "http://host.example:8080",
"username": "user",
"password": "pass",
}
def test_no_creds(self) -> None:
got = parse_proxy_url("http://host.example:3128")
assert got == {"server": "http://host.example:3128"}
def test_creds_not_leaked_into_server(self) -> None:
# Playwright требует creds ОТДЕЛЬНЫМИ ключами, не в server-URL.
# Используем уникальные токены, которых нет в host/scheme.
got = parse_proxy_url("http://secretuser:secretpass@1.2.3.4:9999")
assert got is not None
assert got["server"] == "http://1.2.3.4:9999"
assert "secretuser" not in got["server"]
assert "secretpass" not in got["server"]
assert "@" not in got["server"]
def test_scheme_preserved(self) -> None:
got = parse_proxy_url("https://u:p@host:443")
assert got is not None
assert got["server"] == "https://host:443"
def test_default_scheme_when_missing(self) -> None:
# urlsplit без схемы кладёт всё в path → host не резолвится → ValueError.
with pytest.raises(ValueError):
parse_proxy_url("host.example:8080")
def test_host_required(self) -> None:
with pytest.raises(ValueError):
parse_proxy_url("http://:8080")
def test_error_does_not_leak_credentials(self) -> None:
# ValueError всплывает в kn_scrape_runs.error / log_progress / Sentry —
# НЕ должен содержать пароль (#1945 sec-review).
bad = "http://secretuser:secretpass@:8080" # есть creds, но нет host
with pytest.raises(ValueError) as exc:
parse_proxy_url(bad)
msg = str(exc.value)
assert "secretpass" not in msg
assert "secretuser" not in msg
assert bad not in msg
class TestBrowserSessionThrottleThreading:
"""concurrency/jitter/proxy прокидываются в инстанс-поля BrowserSession."""
def test_concurrency_threaded(self) -> None:
sess = BrowserSession(concurrency=2)
assert sess._concurrency == 2
assert sess._sem._value == 2
def test_jitter_threaded(self) -> None:
sess = BrowserSession(jitter_min_ms=1200, jitter_max_ms=3000)
assert sess._jitter_min_ms == 1200
assert sess._jitter_max_ms == 3000
def test_proxy_threaded_and_parsed(self) -> None:
sess = BrowserSession(proxy_url="http://u:p@proxy.local:8000")
assert sess._proxy == {
"server": "http://proxy.local:8000",
"username": "u",
"password": "p",
}
class TestBrowserSessionDefaultsUnchanged:
"""None у каждого рычага = старое поведение (другие скраперы НЕ затронуты)."""
def test_default_concurrency_is_module_default(self) -> None:
sess = BrowserSession()
assert sess._concurrency == _BROWSER_CONCURRENCY == 8
assert sess._sem._value == 8
def test_default_jitter_is_module_default(self) -> None:
sess = BrowserSession()
assert sess._jitter_min_ms == _DEFAULT_JITTER_MIN_MS == 600
assert sess._jitter_max_ms == _DEFAULT_JITTER_MAX_MS == 1500
def test_default_proxy_is_none(self) -> None:
sess = BrowserSession()
assert sess._proxy is None
def test_explicit_none_proxy_is_none(self) -> None:
sess = BrowserSession(proxy_url=None)
assert sess._proxy is None