Spaces:
Paused
Paused
| """Tests for the YesCaptcha-compatible captcha solver API.""" | |
| from __future__ import annotations | |
| import importlib | |
| import os | |
| import sys | |
| from pathlib import Path | |
| from unittest.mock import AsyncMock | |
| import pytest | |
| PROJECT_ROOT = Path(__file__).resolve().parents[1] | |
| if str(PROJECT_ROOT) not in sys.path: | |
| _ = sys.path.insert(0, str(PROJECT_ROOT)) | |
| from fastapi.testclient import TestClient | |
| def _load_app(*, client_key: str | None = None) -> TestClient: | |
| """Reload modules with fresh env vars and return a test client.""" | |
| os.environ.pop("CLIENT_KEY", None) | |
| os.environ.setdefault("CAPTCHA_BASE_URL", "https://example.com/v1") | |
| os.environ.setdefault("CAPTCHA_API_KEY", "test-key") | |
| os.environ.setdefault("CAPTCHA_MODEL", "gpt-5.4") | |
| os.environ.setdefault("CAPTCHA_MULTIMODAL_MODEL", "qwen3.5-2b") | |
| os.environ.setdefault("BROWSER_HEADLESS", "true") | |
| if client_key is not None: | |
| os.environ["CLIENT_KEY"] = client_key | |
| config_mod = importlib.import_module("src.core.config") | |
| routes_mod = importlib.import_module("src.api.routes") | |
| task_mgr_mod = importlib.import_module("src.services.task_manager") | |
| main_mod = importlib.import_module("src.main") | |
| _ = importlib.reload(config_mod) | |
| _ = importlib.reload(task_mgr_mod) | |
| _ = importlib.reload(routes_mod) | |
| main_mod = importlib.reload(main_mod) | |
| return TestClient(getattr(main_mod, "app")) | |
| ALL_TASK_TYPES = [ | |
| "RecaptchaV3TaskProxyless", | |
| "RecaptchaV3TaskProxylessM1", | |
| "RecaptchaV3TaskProxylessM1S7", | |
| "RecaptchaV3TaskProxylessM1S9", | |
| "RecaptchaV3EnterpriseTask", | |
| "RecaptchaV3EnterpriseTaskM1", | |
| "NoCaptchaTaskProxyless", | |
| "RecaptchaV2TaskProxyless", | |
| "RecaptchaV2EnterpriseTaskProxyless", | |
| "HCaptchaTaskProxyless", | |
| "TurnstileTaskProxyless", | |
| "TurnstileTaskProxylessM1", | |
| "ImageToTextTask", | |
| "ImageToTextTaskMuggle", | |
| "ImageToTextTaskM1", | |
| "HCaptchaClassification", | |
| "ReCaptchaV2Classification", | |
| "FunCaptchaClassification", | |
| "AwsClassification", | |
| ] | |
| def test_health_endpoint() -> None: | |
| client = _load_app() | |
| response = client.get("/api/v1/health") | |
| assert response.status_code == 200 | |
| body = response.json() | |
| assert body["status"] == "ok" | |
| assert "cloud_model" in body | |
| assert "local_model" in body | |
| def test_root_endpoint() -> None: | |
| client = _load_app() | |
| response = client.get("/") | |
| assert response.status_code == 200 | |
| body = response.json() | |
| assert body["service"] == "captcha-solver" | |
| assert body["version"] == "3.0.0" | |
| assert "createTask" in body["endpoints"] | |
| assert isinstance(body["supported_task_types"], list) | |
| def test_root_endpoint_reports_all_supported_types() -> None: | |
| client = _load_app() | |
| task_mgr_mod = importlib.import_module("src.services.task_manager") | |
| mgr = getattr(task_mgr_mod, "task_manager") | |
| for task_type in ALL_TASK_TYPES: | |
| mgr.register_solver(task_type, AsyncMock()) | |
| response = client.get("/") | |
| body = response.json() | |
| assert set(body["supported_task_types"]) == set(ALL_TASK_TYPES) | |
| def test_get_balance() -> None: | |
| client = _load_app() | |
| response = client.post("/getBalance", json={"clientKey": "any"}) | |
| assert response.status_code == 200 | |
| body = response.json() | |
| assert body["errorId"] == 0 | |
| assert body["balance"] > 0 | |
| def test_get_balance_requires_client_key() -> None: | |
| client = _load_app(client_key="secret") | |
| bad = client.post("/getBalance", json={"clientKey": "wrong"}) | |
| good = client.post("/getBalance", json={"clientKey": "secret"}) | |
| assert bad.json()["errorId"] == 1 | |
| assert good.json()["errorId"] == 0 | |
| def test_create_task_unsupported_type() -> None: | |
| client = _load_app() | |
| response = client.post( | |
| "/createTask", | |
| json={ | |
| "clientKey": "any", | |
| "task": {"type": "UnsupportedType", "websiteURL": "https://example.com"}, | |
| }, | |
| ) | |
| body = response.json() | |
| assert body["errorId"] == 1 | |
| assert body["errorCode"] == "ERROR_TASK_NOT_SUPPORTED" | |
| def test_create_task_missing_fields_recaptcha_v3() -> None: | |
| client = _load_app() | |
| task_mgr_mod = importlib.import_module("src.services.task_manager") | |
| mgr = getattr(task_mgr_mod, "task_manager") | |
| mgr.register_solver("RecaptchaV3TaskProxyless", AsyncMock()) | |
| try: | |
| response = client.post( | |
| "/createTask", | |
| json={"clientKey": "any", "task": {"type": "RecaptchaV3TaskProxyless"}}, | |
| ) | |
| body = response.json() | |
| assert body["errorId"] == 1 | |
| assert body["errorCode"] == "ERROR_TASK_PROPERTY_EMPTY" | |
| finally: | |
| mgr._solvers.pop("RecaptchaV3TaskProxyless", None) | |
| def test_create_task_missing_fields_recaptcha_v2() -> None: | |
| client = _load_app() | |
| task_mgr_mod = importlib.import_module("src.services.task_manager") | |
| mgr = getattr(task_mgr_mod, "task_manager") | |
| mgr.register_solver("NoCaptchaTaskProxyless", AsyncMock()) | |
| try: | |
| response = client.post( | |
| "/createTask", | |
| json={"clientKey": "any", "task": {"type": "NoCaptchaTaskProxyless"}}, | |
| ) | |
| body = response.json() | |
| assert body["errorId"] == 1 | |
| assert body["errorCode"] == "ERROR_TASK_PROPERTY_EMPTY" | |
| finally: | |
| mgr._solvers.pop("NoCaptchaTaskProxyless", None) | |
| def test_create_task_missing_fields_hcaptcha() -> None: | |
| client = _load_app() | |
| task_mgr_mod = importlib.import_module("src.services.task_manager") | |
| mgr = getattr(task_mgr_mod, "task_manager") | |
| mgr.register_solver("HCaptchaTaskProxyless", AsyncMock()) | |
| try: | |
| response = client.post( | |
| "/createTask", | |
| json={"clientKey": "any", "task": {"type": "HCaptchaTaskProxyless"}}, | |
| ) | |
| body = response.json() | |
| assert body["errorId"] == 1 | |
| assert body["errorCode"] == "ERROR_TASK_PROPERTY_EMPTY" | |
| finally: | |
| mgr._solvers.pop("HCaptchaTaskProxyless", None) | |
| def test_create_task_missing_fields_turnstile() -> None: | |
| client = _load_app() | |
| task_mgr_mod = importlib.import_module("src.services.task_manager") | |
| mgr = getattr(task_mgr_mod, "task_manager") | |
| mgr.register_solver("TurnstileTaskProxyless", AsyncMock()) | |
| try: | |
| response = client.post( | |
| "/createTask", | |
| json={"clientKey": "any", "task": {"type": "TurnstileTaskProxyless"}}, | |
| ) | |
| body = response.json() | |
| assert body["errorId"] == 1 | |
| assert body["errorCode"] == "ERROR_TASK_PROPERTY_EMPTY" | |
| finally: | |
| mgr._solvers.pop("TurnstileTaskProxyless", None) | |
| def test_create_task_missing_fields_image() -> None: | |
| client = _load_app() | |
| task_mgr_mod = importlib.import_module("src.services.task_manager") | |
| mgr = getattr(task_mgr_mod, "task_manager") | |
| mgr.register_solver("ImageToTextTask", AsyncMock()) | |
| try: | |
| response = client.post( | |
| "/createTask", | |
| json={"clientKey": "any", "task": {"type": "ImageToTextTask"}}, | |
| ) | |
| body = response.json() | |
| assert body["errorId"] == 1 | |
| assert body["errorCode"] == "ERROR_TASK_PROPERTY_EMPTY" | |
| finally: | |
| mgr._solvers.pop("ImageToTextTask", None) | |
| def test_create_task_missing_fields_classification() -> None: | |
| client = _load_app() | |
| task_mgr_mod = importlib.import_module("src.services.task_manager") | |
| mgr = getattr(task_mgr_mod, "task_manager") | |
| mgr.register_solver("HCaptchaClassification", AsyncMock()) | |
| try: | |
| response = client.post( | |
| "/createTask", | |
| json={"clientKey": "any", "task": {"type": "HCaptchaClassification"}}, | |
| ) | |
| body = response.json() | |
| assert body["errorId"] == 1 | |
| assert body["errorCode"] == "ERROR_TASK_PROPERTY_EMPTY" | |
| finally: | |
| mgr._solvers.pop("HCaptchaClassification", None) | |
| def test_create_task_invalid_client_key() -> None: | |
| client = _load_app(client_key="correct-key") | |
| response = client.post( | |
| "/createTask", | |
| json={ | |
| "clientKey": "wrong-key", | |
| "task": { | |
| "type": "RecaptchaV3TaskProxyless", | |
| "websiteURL": "https://example.com", | |
| "websiteKey": "key123", | |
| }, | |
| }, | |
| ) | |
| body = response.json() | |
| assert body["errorId"] == 1 | |
| assert body["errorCode"] == "ERROR_KEY_DOES_NOT_EXIST" | |
| def test_get_task_result_not_found() -> None: | |
| client = _load_app() | |
| response = client.post( | |
| "/getTaskResult", | |
| json={"clientKey": "any", "taskId": "nonexistent-id"}, | |
| ) | |
| body = response.json() | |
| assert body["errorId"] == 1 | |
| assert body["errorCode"] == "ERROR_NO_SUCH_CAPCHA_ID" | |
| def test_create_recaptcha_v3_task_accepted() -> None: | |
| client = _load_app() | |
| task_mgr_mod = importlib.import_module("src.services.task_manager") | |
| mgr = getattr(task_mgr_mod, "task_manager") | |
| mock_solver = AsyncMock(return_value={"gRecaptchaResponse": "tok"}) | |
| mock_solver.solve = mock_solver | |
| mgr.register_solver("RecaptchaV3TaskProxyless", mock_solver) | |
| try: | |
| resp = client.post( | |
| "/createTask", | |
| json={ | |
| "clientKey": "any", | |
| "task": { | |
| "type": "RecaptchaV3TaskProxyless", | |
| "websiteURL": "https://example.com", | |
| "websiteKey": "test-key", | |
| }, | |
| }, | |
| ) | |
| body = resp.json() | |
| assert body["errorId"] == 0 | |
| assert body["taskId"] is not None | |
| finally: | |
| mgr._solvers.pop("RecaptchaV3TaskProxyless", None) | |
| def test_create_turnstile_task_accepted() -> None: | |
| client = _load_app() | |
| task_mgr_mod = importlib.import_module("src.services.task_manager") | |
| mgr = getattr(task_mgr_mod, "task_manager") | |
| mock_solver = AsyncMock(return_value={"token": "cf-tok"}) | |
| mock_solver.solve = mock_solver | |
| mgr.register_solver("TurnstileTaskProxyless", mock_solver) | |
| try: | |
| resp = client.post( | |
| "/createTask", | |
| json={ | |
| "clientKey": "any", | |
| "task": { | |
| "type": "TurnstileTaskProxyless", | |
| "websiteURL": "https://example.com", | |
| "websiteKey": "1x000", | |
| }, | |
| }, | |
| ) | |
| body = resp.json() | |
| assert body["errorId"] == 0 | |
| assert body["taskId"] is not None | |
| finally: | |
| mgr._solvers.pop("TurnstileTaskProxyless", None) | |
| def test_create_classification_task_accepted() -> None: | |
| client = _load_app() | |
| task_mgr_mod = importlib.import_module("src.services.task_manager") | |
| mgr = getattr(task_mgr_mod, "task_manager") | |
| mock_solver = AsyncMock(return_value={"objects": [0, 3]}) | |
| mock_solver.solve = mock_solver | |
| mgr.register_solver("ReCaptchaV2Classification", mock_solver) | |
| try: | |
| resp = client.post( | |
| "/createTask", | |
| json={ | |
| "clientKey": "any", | |
| "task": { | |
| "type": "ReCaptchaV2Classification", | |
| "image": "aGVsbG8=", | |
| "question": "Select traffic lights", | |
| }, | |
| }, | |
| ) | |
| body = resp.json() | |
| assert body["errorId"] == 0 | |
| assert body["taskId"] is not None | |
| finally: | |
| mgr._solvers.pop("ReCaptchaV2Classification", None) | |