from __future__ import annotations from typing import Callable import pytest from fastapi import HTTPException from fastapi.testclient import TestClient from app import app from v1.auth.dependencies import get_auth_service from v1.auth.rate_limit import reset_rate_limit_state from v1.auth.schemas import ( AuthUser, OtpSendRequest, PhoneSessionCreateRequest, SessionRefreshRequest, SessionResponse, ) from v1.auth.service import AuthService @pytest.fixture(autouse=True) def reset_auth_rate_limit_state() -> None: reset_rate_limit_state() @pytest.fixture(autouse=True) def force_in_memory_rate_limit(monkeypatch: pytest.MonkeyPatch) -> None: async def _raise_redis_unavailable() -> None: raise RuntimeError("redis unavailable in integration tests") monkeypatch.setattr( "v1.auth.rate_limit.get_or_init_redis_client", _raise_redis_unavailable, ) class FakeAuthService(AuthService): def __init__(self, token_response: SessionResponse) -> None: self._token_response = token_response async def send_otp(self, request: OtpSendRequest) -> None: if request.phone == "+8613811111111": raise HTTPException(status_code=401, detail="Invalid verification code") return None async def create_phone_session( self, request: PhoneSessionCreateRequest ) -> SessionResponse: if request.token == "000000": raise HTTPException(status_code=401, detail="Invalid verification code") return self._token_response async def refresh_session(self, request: SessionRefreshRequest) -> SessionResponse: raise HTTPException(status_code=401, detail="Invalid refresh token") async def delete_session(self, refresh_token: str | None) -> None: return None def _override_auth_service(service: AuthService) -> Callable[[], AuthService]: def _get_service() -> AuthService: return service return _get_service def _token_response() -> SessionResponse: user = AuthUser(id="user-1", phone="+8613812345678") return SessionResponse( access_token="access", refresh_token="refresh", expires_in=3600, token_type="bearer", user=user, ) def test_send_otp_returns_204() -> None: app.dependency_overrides[get_auth_service] = _override_auth_service( FakeAuthService(_token_response()) ) client = TestClient(app) try: response = client.post( "/api/v1/auth/otp/send", json={"phone": "+8613812345678"}, ) assert response.status_code == 204 finally: app.dependency_overrides = {} def test_phone_session_returns_token_response() -> None: app.dependency_overrides[get_auth_service] = _override_auth_service( FakeAuthService(_token_response()) ) client = TestClient(app) try: response = client.post( "/api/v1/auth/phone-session", json={"phone": "+8613812345678", "token": "123456"}, ) assert response.status_code == 200 body = response.json() assert body["access_token"] == "access" assert body["refresh_token"] == "refresh" assert body["user"]["phone"] == "+8613812345678" finally: app.dependency_overrides = {} def test_phone_session_invalid_token_returns_problem_details() -> None: app.dependency_overrides[get_auth_service] = _override_auth_service( FakeAuthService(_token_response()) ) client = TestClient(app) try: response = client.post( "/api/v1/auth/phone-session", json={"phone": "+8613812345678", "token": "000000"}, ) assert response.status_code == 401 assert response.headers["content-type"].startswith("application/problem+json") finally: app.dependency_overrides = {} def test_legacy_routes_are_removed() -> None: app.dependency_overrides[get_auth_service] = _override_auth_service( FakeAuthService(_token_response()) ) client = TestClient(app) try: assert client.post("/api/v1/auth/verifications", json={}).status_code == 404 assert client.post("/api/v1/auth/verify", json={}).status_code == 404 assert client.post("/api/v1/auth/resend", json={}).status_code == 404 assert client.post("/api/v1/auth/sessions", json={}).status_code == 405 finally: app.dependency_overrides = {} def test_send_otp_phone_rate_limited_after_too_many_attempts() -> None: app.dependency_overrides[get_auth_service] = _override_auth_service( FakeAuthService(_token_response()) ) client = TestClient(app) try: for _ in range(3): ok = client.post( "/api/v1/auth/otp/send", json={"phone": "+8613812345678"}, ) assert ok.status_code == 204 blocked = client.post( "/api/v1/auth/otp/send", json={"phone": "+8613812345678"}, ) assert blocked.status_code == 429 finally: app.dependency_overrides = {} def test_phone_session_rate_limited_after_too_many_attempts( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setattr("v1.auth.router.config.runtime.environment", "production") app.dependency_overrides[get_auth_service] = _override_auth_service( FakeAuthService(_token_response()) ) client = TestClient(app) try: for _ in range(6): blocked = client.post( "/api/v1/auth/phone-session", json={"phone": "+8613812345678", "token": "000000"}, ) assert blocked.status_code == 401 blocked = client.post( "/api/v1/auth/phone-session", json={"phone": "+8613812345678", "token": "000000"}, ) assert blocked.status_code == 429 finally: app.dependency_overrides = {}