feature!: параметр user_agent убран, UA всегда родной; docs/Usage.md

- user_agent в запросе → 422 с подсказкой брать UA из ответа: UA, отличный от реального
  Chrome, расходится с userAgentData и движком, Cloudflare такой браузер не пропускает
  (замер: родной 3/3, Chrome 147 и Windows — 0) (#87)
- профиль хранит только отпечаток (экран, DPR), UA не подменяется (#89)
- docs/Usage.md: практики для клиентов API — тот же UA и прокси, что у браузера, сессии,
  cookies, ошибки и повторы, таймауты, пример на httpx
- README, docs/API.md, docs/Introduction.md; тесты под новый контракт

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
protokeyandClaude Opus 5.5 committed 2026-10-13 18:19:08 +04:00
1 parent 0ae5b0c83f
commit fe509c25c3
11 files changed
+202 -125

No files matched your search

+17 -38
View File
@@ -1,36 +1,24 @@
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_fingerprint_generated_once(tmp_path):
first = profile.load_or_create(tmp_path)
assert profile.load_or_create(tmp_path) == first
assert json.loads((tmp_path / profile.SETTINGS_FILE).read_text()) == first
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"]
screen = first["fingerprint"]["screen"]
assert screen["width"] >= profile.MIN_SCREEN[0] and screen["height"] >= profile.MIN_SCREEN[1]
assert settings["fingerprint"]["device_scale_factor"] >= 1
assert first["fingerprint"]["device_scale_factor"] >= 1
assert "user_agent" not in first
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_old_profile_settings_get_fingerprint(tmp_path):
"""Профиль из версии, где сохранялся user_agent клиента: UA отбрасывается, отпечаток создаётся."""
(tmp_path / profile.SETTINGS_FILE).write_text(json.dumps({"user_agent": "First/1"}))
settings = profile.load_or_create(tmp_path)
assert set(settings) == {"fingerprint"}
def test_profiles_get_independent_fingerprints(tmp_path):
@@ -39,7 +27,7 @@ def test_profiles_get_independent_fingerprints(tmp_path):
for i in range(30):
d = tmp_path / str(i)
d.mkdir()
fp = profile.load_or_create(d, None)["fingerprint"]
fp = profile.load_or_create(d)["fingerprint"]
variants.add((fp["screen"]["width"], fp["screen"]["height"], fp["device_scale_factor"]))
assert len(variants) > 1
@@ -50,18 +38,9 @@ def test_generate_falls_back_when_browserforge_fails(monkeypatch):
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
assert profile.generate_fingerprint(attempts=2) == {"screen": {"width": 1920, "height": 1080}, "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},
}
def test_browser_kwargs_do_not_touch_user_agent():
kwargs = profile.browser_kwargs({"fingerprint": {"screen": {"width": 2560, "height": 1440}, "device_scale_factor": 1.25}})
assert kwargs == {"additional_args": {"screen": {"width": 2560, "height": 1440}, "device_scale_factor": 1.25}}
+5
View File
@@ -68,6 +68,11 @@ def test_proxy_optional():
assert solve(proxy="").proxy is None
def test_user_agent_rejected():
with pytest.raises(ValidationError, match="user_agent is not supported"):
solve(user_agent="Mozilla/5.0")
def test_captcha_action_requires_key():
action = {"type": "captcha", "selector": "#c", "value": "recaptcha"}
with pytest.raises(ValidationError, match="requires request field 'captcha'"):
+7 -8
View File
@@ -54,7 +54,6 @@ def client(monkeypatch, tmp_path):
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)
@@ -168,18 +167,18 @@ def test_bad_session_cookie_gets_new_session(client, monkeypatch, tmp_path, bad)
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_user_agent_param_rejected(client):
resp = post(client, user_agent="First/1")
assert resp.status_code == 422
assert "take user_agent from the response" in resp.text
assert FakeSession.calls == []
def test_generated_fingerprint_passed_to_browser(client, monkeypatch):
def test_fingerprint_passed_to_browser_and_ua_untouched(client, monkeypatch):
monkeypatch.setattr(actions, "execute", fake_execute([]))
post(client)
kwargs = FakeSession.calls[0]
assert kwargs["useragent"] == profile.UA_TEMPLATE.format(major="155")
assert "useragent" not in kwargs
assert set(kwargs["additional_args"]) == {"screen", "device_scale_factor"}