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

+16 -9
View File
@@ -3,6 +3,7 @@
make test-browser
"""
import json
import re
import uuid
import pytest
@@ -63,19 +64,17 @@ def test_action_failed_on_real_page(client):
assert data["error"] == "inner_html: selector not found: #missing"
def test_user_agent_once_and_cookies_replace(client):
def test_cookies_replace_profile_cookies(client):
url = "https://httpbin.org/cookies"
read = [{"type": "inner_html", "selector": "pre"}]
cookie = lambda name: {"name": name, "value": "1", "url": "https://httpbin.org", "expires": 4102444800}
first = solve(client, url=url, user_agent="First/1", cookies=[cookie("a")], actions=read)
first = solve(client, url=url, cookies=[cookie("a")], actions=read)
sid = first["session_id"]
assert first["user_agent"] == "First/1"
assert json.loads(first["actions"][0]["result"])["cookies"] == {"a": "1"}
# повторный UA игнорируется; без cookies в запросе профильные сохраняются
second = solve(client, sid, url=url, user_agent="Second/2", actions=read)
assert second["user_agent"] == "First/1"
# без cookies в запросе профильные сохраняются
second = solve(client, sid, url=url, actions=read)
assert json.loads(second["actions"][0]["result"])["cookies"] == {"a": "1"}
# переданные cookies заменяют профильные
@@ -83,7 +82,7 @@ def test_user_agent_once_and_cookies_replace(client):
assert json.loads(third["actions"][0]["result"])["cookies"] == {"b": "1"}
def test_generated_fingerprint_applied_and_stable(client, monkeypatch):
def test_fingerprint_applied_and_stable(client, monkeypatch):
async def page_fp(page):
await page.evaluate(FP_JS)
return json.loads(await page.inner_html("#fp"))
@@ -113,20 +112,28 @@ def test_generated_fingerprint_applied_and_stable(client, monkeypatch):
sids = [solve(client, url="https://example.com")["session_id"] for _ in range(4)]
solve(client, sids[0], url="https://example.com")
major = profile.chrome_major()
fps = {}
for d, measured in results.items():
saved = profile.load(main.USER_DATA_ROOT / d.rsplit("/", 1)[-1])
fp = saved["fingerprint"]
for m in measured:
assert m["screen"] == [fp["screen"]["width"], fp["screen"]["height"], fp["device_scale_factor"]]
assert f"Chrome/{major}." in m["ua"] and f"Google Chrome/{major}" in m["ua_ch"]
# UA родной: версия в строке UA совпадает с userAgentData
major = re.search(r"Google Chrome/(\d+)", m["ua_ch"]).group(1)
assert f"Chrome/{major}." in m["ua"]
assert all(m == measured[0] for m in measured), "fingerprint changed within a profile"
fps[d] = measured[0]
assert len(fps) == 4
print("distinct fingerprints:", len({json.dumps(f, sort_keys=True) for f in fps.values()}), "of", len(fps))
def test_ua_in_response_is_native(client):
"""user_agent в ответе — родной UA браузера, тот, с которым получен cf_clearance."""
data = solve(client, url="https://example.com")
assert re.fullmatch(r"Mozilla/5\.0 \(X11; Linux x86_64\) .* Chrome/\d+\.0\.0\.0 Safari/537\.36", data["user_agent"])
assert "Headless" not in data["user_agent"]
def test_cloudflare(client):
data = solve(client, url=CF_URL, captcha_type="cloudflare", timeout=120, actions=[
{"type": "wait_for", "selector": CF_SELECTOR, "timeout": 60},