"""/v1/solve поверх очереди: контракт v1 не изменился (#84).""" import logging import uuid import pytest import main from api import actions from worker import windows from worker.windows import Outcome from .conftest import FakeSession, fake_execute def post(env, **body): return env.client.post("/v1/solve", json={"url": "https://example.com", "actions": [], **body}) def test_ok(env, monkeypatch): monkeypatch.setattr(actions, "execute", fake_execute([{"type": "wait_for", "ok": True, "duration_ms": 5}])) resp = post(env, 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"] == "page" 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(env): data = post(env).json() assert "html" not in data and "screenshot" not in data assert data["actions"] == [] def test_action_failed(env, 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(env, 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(env, monkeypatch): monkeypatch.setattr(actions, "execute", fake_execute(exc=actions.ChallengeFailed("cloudflare not passed: timeout"))) data = post(env, 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(env): FakeSession.error = RuntimeError("net::ERR_PROXY_CONNECTION_FAILED") data = post(env).json() assert data == { "status": "err", "code": "request_failed", "error": "net::ERR_PROXY_CONNECTION_FAILED", "actions": [], "duration_ms": data["duration_ms"], } @pytest.mark.parametrize("workers", ["busy", "none"]) def test_no_slots(env, workers): if workers == "busy": env.heartbeat(free_slots=0) else: env.redis.flushall() resp = post(env) 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 env.messages == [] def test_not_picked_up_in_time_is_503_and_cancelled(env, monkeypatch): env.auto = False monkeypatch.setattr(main, "V1_QUEUE_WAIT", 0) resp = post(env) assert resp.status_code == 503 assert resp.json()["code"] == "no_slots" # воркер позже достаёт сообщение — отменённую задачу не выполняет env.deliver_pending() assert env.outcomes == [Outcome.SKIPPED] assert FakeSession.instances == [] def test_slot_released_after_request(env): FakeSession.error = RuntimeError("boom") for _ in range(3): assert post(env).status_code == 200 assert env.pool.used == 0 and env.pool.windows == {} assert all(s.closed for s in FakeSession.instances) def test_invalid_request_is_422_without_task(env): resp = post(env, actions=[{"type": "nope"}]) assert resp.status_code == 422 assert env.messages == [] def test_session_cookie_reused(env): sid = str(uuid.uuid4()) env.client.cookies.set("session_id", sid) resp = post(env) assert resp.cookies["session_id"] == sid assert FakeSession.instances[0].kwargs["user_data_dir"] == str(env.profiles / sid) @pytest.mark.parametrize("bad", ["../../etc", "not-a-uuid", "/tmp/x"]) def test_bad_session_cookie_gets_new_session(env, bad): env.client.cookies.set("session_id", bad) resp = post(env) sid = resp.cookies["session_id"] assert sid != bad and uuid.UUID(sid) assert [p.name for p in env.profiles.iterdir()] == [sid] def test_user_agent_param_rejected(env): resp = post(env, user_agent="First/1") assert resp.status_code == 422 assert "take user_agent from the response" in resp.text assert env.messages == [] def test_fingerprint_passed_to_browser_and_ua_untouched(env): post(env) kwargs = FakeSession.instances[0].kwargs assert "useragent" not in kwargs assert set(kwargs["additional_args"]) == {"screen", "device_scale_factor"} def test_cookies_replace_profile_cookies_only_when_passed(env): post(env) post(env, cookies=[{"name": "a", "value": "1", "url": "https://example.com"}]) post(env, cookies=[]) assert [s.kwargs["clear_cookies"] for s in FakeSession.instances] == [False, True, True] def test_proxy_passed_and_geo_resolved(env, monkeypatch): seen = {} async def fake_geo(proxy): seen["proxy"] = proxy return {"locale": "de-DE", "timezone_id": "Europe/Berlin"} monkeypatch.setattr(windows, "resolve_geo", fake_geo) post(env, proxy="http://u:p@host:3128") kwargs = FakeSession.instances[0].kwargs 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(env, monkeypatch): async def fail_geo(proxy): raise AssertionError("geo must not be resolved without proxy") monkeypatch.setattr(windows, "resolve_geo", fail_geo) post(env) assert FakeSession.instances[0].kwargs["proxy"] is None def test_secrets_not_logged_nor_stored(env, monkeypatch, caplog): async def no_geo(proxy): return None monkeypatch.setattr(windows, "resolve_geo", no_geo) caplog.set_level(logging.DEBUG) post( env, 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"}, {"type": "typing", "selector": "#p", "value": "PassSecret5"}, ], ) log = caplog.text assert "Incoming payload" in log and "Outgoing payload" in log for secret in ("someuser", "TopSecret1", "KEYSECRET2", "CookieSecret3", "PassSecret4", "PassSecret5", "secret-cookie", "page"): assert secret not in log # в Redis — только результат (там cookies и html страницы), но не секреты запроса stored = " ".join(str(env.redis.hgetall(k)) for k in env.redis.keys("*task*")) for secret in ("TopSecret1", "KEYSECRET2", "PassSecret4"): assert secret not in stored def test_health(env): data = env.client.get("/health").json() assert data["status"] == "ok" assert (data["browser_slots"], data["free_slots"]) == (2, 2) assert [w["worker_id"] for w in data["workers"]] == ["w1"]