import base64 from http import HTTPStatus from json import JSONDecodeError from unittest.mock import AsyncMock, MagicMock import httpx2 import pytest from fastapi import HTTPException from playwright.async_api import TimeoutError as PlaywrightTimeoutError from starlette.testclient import TestClient from main import app from src.challenge import CF_INTERSTITIAL_INDICATORS_SELECTORS from src.endpoints import read_item from src.models import LinkRequest from src.utils import BrowserDepClass, TimeoutTimer, remaining_ms client = TestClient(app) test_websites = [ "https://ext.to/", # "https://www.ygg.re/", "https://extratorrent.st/", "https://speed.cd/login", 'https://www.yggtorrent.top/engine/search?do=search&order=desc&sort=publish_date&name="UNESCAPED"+"DOUBLEQUOTES"&category=2145', "https://1337x.to/home/", ] @pytest.mark.parametrize("website", test_websites) def test_bypass(website: str): """ Tests if the service can bypass cloudflare/DDOS-GUARD on given websites. This test is skipped if the website is not reachable or does not have cloudflare/DDOS-GUARD. """ test_request = httpx2.get( website, ) if ( test_request.status_code >= HTTPStatus.INTERNAL_SERVER_ERROR and "Just a moment..." not in test_request.text ): try: error_details = test_request.json() except JSONDecodeError: error_details = test_request.text pytest.skip( f"Skipping {website} - ({test_request.status_code}) {error_details}" ) response = client.post( "/v1", json=LinkRequest.model_construct( url=website, cmd="request.get", max_timeout=60 ).model_dump(), ) 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(): """ JSON APIs must return 200, not crash on the UA evaluate. Firefox renders application/json in a built-in viewer whose CSP blocks Playwright's eval-based evaluate() (issue #394). The browser must be launched with the viewer disabled so /v1 works and returns the raw JSON. """ url = "https://api.ipify.org?format=json" test_request = httpx2.get(url) if test_request.status_code >= HTTPStatus.INTERNAL_SERVER_ERROR: pytest.skip( f"Skipping JSON API test - upstream error ({test_request.status_code})" ) response = client.post( "/v1", json=LinkRequest.model_construct(url=url, cmd="request.get").model_dump(), ) if response.status_code == HTTPStatus.REQUEST_TIMEOUT: pytest.skip("Skipping JSON API test - timed out (upstream issue)") assert response.status_code == HTTPStatus.OK solution = response.json()["solution"] assert solution["userAgent"] assert '"ip"' in solution["response"] def test_health_check(): """ Tests the health check endpoint. This test ensures that the health check endpoint returns HTTPStatus.OK. """ response = client.get("/health") assert response.status_code == HTTPStatus.OK def test_pdf_handling(): """Tests that PDF URLs return the raw PDF bytes, not the Firefox viewer HTML.""" pdf_url = "https://mondaymandala.com/wp-content/uploads/Mickey-And-Minnie-Mouse-Holding-An-Easter-Egg-Basket-Coloring-Page-For-Kids.pdf" response = client.post( "/v1", json=LinkRequest.model_construct(url=pdf_url, cmd="request.get").model_dump(), ) if response.status_code == HTTPStatus.REQUEST_TIMEOUT: pytest.skip("Skipping PDF test - timed out (upstream issue)") assert response.status_code == HTTPStatus.OK solution = response.json()["solution"] if solution.get("contentType") != "application/pdf": pytest.skip( "Skipping PDF test - PDF bytes could not be fetched (upstream issue)" ) assert solution["response"] # non-empty base64 decoded = base64.b64decode(solution["response"]) assert decoded[:5] == b"%PDF-" @pytest.mark.parametrize( ("payload", "expected"), [ ({"max_timeout": 60}, 60), # native API: seconds ({"maxTimeout": 60}, 60), # FlareSolverr alias, seconds-range value ({"maxTimeout": 60000}, 60), # FlareSolverr alias: milliseconds ({"maxTimeout": 55000}, 55), ({"maxTimeout": 1000}, 1), ({}, 60), # default ], ) def test_max_timeout_normalization(payload: dict, expected: int): """MaxTimeout must accept FlareSolverr's milliseconds while keeping seconds.""" request = LinkRequest(url="https://example.com", **payload) assert request.max_timeout == expected def fake_dep( *, fail_states: set[str] | None = None, challenged: bool = False, marker_counts: list[int] | None = None, widget_box: dict[str, float] | None = None, user_agent: str | None = "UnitTestBrowser/1.0", ) -> BrowserDepClass: """Build a browser dependency pair backed by mocks.""" page = AsyncMock() page.url = "https://example.test/login" page.goto.return_value = MagicMock( status=HTTPStatus.OK, headers={"content-type": "text/html"}, request=MagicMock(headers={"user-agent": user_agent} if user_agent else {}), ) page.title.return_value = "Login" page.content.return_value = "Login" remaining = list(marker_counts or []) def count_for(selector: str) -> int: """Answer the marker check from the script, else from `challenged`.""" if selector not in CF_INTERSTITIAL_INDICATORS_SELECTORS or not remaining: return 1 if challenged else 0 return remaining.pop(0) if len(remaining) > 1 else remaining[0] def locator(selector: str) -> MagicMock: handle = MagicMock() handle.count = AsyncMock(side_effect=lambda: count_for(selector)) handle.first.scroll_into_view_if_needed = AsyncMock(return_value=None) handle.first.bounding_box = AsyncMock(return_value=widget_box) handle.first.input_value = AsyncMock(return_value="") return handle page.locator = MagicMock(side_effect=locator) def wait_for_load_state(state: str, **_kwargs: object) -> None: """Fail the wait when asked for a configured state.""" if state in (fail_states or set()): message = "load state wait timed out" raise PlaywrightTimeoutError(message) page.wait_for_load_state.side_effect = wait_for_load_state context = AsyncMock() context.cookies.return_value = [] return BrowserDepClass(page=page, context=context) @pytest.mark.asyncio async def test_networkidle_timeout_after_domcontentloaded_returns_content(): """Pages that never go idle after DOM load must still return their content.""" dep = fake_dep(fail_states={"networkidle"}) response = await read_item( LinkRequest(url="https://example.test/login"), dep, ) assert response.status == "ok" assert response.solution.response == "Login" @pytest.mark.asyncio async def test_domcontentloaded_timeout_returns_408(): """Fatal timeouts during initial page load still return a controlled 408.""" with pytest.raises(HTTPException) as exc: await read_item( LinkRequest(url="https://example.test/login"), fake_dep(fail_states={"domcontentloaded"}), ) assert exc.value.status_code == HTTPStatus.REQUEST_TIMEOUT @pytest.mark.asyncio async def test_status_is_always_ok_like_flaresolverr(): """FlareSolverr hardcodes 200 because Selenium cannot report the real code.""" dep = fake_dep() dep.page.goto.return_value = MagicMock( status=HTTPStatus.FORBIDDEN, headers={"content-type": "text/html"}, request=MagicMock(headers={"user-agent": "UnitTestBrowser/1.0"}), ) response = await read_item(LinkRequest(url="https://example.test/login"), dep) assert response.solution.status == HTTPStatus.OK def test_exhausted_budget_never_disables_playwright_timeouts(): """Playwright reads timeout=0 as no timeout at all, so the floor must hold.""" spent = TimeoutTimer(duration=0) assert spent.remaining() == 0 assert remaining_ms(spent) > 0 @pytest.mark.asyncio async def test_missing_user_agent_header_is_not_a_500(): """A request without a user-agent header degrades to empty, never a 500 (#394).""" response = await read_item( LinkRequest(url="https://example.test/login"), fake_dep(user_agent=None), ) assert response.status == "ok" assert response.solution.user_agent == "" @pytest.mark.asyncio async def test_checkbox_is_clicked_while_the_challenge_is_up(): """A measurable widget gets a humanised press, not a raw synthetic click.""" dep = fake_dep( challenged=True, marker_counts=[1, 1, 0], widget_box={"x": 100.0, "y": 200.0, "width": 300.0, "height": 60.0}, ) response = await read_item( LinkRequest(url="https://example.test/login", max_timeout=5), dep ) assert response.status == "ok" dep.page.mouse.move.assert_awaited_once_with(125.0, 230.0) dep.page.mouse.down.assert_awaited_once() dep.page.mouse.up.assert_awaited_once() @pytest.mark.asyncio async def test_challenge_that_clears_on_its_own_is_never_clicked(): """A challenge is over when its markup goes, and until then we keep our hands off.""" dep = fake_dep( challenged=True, marker_counts=[1, 0], widget_box={"x": 100.0, "y": 200.0, "width": 300.0, "height": 60.0}, ) response = await read_item( LinkRequest(url="https://example.test/login", max_timeout=5), dep ) assert response.status == "ok" dep.page.mouse.down.assert_not_awaited() @pytest.mark.asyncio 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]) with pytest.raises(HTTPException) as exc: await read_item( LinkRequest(url="https://example.test/login", max_timeout=2), dep ) assert exc.value.status_code == HTTPStatus.REQUEST_TIMEOUT @pytest.mark.asyncio async def test_marker_vanishing_mid_navigation_is_not_a_solved_challenge(): """The marker drops out between challenge rounds; one clear read proves nothing.""" dep = fake_dep(challenged=True, marker_counts=[1, 0, 1]) with pytest.raises(HTTPException) as exc: await read_item( LinkRequest(url="https://example.test/login", max_timeout=2), dep ) assert exc.value.status_code == HTTPStatus.REQUEST_TIMEOUT