Files
robotex/tests/unit/test_orchestrator.py
T
protokeyandClaude Opus 5.5 0ae5b0c83f test: юнит-тесты и сквозные тесты с Chrome (#21)
- 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>
2026-10-13 18:10:21 +04:00

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())