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,115 @@
|
||||
"""api.actions.execute на поддельной странице: порядок, остановка на ошибке, антибот, длительность."""
|
||||
import pytest
|
||||
|
||||
from antibot.orchestrator import ChallengeOutcome
|
||||
from antibot.base import Stage
|
||||
from api import actions
|
||||
from api.schemas import ActionRequest
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
|
||||
class FakeElement:
|
||||
def __init__(self, html):
|
||||
self.html = html
|
||||
self.hovered = False
|
||||
|
||||
async def inner_html(self):
|
||||
return self.html
|
||||
|
||||
async def hover(self, **kwargs):
|
||||
self.hovered = True
|
||||
|
||||
|
||||
class FakePage:
|
||||
def __init__(self, elements=None):
|
||||
self.elements = elements or {}
|
||||
self.filled = {}
|
||||
|
||||
async def query_selector(self, selector):
|
||||
return self.elements.get(selector)
|
||||
|
||||
async def wait_for_selector(self, selector, timeout):
|
||||
if selector not in self.elements:
|
||||
raise TimeoutError(f"Timeout {timeout}ms exceeded")
|
||||
|
||||
async def fill(self, selector, value):
|
||||
self.filled[selector] = value
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def challenge(monkeypatch):
|
||||
"""Подмена прохождения антибота; outcome можно поменять в тесте."""
|
||||
state = {"outcome": ChallengeOutcome(True, reason="antibot not requested"), "calls": 0}
|
||||
|
||||
async def fake(*args, **kwargs):
|
||||
state["calls"] += 1
|
||||
return state["outcome"]
|
||||
|
||||
monkeypatch.setattr(actions, "pass_challenges", fake)
|
||||
return state
|
||||
|
||||
|
||||
def acts(*items):
|
||||
return [ActionRequest(**a) for a in items]
|
||||
|
||||
|
||||
async def test_actions_run_in_order(challenge):
|
||||
page = FakePage({"h1": FakeElement("Title"), "a": FakeElement("link")})
|
||||
result = await actions.execute(page, acts(
|
||||
{"type": "wait_for", "selector": "h1", "timeout": 1},
|
||||
{"type": "fill", "selector": "#q", "value": "text"},
|
||||
{"type": "mouse_move", "selector": "a"},
|
||||
{"type": "inner_html", "selector": "h1"},
|
||||
))
|
||||
assert [(r["type"], r["ok"], r.get("result")) for r in result] == [
|
||||
("wait_for", True, None), ("fill", True, None), ("mouse_move", True, None), ("inner_html", True, "Title"),
|
||||
]
|
||||
assert all(r["duration_ms"] >= 0 for r in result)
|
||||
assert page.filled == {"#q": "text"}
|
||||
assert page.elements["a"].hovered
|
||||
|
||||
|
||||
async def test_stops_on_first_failure(challenge):
|
||||
page = FakePage({"h1": FakeElement("Title")})
|
||||
result = await actions.execute(page, acts(
|
||||
{"type": "inner_html", "selector": "h1"},
|
||||
{"type": "inner_html", "selector": "#missing"},
|
||||
{"type": "inner_html", "selector": "h1"},
|
||||
))
|
||||
assert [r["ok"] for r in result] == [True, False]
|
||||
assert result[1]["result"] == "selector not found: #missing"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("action", [
|
||||
{"type": "mouse_move", "selector": "#missing"},
|
||||
{"type": "inner_html", "selector": "#missing"},
|
||||
])
|
||||
async def test_selector_not_found(challenge, action):
|
||||
result = await actions.execute(FakePage(), acts(action))
|
||||
assert result == [{"type": action["type"], "ok": False, "result": "selector not found: #missing", "duration_ms": result[0]["duration_ms"]}]
|
||||
|
||||
|
||||
async def test_wait_for_timeout_is_failure(challenge):
|
||||
result = await actions.execute(FakePage(), acts({"type": "wait_for", "selector": "#x", "timeout": 2}))
|
||||
assert result[0]["ok"] is False
|
||||
assert "2000ms" in result[0]["result"]
|
||||
|
||||
|
||||
async def test_empty_actions(challenge):
|
||||
assert await actions.execute(FakePage(), []) == []
|
||||
|
||||
|
||||
async def test_challenge_failed_skips_actions(challenge):
|
||||
challenge["outcome"] = ChallengeOutcome(False, Stage.CHECKBOX, "cloudflare", "timeout")
|
||||
page = FakePage()
|
||||
with pytest.raises(actions.ChallengeFailed, match=r"cloudflare not passed: timeout \(stage: checkbox\)"):
|
||||
await actions.execute(page, acts({"type": "fill", "selector": "#q", "value": "x"}), antibot_name="cloudflare")
|
||||
assert page.filled == {}
|
||||
|
||||
|
||||
async def test_pass_challenge_action(challenge):
|
||||
challenge["outcome"] = ChallengeOutcome(True, reason="passed")
|
||||
result = await actions.execute(FakePage(), acts({"type": "pass_challenge"}), antibot_name="cloudflare")
|
||||
assert result[0]["ok"] and result[0]["result"] == "passed"
|
||||
assert challenge["calls"] == 2
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Цикл 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())
|
||||
@@ -0,0 +1,67 @@
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from api import profile
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def chrome_155(monkeypatch):
|
||||
monkeypatch.setattr(profile, "chrome_major", lambda: "155")
|
||||
|
||||
|
||||
def test_passed_user_agent_is_saved_once(tmp_path):
|
||||
first = profile.load_or_create(tmp_path, "First/1")
|
||||
second = profile.load_or_create(tmp_path, "Second/2")
|
||||
|
||||
assert first == second == {"user_agent": "First/1"}
|
||||
assert json.loads((tmp_path / profile.SETTINGS_FILE).read_text()) == {"user_agent": "First/1"}
|
||||
|
||||
|
||||
def test_generated_fingerprint_without_user_agent(tmp_path):
|
||||
settings = profile.load_or_create(tmp_path, None)
|
||||
|
||||
assert settings["user_agent"] == profile.UA_TEMPLATE.format(major="155")
|
||||
screen = settings["fingerprint"]["screen"]
|
||||
assert screen["width"] >= profile.MIN_SCREEN[0] and screen["height"] >= profile.MIN_SCREEN[1]
|
||||
assert settings["fingerprint"]["device_scale_factor"] >= 1
|
||||
|
||||
|
||||
def test_generated_fingerprint_is_stable_and_ignores_later_user_agent(tmp_path):
|
||||
first = profile.load_or_create(tmp_path, None)
|
||||
assert profile.load_or_create(tmp_path, None) == first
|
||||
assert profile.load_or_create(tmp_path, "Later/1") == first
|
||||
|
||||
|
||||
def test_profiles_get_independent_fingerprints(tmp_path):
|
||||
"""Отпечатки генерируются независимо: из выборки профилей получается больше одного варианта."""
|
||||
variants = set()
|
||||
for i in range(30):
|
||||
d = tmp_path / str(i)
|
||||
d.mkdir()
|
||||
fp = profile.load_or_create(d, None)["fingerprint"]
|
||||
variants.add((fp["screen"]["width"], fp["screen"]["height"], fp["device_scale_factor"]))
|
||||
assert len(variants) > 1
|
||||
|
||||
|
||||
def test_generate_falls_back_when_browserforge_fails(monkeypatch):
|
||||
class Failing:
|
||||
def generate(self, **kwargs):
|
||||
raise ValueError("Cannot generate headers")
|
||||
|
||||
monkeypatch.setattr(profile, "FingerprintGenerator", Failing)
|
||||
fp = profile.generate_fingerprint(attempts=2)
|
||||
assert fp["screen"] == {"width": 1920, "height": 1080}
|
||||
assert fp["device_scale_factor"] == 1
|
||||
|
||||
|
||||
def test_browser_kwargs():
|
||||
assert profile.browser_kwargs({"user_agent": "UA/1"}) == {"useragent": "UA/1"}
|
||||
kwargs = profile.browser_kwargs({
|
||||
"user_agent": "UA/1",
|
||||
"fingerprint": {"screen": {"width": 2560, "height": 1440}, "device_scale_factor": 1.25},
|
||||
})
|
||||
assert kwargs == {
|
||||
"useragent": "UA/1",
|
||||
"additional_args": {"screen": {"width": 2560, "height": 1440}, "device_scale_factor": 1.25},
|
||||
}
|
||||
@@ -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"
|
||||
@@ -0,0 +1,241 @@
|
||||
"""/v1/solve без браузера: движок подменён, проверяются сборка ответа, коды ошибок, слоты, сессии и логи."""
|
||||
import asyncio
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
import main
|
||||
from api import actions, profile
|
||||
|
||||
|
||||
class FakeContext:
|
||||
async def cookies(self):
|
||||
return [{"name": "cf_clearance", "value": "secret-cookie"}]
|
||||
|
||||
|
||||
class FakePage:
|
||||
context = FakeContext()
|
||||
|
||||
async def screenshot(self, type):
|
||||
return b"png"
|
||||
|
||||
async def inner_html(self, selector):
|
||||
return "<body>page</body>"
|
||||
|
||||
async def evaluate(self, js):
|
||||
return "UA/engine"
|
||||
|
||||
|
||||
class FakeSession:
|
||||
"""Вместо AsyncStealthySession: запоминает kwargs и вызывает page_action на FakePage."""
|
||||
calls: list[dict] = []
|
||||
error: Exception | None = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
FakeSession.calls.append(kwargs)
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *exc):
|
||||
return False
|
||||
|
||||
async def fetch(self, url, page_action):
|
||||
if FakeSession.error:
|
||||
raise FakeSession.error
|
||||
return await page_action(FakePage())
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(monkeypatch, tmp_path):
|
||||
FakeSession.calls, FakeSession.error = [], None
|
||||
monkeypatch.setattr(main, "AsyncStealthySession", FakeSession)
|
||||
monkeypatch.setattr(main, "USER_DATA_ROOT", tmp_path)
|
||||
monkeypatch.setattr(main, "_slots", asyncio.Semaphore(2))
|
||||
monkeypatch.setattr(profile, "chrome_major", lambda: "155")
|
||||
return TestClient(main.app)
|
||||
|
||||
|
||||
def fake_execute(result=None, exc=None):
|
||||
async def execute(*args, **kwargs):
|
||||
if exc:
|
||||
raise exc
|
||||
return result
|
||||
return execute
|
||||
|
||||
|
||||
def post(client, **body):
|
||||
return client.post("/v1/solve", json={"url": "https://example.com", "actions": [], **body})
|
||||
|
||||
|
||||
def test_ok(client, monkeypatch):
|
||||
monkeypatch.setattr(actions, "execute", fake_execute([{"type": "wait_for", "ok": True, "duration_ms": 5}]))
|
||||
resp = post(client, html=True, screenshot=True)
|
||||
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["status"] == "ok"
|
||||
assert "code" not in data and "error" not in data
|
||||
assert data["user_agent"] == "UA/engine"
|
||||
assert data["actions"] == [{"type": "wait_for", "ok": True, "duration_ms": 5}]
|
||||
assert data["html"] == "<body>page</body>"
|
||||
assert data["screenshot"] == "cG5n" # base64(b"png")
|
||||
assert data["duration_ms"] >= 0
|
||||
assert uuid.UUID(resp.cookies["session_id"])
|
||||
|
||||
|
||||
def test_html_and_screenshot_only_on_request(client, monkeypatch):
|
||||
monkeypatch.setattr(actions, "execute", fake_execute([]))
|
||||
data = post(client).json()
|
||||
assert "html" not in data and "screenshot" not in data
|
||||
assert data["actions"] == []
|
||||
|
||||
|
||||
def test_action_failed(client, monkeypatch):
|
||||
monkeypatch.setattr(actions, "execute", fake_execute([
|
||||
{"type": "click", "ok": True, "duration_ms": 1},
|
||||
{"type": "inner_html", "ok": False, "result": "selector not found: #x", "duration_ms": 1},
|
||||
]))
|
||||
data = post(client, screenshot=True).json()
|
||||
assert data["status"] == "err"
|
||||
assert data["code"] == "action_failed"
|
||||
assert data["error"] == "inner_html: selector not found: #x"
|
||||
assert data["screenshot"]
|
||||
|
||||
|
||||
def test_challenge_failed(client, monkeypatch):
|
||||
monkeypatch.setattr(actions, "execute", fake_execute(exc=actions.ChallengeFailed("cloudflare not passed: timeout")))
|
||||
data = post(client, screenshot=True).json()
|
||||
assert data["status"] == "err"
|
||||
assert data["code"] == "challenge_failed"
|
||||
assert data["error"] == "cloudflare not passed: timeout"
|
||||
assert data["actions"] == []
|
||||
assert data["cookies"] and data["screenshot"]
|
||||
|
||||
|
||||
def test_request_failed(client):
|
||||
FakeSession.error = RuntimeError("net::ERR_PROXY_CONNECTION_FAILED")
|
||||
data = post(client).json()
|
||||
assert data == {
|
||||
"status": "err", "code": "request_failed", "error": "net::ERR_PROXY_CONNECTION_FAILED",
|
||||
"actions": [], "duration_ms": data["duration_ms"],
|
||||
}
|
||||
|
||||
|
||||
def test_no_slots(client, monkeypatch):
|
||||
monkeypatch.setattr(main, "_slots", asyncio.Semaphore(0))
|
||||
resp = post(client)
|
||||
assert resp.status_code == 503
|
||||
assert resp.headers["retry-after"] == main.RETRY_AFTER
|
||||
assert resp.json() == {"status": "err", "code": "no_slots", "error": "No slots available"}
|
||||
assert FakeSession.calls == []
|
||||
|
||||
|
||||
def test_slot_released_after_request(client, monkeypatch):
|
||||
sem = asyncio.Semaphore(1)
|
||||
monkeypatch.setattr(main, "_slots", sem)
|
||||
monkeypatch.setattr(actions, "execute", fake_execute([]))
|
||||
FakeSession.error = RuntimeError("boom")
|
||||
for _ in range(3):
|
||||
assert post(client).status_code == 200
|
||||
assert not sem.locked()
|
||||
|
||||
|
||||
def test_invalid_request_is_422_without_browser(client):
|
||||
resp = post(client, actions=[{"type": "nope"}])
|
||||
assert resp.status_code == 422
|
||||
assert FakeSession.calls == []
|
||||
|
||||
|
||||
def test_session_cookie_reused(client, monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(actions, "execute", fake_execute([]))
|
||||
sid = str(uuid.uuid4())
|
||||
client.cookies.set("session_id", sid)
|
||||
resp = post(client)
|
||||
assert resp.cookies["session_id"] == sid
|
||||
assert FakeSession.calls[0]["user_data_dir"] == str(tmp_path / sid)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad", ["../../etc", "not-a-uuid", "/tmp/x"])
|
||||
def test_bad_session_cookie_gets_new_session(client, monkeypatch, tmp_path, bad):
|
||||
monkeypatch.setattr(actions, "execute", fake_execute([]))
|
||||
client.cookies.set("session_id", bad)
|
||||
resp = post(client)
|
||||
sid = resp.cookies["session_id"]
|
||||
assert sid != bad and uuid.UUID(sid)
|
||||
assert [p.name for p in tmp_path.iterdir()] == [sid]
|
||||
|
||||
|
||||
def test_user_agent_set_once_per_profile(client, monkeypatch):
|
||||
monkeypatch.setattr(actions, "execute", fake_execute([]))
|
||||
post(client, user_agent="First/1")
|
||||
post(client, user_agent="Second/2")
|
||||
assert [c["useragent"] for c in FakeSession.calls] == ["First/1", "First/1"]
|
||||
|
||||
|
||||
def test_generated_fingerprint_passed_to_browser(client, monkeypatch):
|
||||
monkeypatch.setattr(actions, "execute", fake_execute([]))
|
||||
post(client)
|
||||
kwargs = FakeSession.calls[0]
|
||||
assert kwargs["useragent"] == profile.UA_TEMPLATE.format(major="155")
|
||||
assert set(kwargs["additional_args"]) == {"screen", "device_scale_factor"}
|
||||
|
||||
|
||||
def test_cookies_replace_profile_cookies_only_when_passed(client, monkeypatch):
|
||||
monkeypatch.setattr(actions, "execute", fake_execute([]))
|
||||
post(client)
|
||||
post(client, cookies=[{"name": "a", "value": "1", "url": "https://example.com"}])
|
||||
post(client, cookies=[])
|
||||
assert [c["clear_cookies"] for c in FakeSession.calls] == [False, True, True]
|
||||
|
||||
|
||||
def test_proxy_passed_and_geo_resolved(client, monkeypatch):
|
||||
monkeypatch.setattr(actions, "execute", fake_execute([]))
|
||||
seen = {}
|
||||
|
||||
async def fake_geo(proxy):
|
||||
seen["proxy"] = proxy
|
||||
return {"locale": "de-DE", "timezone_id": "Europe/Berlin"}
|
||||
|
||||
monkeypatch.setattr(main, "resolve_geo", fake_geo)
|
||||
post(client, proxy="http://u:p@host:3128")
|
||||
kwargs = FakeSession.calls[0]
|
||||
assert kwargs["proxy"] == {"server": "http://host:3128", "username": "u", "password": "p"}
|
||||
assert seen["proxy"] == kwargs["proxy"]
|
||||
assert (kwargs["locale"], kwargs["timezone_id"]) == ("de-DE", "Europe/Berlin")
|
||||
|
||||
|
||||
def test_without_proxy_no_geo(client, monkeypatch):
|
||||
monkeypatch.setattr(actions, "execute", fake_execute([]))
|
||||
|
||||
async def fail_geo(proxy):
|
||||
raise AssertionError("geo must not be resolved without proxy")
|
||||
|
||||
monkeypatch.setattr(main, "resolve_geo", fail_geo)
|
||||
post(client)
|
||||
assert FakeSession.calls[0]["proxy"] is None
|
||||
|
||||
|
||||
def test_secrets_not_logged(client, monkeypatch, caplog):
|
||||
monkeypatch.setattr(actions, "execute", fake_execute([]))
|
||||
|
||||
async def no_geo(proxy):
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(main, "resolve_geo", no_geo)
|
||||
caplog.set_level(logging.DEBUG, logger="main")
|
||||
post(
|
||||
client, proxy="http://someuser:TopSecret1@host:3128", captcha="KEYSECRET2", html=True,
|
||||
cookies=[{"name": "sid", "value": "CookieSecret3", "url": "https://example.com"}],
|
||||
actions=[{"type": "fill", "selector": "#p", "value": "PassSecret4"}],
|
||||
)
|
||||
log = caplog.text
|
||||
assert "Incoming payload" in log and "Outgoing payload" in log
|
||||
for secret in ("someuser", "TopSecret1", "KEYSECRET2", "CookieSecret3", "PassSecret4", "secret-cookie", "<body>page"):
|
||||
assert secret not in log
|
||||
|
||||
|
||||
def test_health(client):
|
||||
assert client.get("/health").json() == {"status": "ok", "browser_slots": main.BROWSER_SLOTS, "free_slots": 2}
|
||||
Reference in new issue
Block a user