From 3dcf5296098ca737ca30cc913c35a8948b45af27 Mon Sep 17 00:00:00 2001 From: ThePhaseless Date: Tue, 18 Aug 2026 02:45:19 +0200 Subject: [PATCH] refactor: drop the solver and prove the challenge page is gone Nothing calls ClickSolver any more now that the checkbox is clicked through the mouse, so take it out of the browser dependency instead of leaving it constructed but unused. Assert on the returned body as well: a 200 alone passed even when the interstitial itself was handed back, which is the defect this branch fixes. Judge on _cf_chl_opt, since Cloudflare keeps serving cdn-cgi/challenge-platform as a beacon on pages that are already cleared. Co-Authored-By: Claude Opus 5 --- src/utils.py | 17 +---------------- tests/main_test.py | 16 +++++----------- tests/owui_test.py | 2 +- 3 files changed, 7 insertions(+), 28 deletions(-) diff --git a/src/utils.py b/src/utils.py index baba58c..aca7d52 100644 --- a/src/utils.py +++ b/src/utils.py @@ -6,16 +6,11 @@ from typing import Annotated, NamedTuple, cast from fastapi import Header from invisible_playwright.async_api import InvisiblePlaywright from playwright.async_api import Browser, BrowserContext, Page -from playwright_captcha import ( - ClickSolver, - FrameworkType, -) from pydantic import BaseModel, Field from src.consts import ( BROWSER_LOCALE, LOG_LEVEL, - MAX_ATTEMPTS, PROXY_PASSWORD, PROXY_SERVER, PROXY_USERNAME, @@ -46,7 +41,6 @@ class TimeoutTimer(BaseModel): class BrowserDepClass(NamedTuple): page: Page - solver: ClickSolver context: BrowserContext @@ -106,13 +100,4 @@ async def get_browser( browser = cast("Browser", browser_raw) context = await browser.new_context() page = await context.new_page() - solver = ClickSolver( - framework=FrameworkType.PLAYWRIGHT, - page=page, - max_attempts=MAX_ATTEMPTS, - attempt_delay=1, - ) - try: - yield BrowserDepClass(page, solver, context) - finally: - await solver.cleanup() + yield BrowserDepClass(page, context) diff --git a/tests/main_test.py b/tests/main_test.py index 6a1a1d3..f2915ca 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -10,10 +10,6 @@ from playwright.async_api import TimeoutError as PlaywrightTimeoutError from playwright_captcha.solvers.click.cloudflare.utils.detection import ( CF_INTERSTITIAL_INDICATORS_SELECTORS, ) -from playwright_captcha.utils.exceptions import ( - CaptchaDetectionError, - CaptchaSolvingError, -) from starlette.testclient import TestClient from main import app @@ -63,6 +59,9 @@ def test_bypass(website: str): ) assert response.status_code == HTTPStatus.OK + solution = response.json()["solution"] + assert "_cf_chl_opt" not in solution["response"] + assert "__cf_chl" not in solution["url"] def test_json_api(): @@ -148,7 +147,7 @@ def fake_dep( challenged: bool = False, marker_counts: list[int] | None = None, ) -> BrowserDepClass: - """Build a browser dependency triple backed by mocks.""" + """Build a browser dependency pair backed by mocks.""" page = AsyncMock() page.url = "https://example.test/login" page.goto.return_value = MagicMock( @@ -186,7 +185,7 @@ def fake_dep( context = AsyncMock() context.cookies.return_value = [] - return BrowserDepClass(page=page, solver=AsyncMock(), context=context) + return BrowserDepClass(page=page, context=context) @pytest.mark.asyncio @@ -201,7 +200,6 @@ async def test_networkidle_timeout_after_domcontentloaded_returns_content(): assert response.status == "ok" assert response.solution.status == HTTPStatus.OK assert response.solution.response == "Login" - dep.solver.solve_captcha.assert_not_called() @pytest.mark.asyncio @@ -239,9 +237,6 @@ async def test_user_agent_survives_csp_blocked_evaluate(): async def test_challenge_that_clears_is_reported_as_success(): """A challenge is over when its markup goes, not when the solver says so.""" dep = fake_dep(challenged=True, marker_counts=[1, 0]) - dep.solver.solve_captcha.side_effect = CaptchaSolvingError( - "challenge still present" - ) response = await read_item( LinkRequest(url="https://example.test/login", max_timeout=5), dep @@ -255,7 +250,6 @@ async def test_challenge_that_clears_is_reported_as_success(): async def test_challenge_that_never_clears_returns_408(): """A challenge still up when the budget runs out is a timeout, not a 500.""" dep = fake_dep(challenged=True, marker_counts=[1]) - dep.solver.solve_captcha.side_effect = CaptchaDetectionError("iframes not found") with pytest.raises(HTTPException) as exc: await read_item( diff --git a/tests/owui_test.py b/tests/owui_test.py index 3d51a4f..60d3eae 100644 --- a/tests/owui_test.py +++ b/tests/owui_test.py @@ -89,7 +89,7 @@ def fake_dep(*, html: str = ARTICLE_HTML) -> BrowserDepClass: raise PlaywrightTimeoutError(message) page.wait_for_load_state.side_effect = wait_for_load_state - return BrowserDepClass(page=page, solver=AsyncMock(), context=AsyncMock()) + return BrowserDepClass(page=page, context=AsyncMock()) @pytest.mark.asyncio