Files
shelfmark/tests/e2e/platform/suite/test_cluster_bypasser.py
T
CaliBrain 03e219eb43 Let the bypasser solve bot challenges on Anna's Archive search (#1198)
Anna's Archive put a DDoS-Guard JS challenge in front of /search: the
homepage still returns 200, but /search and /md5/<id> answer 403 on
every mirror (.gl, .pk, .gd all confirmed). Search fetched both with
allow_bypasser_fallback=False, which rotates mirrors on a 403 instead of
invoking the bypasser, so it walked the whole mirror list, exhausted it,
and surfaced "Unable to reach download source. Network restricted or
mirrors are blocked." as a 503 on every query. Adding mirrors could not
help — they sit behind the same gate — and neither could USE_CF_BYPASS,
since search never reached that branch.
Fetch search and the detail page with allow_bypasser_fallback=True so a
403 hands over to the bypasser, which already detects this challenge
(DDOS_GUARD_INDICATORS matches the live page). Echoing the __ddg cookies
back does not clear it; it needs real JS execution. The download-count
fetch keeps allow_bypasser_fallback=False: it is decoration on the
details modal and not worth holding the modal open for a browser solve.
Fixes #1196
2026-08-13 13:06:11 -04:00

101 lines
3.9 KiB
Python

"""Cluster 1 — Cloudflare bypasser wiring + gated-search behavior.
AA search/detail used to be fetched with ``allow_bypasser_fallback=False``, so a
search behind a Cloudflare gate returned 503 no matter how the bypasser was
configured — which left search dead when AA put DDoS-Guard in front of ``/search``.
Both now pass ``allow_bypasser_fallback=True``: a 403 switches straight to the
bypasser instead of rotating to another mirror behind the same gate.
So these tests assert:
* the external bypasser is configured from env,
* a CF-gated search *recovers* once the external bypasser is available, and
* with the bypasser off it still fails *cleanly* (a 503 the client can act on,
not a hang or a crash) — the negative control that the gate is not ignored.
Guards: #284 #226 #202 #1030 #410 #369.
"""
from __future__ import annotations
import os
from pathlib import Path
import pytest
import requests
def _boot_log() -> str:
path = os.environ.get("E2E_SHELFMARK_LOG")
if not path or not Path(path).exists():
return ""
return Path(path).read_text(encoding="utf-8", errors="ignore")
def _cf_gated_search_has_no_releases(client) -> bool:
"""A CF-gated AA search must not yield releases (the gate isn't bypassed for
search).
NOTE (observed live): with ``USE_CF_BYPASS=false`` the search is *slow* to fail
— the app retries and can take ~60s, vs a fast 503 when the bypasser is enabled
(potential #1001 "hung on bypass protection"). We bound the wait and treat a
timeout the same as a clean failure: in both cases no releases were obtained,
which is the point of this negative control.
"""
try:
resp = client.get(
"/api/releases",
params={"source": "direct_download", "query": "Mistborn"},
timeout=30,
)
except requests.exceptions.Timeout:
return True # could not complete -> definitively no releases obtained
assert resp.status_code in (200, 404, 500, 503), (
f"CF-gated search returned an unexpected status: {resp.status_code} {resp.text[:200]}"
)
return not client.releases_from(resp)
@pytest.mark.profiles("bypasser-external")
def test_external_bypasser_is_configured(client) -> None:
"""The external (FlareSolverr) bypasser path is selected via env."""
assert client.get("/api/health").status_code == 200
log = _boot_log()
if not log:
pytest.skip("E2E_SHELFMARK_LOG not available")
assert "USING_EXTERNAL_BYPASSER" in log and "EXT_BYPASSER_URL" in log, (
"external bypasser config was not synced from env"
)
@pytest.mark.profiles("bypasser-external")
def test_cf_gated_search_recovers_via_external_bypasser(client) -> None:
"""A CF-gated search is solved through the bypasser instead of returning 503.
The mock FlareSolverr refetches with the clearance cookie, which the cloudflare
role then passes through to the AA origin — so the 403 that used to end the
search now turns into real results.
"""
resp = client.get(
"/api/releases",
params={"source": "direct_download", "query": "Mistborn"},
timeout=120,
)
assert resp.status_code == 200, (
f"CF-gated search did not recover through the external bypasser: "
f"{resp.status_code} {resp.text[:200]}"
)
assert client.releases_from(resp), (
"external bypasser was configured but the gated search returned no releases — "
"the 403 did not switch search over to the bypasser"
)
@pytest.mark.profiles("bypasser-disabled")
def test_cf_gated_search_fails_when_bypasser_off(client) -> None:
"""Negative control: AA behind Cloudflare + bypasser OFF -> no releases, clean
failure. A regression that ignored the gate would wrongly return results."""
assert _cf_gated_search_has_no_releases(client), (
"results returned even though AA is Cloudflare-gated and the bypasser is "
"disabled — the challenge is being ignored (regression for #202/#410)"
)