from __future__ import annotations from types import SimpleNamespace from unittest.mock import MagicMock import pytest from fastapi import HTTPException from v1.auth.gateway import SupabaseAuthGateway from v1.auth.schemas import PasswordResetConfirmRequest, PasswordResetRequest class TestSupabaseAuthGateway: @pytest.fixture def gateway( self, monkeypatch: pytest.MonkeyPatch ) -> tuple[SupabaseAuthGateway, MagicMock, MagicMock]: mock_client = MagicMock() mock_admin_client = MagicMock() monkeypatch.setattr("v1.auth.gateway.supabase_service.get_client", lambda: mock_client) monkeypatch.setattr( "v1.auth.gateway.supabase_service.get_admin_client", lambda: mock_admin_client, ) return SupabaseAuthGateway(), mock_client, mock_admin_client @pytest.mark.asyncio async def test_request_password_reset_calls_email_with_string( self, gateway: tuple[SupabaseAuthGateway, MagicMock, MagicMock] ) -> None: sut, mock_client, _ = gateway mock_reset_email = MagicMock() mock_client.auth.reset_password_email = mock_reset_email request = PasswordResetRequest(email="test@example.com") await sut.request_password_reset(request) mock_reset_email.assert_called_once_with("test@example.com") @pytest.mark.asyncio async def test_request_password_reset_with_redirect( self, gateway: tuple[SupabaseAuthGateway, MagicMock, MagicMock] ) -> None: sut, mock_client, _ = gateway mock_reset_email = MagicMock() mock_client.auth.reset_password_email = mock_reset_email request = PasswordResetRequest( email="test@example.com", redirect_to="http://localhost:3000/reset-password", ) await sut.request_password_reset(request) mock_reset_email.assert_called_once_with( "test@example.com", options={"redirect_to": "http://localhost:3000/reset-password"}, ) @pytest.mark.asyncio async def test_request_password_reset_swallows_auth_error( self, gateway: tuple[SupabaseAuthGateway, MagicMock, MagicMock] ) -> None: sut, mock_client, _ = gateway from supabase import AuthError mock_reset_email = MagicMock(side_effect=AuthError("rate limit exceeded", None)) mock_client.auth.reset_password_email = mock_reset_email request = PasswordResetRequest(email="test@example.com") result = await sut.request_password_reset(request) mock_reset_email.assert_called_once() assert result is None @pytest.mark.asyncio async def test_request_password_reset_extracts_email_from_mapping( self, gateway: tuple[SupabaseAuthGateway, MagicMock, MagicMock] ) -> None: sut, mock_client, _ = gateway mock_reset_email = MagicMock() mock_client.auth.reset_password_email = mock_reset_email request = PasswordResetRequest.model_construct( email={"email": "test@example.com"}, redirect_to=None, ) await sut.request_password_reset(request) mock_reset_email.assert_called_once_with("test@example.com") @pytest.mark.asyncio async def test_request_password_reset_rejects_invalid_email_shape( self, gateway: tuple[SupabaseAuthGateway, MagicMock, MagicMock] ) -> None: sut, _, _ = gateway request = PasswordResetRequest.model_construct( email={"unexpected": "value"}, redirect_to=None, ) with pytest.raises(HTTPException) as exc_info: await sut.request_password_reset(request) assert exc_info.value.status_code == 422 assert exc_info.value.detail == "Invalid email" @pytest.mark.asyncio async def test_confirm_password_reset_updates_password_by_user_id( self, gateway: tuple[SupabaseAuthGateway, MagicMock, MagicMock] ) -> None: sut, mock_client, mock_admin_client = gateway verify_response = SimpleNamespace( session=SimpleNamespace(access_token="access"), user=SimpleNamespace(id="user-1"), ) mock_verify_otp = MagicMock(return_value=verify_response) mock_client.auth.verify_otp = mock_verify_otp mock_update_user_by_id = MagicMock() mock_admin_client.auth.admin = SimpleNamespace( update_user_by_id=mock_update_user_by_id ) request = PasswordResetConfirmRequest( email="test@example.com", token="123456", new_password="newpassword123", ) await sut.confirm_password_reset(request) mock_verify_otp.assert_called_once_with( { "type": "recovery", "email": "test@example.com", "token": "123456", } ) mock_update_user_by_id.assert_called_once_with( "user-1", {"password": "newpassword123"}, ) @pytest.mark.asyncio async def test_confirm_password_reset_raises_when_user_id_missing( self, gateway: tuple[SupabaseAuthGateway, MagicMock, MagicMock] ) -> None: sut, mock_client, _ = gateway verify_response = SimpleNamespace( session=SimpleNamespace(access_token="access"), user=SimpleNamespace(id=""), ) mock_client.auth.verify_otp = MagicMock(return_value=verify_response) request = PasswordResetConfirmRequest( email="test@example.com", token="123456", new_password="newpassword123", ) with pytest.raises(HTTPException) as exc_info: await sut.confirm_password_reset(request) assert exc_info.value.status_code == 401 assert exc_info.value.detail == "Invalid or expired verification code"