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>
This commit is contained in:
1 parent
58100fa45d
commit
0ae5b0c83f
13 files changed
+773
-85
No files matched your search
@@ -0,0 +1,75 @@
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from api.schemas import ActionRequest, SolveRequest, WAIT_FOR_DEFAULT_TIMEOUT
|
||||
|
||||
|
||||
def solve(**kwargs) -> SolveRequest:
|
||||
return SolveRequest(**{"url": "https://example.com", "actions": [], **kwargs})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("action, error", [
|
||||
({"type": "nope"}, "unknown action type"),
|
||||
({"type": "wait_for"}, "requires: selector"),
|
||||
({"type": "fill", "selector": "#q"}, "requires: value"),
|
||||
({"type": "fill", "selector": "#q", "value": ""}, "requires: value"),
|
||||
({"type": "click"}, "requires: selector"),
|
||||
({"type": "delay"}, "requires: value"),
|
||||
({"type": "delay", "value": "abc"}, "integer"),
|
||||
({"type": "delay", "value": "601"}, "between 0 and 600"),
|
||||
({"type": "wait_for", "selector": "a", "timeout": 0}, "between 1 and 7200"),
|
||||
({"type": "wait_for", "selector": "a", "timeout": 7201}, "between 1 and 7200"),
|
||||
({"type": "captcha", "selector": "#c", "value": "datadome"}, "captcha value must be one of"),
|
||||
])
|
||||
def test_invalid_action(action, error):
|
||||
with pytest.raises(ValidationError, match=error):
|
||||
ActionRequest(**action)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("action", [
|
||||
{"type": "wait_for", "selector": "a", "timeout": 10},
|
||||
{"type": "fill", "selector": "#q", "value": "x"},
|
||||
{"type": "delay", "value": "0"},
|
||||
{"type": "pass_challenge"},
|
||||
{"type": "captcha", "selector": "#c", "value": "hcaptcha"},
|
||||
{"type": "inner_html", "selector": "h1"},
|
||||
])
|
||||
def test_valid_action(action):
|
||||
assert ActionRequest(**action).type == action["type"]
|
||||
|
||||
|
||||
def test_wait_for_default_timeout():
|
||||
assert ActionRequest(type="wait_for", selector="a").timeout == WAIT_FOR_DEFAULT_TIMEOUT
|
||||
|
||||
|
||||
@pytest.mark.parametrize("proxy", [
|
||||
"http://host:8080",
|
||||
"https://user:pass@host:443",
|
||||
"socks5://10.0.0.1:1080",
|
||||
])
|
||||
def test_valid_proxy(proxy):
|
||||
assert solve(proxy=proxy).proxy == proxy
|
||||
|
||||
|
||||
@pytest.mark.parametrize("proxy, error", [
|
||||
("host:8080", "scheme"),
|
||||
("ftp://host:21", "scheme"),
|
||||
("http://host", "host:port"),
|
||||
("http://:8080", "host:port"),
|
||||
("http://host:99999", "invalid proxy url"),
|
||||
])
|
||||
def test_invalid_proxy(proxy, error):
|
||||
with pytest.raises(ValidationError, match=error):
|
||||
solve(proxy=proxy)
|
||||
|
||||
|
||||
def test_proxy_optional():
|
||||
assert solve().proxy is None
|
||||
assert solve(proxy="").proxy is None
|
||||
|
||||
|
||||
def test_captcha_action_requires_key():
|
||||
action = {"type": "captcha", "selector": "#c", "value": "recaptcha"}
|
||||
with pytest.raises(ValidationError, match="requires request field 'captcha'"):
|
||||
solve(actions=[action])
|
||||
assert solve(actions=[action], captcha="key").captcha == "key"
|
||||
Reference in new issue
Block a user