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 <noreply@anthropic.com>
This commit is contained in:
ThePhaseless
2026-08-18 02:45:19 +02:00
co-authored by Claude Opus 5
parent 61db8d82ee
commit 3dcf529609
3 changed files with 7 additions and 28 deletions
+1 -16
View File
@@ -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)
+5 -11
View File
@@ -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 == "<html><title>Login</title></html>"
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(
+1 -1
View File
@@ -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