from __future__ import annotations import base64 import json import time import pytest from httpx import ASGITransport, AsyncClient from app.core.config import get_settings from app.main import app from app.security.captcha import create_captcha_challenge, verify_bypass_token def _assert_png_base64(image_base64: str) -> None: decoded = base64.b64decode(image_base64) assert decoded.startswith(b"\x89PNG\r\n\x1a\n") def _decode_token_payload(token: str) -> dict: payload_b64 = token.split(".")[0] padding = "=" * (-len(payload_b64) % 4) payload_raw = base64.urlsafe_b64decode(payload_b64 + padding) return json.loads(payload_raw.decode("utf-8")) @pytest.mark.anyio async def test_captcha_challenge_generation_produces_image() -> None: challenge = create_captcha_challenge() assert challenge.challenge_token assert challenge.image_base64 assert challenge.answer _assert_png_base64(challenge.image_base64) @pytest.mark.anyio async def test_captcha_verify_rejects_invalid_answer() -> None: challenge = create_captcha_challenge() async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test", ) as client: response = await client.post( "/api/captcha/verify", json={"challengeToken": challenge.challenge_token, "answer": "incorrect"}, ) assert response.status_code == 400 payload = response.json() assert payload.get("error") or payload.get("detail") @pytest.mark.anyio async def test_captcha_verify_returns_bypass_token() -> None: challenge = create_captcha_challenge() client_ip = "203.0.113.9" async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test", ) as client: response = await client.post( "/api/captcha/verify", json={"challengeToken": challenge.challenge_token, "answer": challenge.answer}, headers={"x-forwarded-for": client_ip}, ) assert response.status_code == 200 payload = response.json() assert payload["success"] is True token = payload["bypassToken"] assert token assert verify_bypass_token(token, client_ip) settings = get_settings() decoded_payload = _decode_token_payload(token) assert decoded_payload["ip"] == client_ip assert isinstance(decoded_payload.get("exp"), int) now = int(time.time()) ttl_seconds = settings.captcha_bypass_ttl_seconds assert ttl_seconds - 10 <= decoded_payload["exp"] - now <= ttl_seconds + 10