Files

203 lines
6.8 KiB
Python

from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
from core.http.errors import ApiProblemError
from core.config.settings import config
from v1.auth.gateway import SupabaseAuthGateway
from v1.auth.schemas import (
AuthUser,
OtpSendRequest,
PhoneSessionCreateRequest,
SessionResponse,
SessionRefreshRequest,
)
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,
)
monkeypatch.setattr(config.runtime, "environment", "test")
return SupabaseAuthGateway(), mock_client, mock_admin_client
@pytest.mark.asyncio
async def test_send_otp_sets_should_create_user(
self, gateway: tuple[SupabaseAuthGateway, MagicMock, MagicMock]
) -> None:
sut, mock_client, _ = gateway
mock_sign_in_with_otp = MagicMock()
mock_client.auth.sign_in_with_otp = mock_sign_in_with_otp
await sut.send_otp(OtpSendRequest(phone="+8613812345678"))
mock_sign_in_with_otp.assert_called_once_with(
{
"phone": "+8613812345678",
"options": {"should_create_user": True},
}
)
@pytest.mark.asyncio
async def test_create_phone_session_uses_verify_otp(
self, gateway: tuple[SupabaseAuthGateway, MagicMock, MagicMock]
) -> None:
sut, mock_client, _ = gateway
verify_response = SimpleNamespace(
session=SimpleNamespace(
access_token="access",
refresh_token="refresh",
expires_in=3600,
token_type="bearer",
),
user=SimpleNamespace(id="user-1", phone="+8613812345678"),
)
mock_client.auth.verify_otp = MagicMock(return_value=verify_response)
response = await sut.create_phone_session(
PhoneSessionCreateRequest(phone="+8613812345678", token="123456")
)
assert response.user.id == "user-1"
assert response.access_token == "access"
@pytest.mark.asyncio
async def test_create_phone_session_normalizes_phone_without_plus_prefix(
self, gateway: tuple[SupabaseAuthGateway, MagicMock, MagicMock]
) -> None:
sut, mock_client, _ = gateway
verify_response = SimpleNamespace(
session=SimpleNamespace(
access_token="access",
refresh_token="refresh",
expires_in=3600,
token_type="bearer",
),
user=SimpleNamespace(id="user-1", phone="14155552671"),
)
mock_client.auth.verify_otp = MagicMock(return_value=verify_response)
response = await sut.create_phone_session(
PhoneSessionCreateRequest(phone="+14155552671", token="123456")
)
assert response.user.phone == "+14155552671"
@pytest.mark.asyncio
async def test_refresh_session_maps_invalid_token(
self, gateway: tuple[SupabaseAuthGateway, MagicMock, MagicMock]
) -> None:
sut, mock_client, _ = gateway
mock_client.auth.refresh_session = MagicMock(
return_value=SimpleNamespace(session=None, user=None)
)
with pytest.raises(ApiProblemError) as exc_info:
await sut.refresh_session(SessionRefreshRequest(refresh_token="bad"))
assert exc_info.value.status_code == 401
@pytest.mark.asyncio
async def test_refresh_session_uses_dev_flow_in_dev_environment(
self,
gateway: tuple[SupabaseAuthGateway, MagicMock, MagicMock],
monkeypatch: pytest.MonkeyPatch,
) -> None:
sut, mock_client, _ = gateway
monkeypatch.setattr(config.runtime, "environment", "dev")
expected = SessionResponse(
access_token="dev-access",
refresh_token="dev-refresh",
expires_in=3600,
token_type="bearer",
user=AuthUser(id="user-dev", phone="+8613812345678"),
)
async def _fake_refresh_dev_phone_session(*, request: SessionRefreshRequest) -> SessionResponse:
assert request.refresh_token == "dev-refresh"
return expected
monkeypatch.setattr(
"v1.auth.gateway.refresh_dev_phone_session",
_fake_refresh_dev_phone_session,
)
response = await sut.refresh_session(
SessionRefreshRequest(refresh_token="dev-refresh")
)
assert response == expected
assert mock_client.auth.refresh_session.call_count == 0
@pytest.mark.asyncio
async def test_get_user_by_phone_uses_in_memory_cache(
self,
gateway: tuple[SupabaseAuthGateway, MagicMock, MagicMock],
monkeypatch: pytest.MonkeyPatch,
) -> None:
sut, _, _ = gateway
user = SimpleNamespace(
id="user-1",
phone="+8613811112222",
created_at="2026-03-16T00:00:00Z",
phone_confirmed_at=None,
)
list_calls = {"count": 0}
def _fake_list_auth_users(_client: object) -> list[SimpleNamespace]:
list_calls["count"] += 1
return [user]
monkeypatch.setattr("v1.auth.gateway._list_auth_users", _fake_list_auth_users)
first = await sut.get_user_by_phone("+8613811112222")
second = await sut.get_user_by_phone("+8613811112222")
assert first.id == "user-1"
assert second.phone == "+8613811112222"
assert list_calls["count"] == 1
@pytest.mark.asyncio
async def test_search_user_ids_by_phone_supports_suffix_query(
self,
gateway: tuple[SupabaseAuthGateway, MagicMock, MagicMock],
monkeypatch: pytest.MonkeyPatch,
) -> None:
sut, _, _ = gateway
users = [
SimpleNamespace(
id="user-cn",
phone="+8613811112222",
created_at="2026-03-16T00:00:00Z",
phone_confirmed_at=None,
),
SimpleNamespace(
id="user-us",
phone="+14155552671",
created_at="2026-03-16T00:00:00Z",
phone_confirmed_at=None,
),
]
monkeypatch.setattr("v1.auth.gateway._list_auth_users", lambda _client: users)
matched_cn = await sut.search_user_ids_by_phone("13811112222")
matched_us = await sut.search_user_ids_by_phone("4155552671")
assert matched_cn == ["user-cn"]
assert matched_us == ["user-us"]