- tests/unit (make test, без браузера, ~5 с): схемы и валидация, /v1/solve с подменённым движком (коды ошибок, слоты, сессии, UA и cookies профиля, маскирование секретов в логах), действия, оркестратор антибота, профиль и отпечаток - tests/browser (make test-browser, в Docker): example.com, UA один раз и замена cookies на httpbin, применение и стабильность отпечатка, прохождение Cloudflare - requirements-dev.txt, make sync-dev; Makefile вызывает pip через python -m pip (у перенесённого .venv битый shebang bin/pip) - fix: в CloudflareSolver вызов bounding_box() у неожиданной корутины — ветка без iframe всегда падала с AttributeError Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
91 lines
2.9 KiB
Python
91 lines
2.9 KiB
Python
"""Цикл detect → solve → verify в antibot.orchestrator на поддельном провайдере."""
|
|
import pytest
|
|
from patchright.async_api import Error as PlaywrightError
|
|
|
|
from antibot import orchestrator
|
|
from antibot.base import ChallengeInfo, Stage
|
|
from antibot.exceptions import ProviderNotDetected
|
|
|
|
pytestmark = pytest.mark.anyio
|
|
|
|
|
|
class FakePage:
|
|
def __init__(self):
|
|
self.waits = []
|
|
|
|
async def wait_for_load_state(self, state):
|
|
pass
|
|
|
|
async def wait_for_timeout(self, ms):
|
|
self.waits.append(ms)
|
|
|
|
|
|
class FakeProvider:
|
|
provider = "fake"
|
|
|
|
def __init__(self, detects, solve_error=None):
|
|
self.detects = list(detects) # что вернуть/бросить на каждый detect
|
|
self.solve_error = solve_error
|
|
self.solves = 0
|
|
|
|
async def detect(self, page):
|
|
item = self.detects.pop(0) if len(self.detects) > 1 else self.detects[0]
|
|
if isinstance(item, Exception):
|
|
raise item
|
|
return item
|
|
|
|
async def solve(self, page, info):
|
|
self.solves += 1
|
|
if self.solve_error:
|
|
raise self.solve_error
|
|
|
|
|
|
def info(success, stage=Stage.CHECKBOX):
|
|
return ChallengeInfo(stage=stage, provider="fake", success=success, meta={"reason": "done" if success else None})
|
|
|
|
|
|
@pytest.fixture
|
|
def use_provider(monkeypatch):
|
|
def install(provider):
|
|
async def select_one(page, name, **kwargs):
|
|
return provider
|
|
monkeypatch.setattr(orchestrator, "select_one", select_one)
|
|
return provider
|
|
return install
|
|
|
|
|
|
async def test_not_requested():
|
|
outcome = await orchestrator.pass_challenges(None, FakePage())
|
|
assert outcome.cleared and outcome.reason == "antibot not requested"
|
|
|
|
|
|
async def test_solved_after_one_step(use_provider):
|
|
provider = use_provider(FakeProvider([info(False), info(True)]))
|
|
outcome = await orchestrator.pass_challenges("fake", FakePage())
|
|
assert outcome.cleared and outcome.reason == "done"
|
|
assert provider.solves == 1
|
|
|
|
|
|
async def test_detect_survives_navigation(use_provider):
|
|
"""Челлендж уходит на целевую страницу посреди detect — повторяем detect, а не падаем."""
|
|
destroyed = PlaywrightError("Page.query_selector: Execution context was destroyed, most likely because of a navigation.")
|
|
provider = use_provider(FakeProvider([destroyed, info(True)]))
|
|
outcome = await orchestrator.pass_challenges("fake", FakePage())
|
|
assert outcome.cleared
|
|
assert provider.solves == 0
|
|
|
|
|
|
async def test_failing_solve_pauses_and_times_out(use_provider):
|
|
page = FakePage()
|
|
use_provider(FakeProvider([info(False)], solve_error=RuntimeError("click failed")))
|
|
outcome = await orchestrator.pass_challenges("fake", page, timeout_ms=50)
|
|
assert not outcome.cleared
|
|
assert outcome.reason == "timeout"
|
|
assert outcome.last_stage == Stage.CHECKBOX
|
|
assert page.waits and set(page.waits) == {orchestrator.SOLVE_RETRY_DELAY_MS}
|
|
|
|
|
|
async def test_unknown_provider():
|
|
with pytest.raises(ProviderNotDetected, match="unknown captcha_type 'datadome'"):
|
|
await orchestrator.pass_challenges("datadome", FakePage())
|