test: update integration tests for RESTful routes

This commit is contained in:
qzl
2026-02-26 14:08:10 +08:00
parent 3cab7b03f7
commit 2709d88c68
5 changed files with 360 additions and 384 deletions
+110 -112
View File
@@ -10,19 +10,18 @@ from fastapi.testclient import TestClient
from app import app
from core.auth.models import CurrentUser
from v1.auth.dependencies import get_auth_service
from v1.profile.dependencies import get_current_user
from v1.users.dependencies import get_current_user
from v1.auth.rate_limit import reset_rate_limit_state
from v1.auth.schemas import (
AuthResendCodeResponse,
AuthSignupStartResponse,
AuthTokenResponse,
AuthUserByEmailResponse,
AuthUser,
LoginRequest,
RefreshRequest,
SignupResendRequest,
SignupStartRequest,
SignupVerifyRequest,
SessionCreateRequest,
SessionRefreshRequest,
SessionResponse,
UserByEmailResponse,
VerificationCreateRequest,
VerificationCreateResponse,
VerificationResendRequest,
VerificationVerifyRequest,
)
from v1.auth.service import AuthService
@@ -33,39 +32,39 @@ def reset_auth_rate_limit_state() -> None:
class FakeAuthService(AuthService):
def __init__(self, token_response: AuthTokenResponse) -> None:
def __init__(self, token_response: SessionResponse) -> None:
self._token_response = token_response
async def signup_start(
self, request: SignupStartRequest
) -> AuthSignupStartResponse:
async def create_verification(
self, request: VerificationCreateRequest
) -> VerificationCreateResponse:
if request.email == "exists@example.com":
raise HTTPException(status_code=422, detail="Invalid signup request")
return AuthSignupStartResponse(email=request.email)
return VerificationCreateResponse(email=request.email)
async def signup_verify(self, request: SignupVerifyRequest) -> AuthTokenResponse:
async def verify_verification(
self, request: VerificationVerifyRequest
) -> SessionResponse:
if request.token == "000000":
raise HTTPException(status_code=401, detail="Invalid verification code")
return self._token_response
async def signup_resend(
self, request: SignupResendRequest
) -> AuthResendCodeResponse:
return AuthResendCodeResponse()
async def login(self, request: LoginRequest) -> AuthTokenResponse:
raise HTTPException(status_code=401, detail="Invalid credentials")
async def refresh(self, request: RefreshRequest) -> AuthTokenResponse:
raise HTTPException(status_code=401, detail="Invalid refresh token")
async def logout(self, refresh_token: str | None) -> None:
async def resend_verification(self, request: VerificationResendRequest) -> None:
return None
async def get_user_by_email(self, email: str) -> AuthUserByEmailResponse:
async def create_session(self, request: SessionCreateRequest) -> SessionResponse:
raise HTTPException(status_code=401, detail="Invalid credentials")
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
async def get_user_by_email(self, email: str) -> UserByEmailResponse:
if email == "missing@example.com":
raise HTTPException(status_code=404, detail="User not found")
return AuthUserByEmailResponse(
return UserByEmailResponse(
id="user-1",
email=email,
created_at="2026-02-24T00:00:00Z",
@@ -82,7 +81,7 @@ def _override_auth_service(service: AuthService) -> Callable[[], AuthService]:
def test_signup_start_returns_pending_response() -> None:
user = AuthUser(id="user-1", email="user@example.com")
token_response = AuthTokenResponse(
token_response = SessionResponse(
access_token="access",
refresh_token="refresh",
expires_in=3600,
@@ -96,7 +95,7 @@ def test_signup_start_returns_pending_response() -> None:
client = TestClient(app)
try:
response = client.post(
"/api/v1/auth/signup/start",
"/api/v1/auth/verifications",
json={
"username": "demo",
"email": "user@example.com",
@@ -105,7 +104,6 @@ def test_signup_start_returns_pending_response() -> None:
)
assert response.status_code == 202
body = response.json()
assert body["status"] == "pending_verification"
assert body["email"] == "user@example.com"
finally:
app.dependency_overrides = {}
@@ -113,7 +111,7 @@ def test_signup_start_returns_pending_response() -> None:
def test_signup_verify_returns_token_response() -> None:
user = AuthUser(id="user-1", email="user@example.com")
token_response = AuthTokenResponse(
token_response = SessionResponse(
access_token="access",
refresh_token="refresh",
expires_in=3600,
@@ -127,7 +125,7 @@ def test_signup_verify_returns_token_response() -> None:
client = TestClient(app)
try:
response = client.post(
"/api/v1/auth/signup/verify",
"/api/v1/auth/verifications/verify",
json={"email": "user@example.com", "token": "123456"},
)
assert response.status_code == 200
@@ -141,7 +139,7 @@ def test_signup_verify_returns_token_response() -> None:
def test_signup_resend_returns_generic_message() -> None:
user = AuthUser(id="user-1", email="user@example.com")
token_response = AuthTokenResponse(
token_response = SessionResponse(
access_token="access",
refresh_token="refresh",
expires_in=3600,
@@ -155,21 +153,18 @@ def test_signup_resend_returns_generic_message() -> None:
client = TestClient(app)
try:
response = client.post(
"/api/v1/auth/signup/resend",
"/api/v1/auth/verifications/resend",
json={"email": "user@example.com"},
)
assert response.status_code == 200
body = response.json()
assert (
body["message"] == "If the email exists, a verification code has been sent"
)
assert response.status_code == 204
assert response.content == b""
finally:
app.dependency_overrides = {}
def test_signup_verify_invalid_token_returns_problem_details() -> None:
user = AuthUser(id="user-1", email="user@example.com")
token_response = AuthTokenResponse(
token_response = SessionResponse(
access_token="access",
refresh_token="refresh",
expires_in=3600,
@@ -183,7 +178,7 @@ def test_signup_verify_invalid_token_returns_problem_details() -> None:
client = TestClient(app)
try:
response = client.post(
"/api/v1/auth/signup/verify",
"/api/v1/auth/verifications/verify",
json={"email": "user@example.com", "token": "000000"},
)
assert response.status_code == 401
@@ -198,7 +193,7 @@ def test_signup_verify_invalid_token_returns_problem_details() -> None:
def test_signup_start_existing_email_returns_problem_details() -> None:
user = AuthUser(id="user-1", email="user@example.com")
token_response = AuthTokenResponse(
token_response = SessionResponse(
access_token="access",
refresh_token="refresh",
expires_in=3600,
@@ -212,7 +207,7 @@ def test_signup_start_existing_email_returns_problem_details() -> None:
client = TestClient(app)
try:
response = client.post(
"/api/v1/auth/signup/start",
"/api/v1/auth/verifications",
json={
"username": "demo",
"email": "exists@example.com",
@@ -231,7 +226,7 @@ def test_signup_start_existing_email_returns_problem_details() -> None:
def test_signup_verify_rate_limited_after_too_many_attempts() -> None:
user = AuthUser(id="user-1", email="user@example.com")
token_response = AuthTokenResponse(
token_response = SessionResponse(
access_token="access",
refresh_token="refresh",
expires_in=3600,
@@ -246,13 +241,13 @@ def test_signup_verify_rate_limited_after_too_many_attempts() -> None:
try:
for _ in range(10):
ok = client.post(
"/api/v1/auth/signup/verify",
"/api/v1/auth/verifications/verify",
json={"email": "user@example.com", "token": "123456"},
)
assert ok.status_code == 200
blocked = client.post(
"/api/v1/auth/signup/verify",
"/api/v1/auth/verifications/verify",
json={"email": "user@example.com", "token": "123456"},
)
assert blocked.status_code == 429
@@ -263,39 +258,7 @@ def test_signup_verify_rate_limited_after_too_many_attempts() -> None:
def test_signup_resend_rate_limited_after_too_many_attempts() -> None:
user = AuthUser(id="user-1", email="user@example.com")
token_response = AuthTokenResponse(
access_token="access",
refresh_token="refresh",
expires_in=3600,
token_type="bearer",
user=user,
)
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/signup/resend",
json={"email": "user@example.com"},
)
assert ok.status_code == 200
blocked = client.post(
"/api/v1/auth/signup/resend",
json={"email": "user@example.com"},
)
assert blocked.status_code == 429
assert blocked.headers["content-type"].startswith("application/problem+json")
finally:
app.dependency_overrides = {}
def test_signup_start_rate_limited_after_too_many_attempts() -> None:
user = AuthUser(id="user-1", email="user@example.com")
token_response = AuthTokenResponse(
token_response = SessionResponse(
access_token="access",
refresh_token="refresh",
expires_in=3600,
@@ -310,7 +273,39 @@ def test_signup_start_rate_limited_after_too_many_attempts() -> None:
try:
for _ in range(5):
ok = client.post(
"/api/v1/auth/signup/start",
"/api/v1/auth/verifications/resend",
json={"email": "user@example.com"},
)
assert ok.status_code == 204
blocked = client.post(
"/api/v1/auth/verifications/resend",
json={"email": "user@example.com"},
)
assert blocked.status_code == 429
assert blocked.headers["content-type"].startswith("application/problem+json")
finally:
app.dependency_overrides = {}
def test_signup_start_rate_limited_after_too_many_attempts() -> None:
user = AuthUser(id="user-1", email="user@example.com")
token_response = SessionResponse(
access_token="access",
refresh_token="refresh",
expires_in=3600,
token_type="bearer",
user=user,
)
app.dependency_overrides[get_auth_service] = _override_auth_service(
FakeAuthService(token_response)
)
client = TestClient(app)
try:
for _ in range(5):
ok = client.post(
"/api/v1/auth/verifications",
json={
"username": "demo",
"email": "user@example.com",
@@ -320,7 +315,7 @@ def test_signup_start_rate_limited_after_too_many_attempts() -> None:
assert ok.status_code == 202
blocked = client.post(
"/api/v1/auth/signup/start",
"/api/v1/auth/verifications",
json={
"username": "demo",
"email": "user@example.com",
@@ -335,7 +330,7 @@ def test_signup_start_rate_limited_after_too_many_attempts() -> None:
def test_login_invalid_returns_problem_details() -> None:
user = AuthUser(id="user-1", email="user@example.com")
token_response = AuthTokenResponse(
token_response = SessionResponse(
access_token="access",
refresh_token="refresh",
expires_in=3600,
@@ -349,7 +344,7 @@ def test_login_invalid_returns_problem_details() -> None:
client = TestClient(app)
try:
response = client.post(
"/api/v1/auth/login",
"/api/v1/auth/sessions",
json={"email": "user@example.com", "password": "wrongpw"},
)
assert response.status_code == 401
@@ -364,7 +359,7 @@ def test_login_invalid_returns_problem_details() -> None:
def test_refresh_invalid_returns_problem_details() -> None:
user = AuthUser(id="user-1", email="user@example.com")
token_response = AuthTokenResponse(
token_response = SessionResponse(
access_token="access",
refresh_token="refresh",
expires_in=3600,
@@ -378,7 +373,7 @@ def test_refresh_invalid_returns_problem_details() -> None:
client = TestClient(app)
try:
response = client.post(
"/api/v1/auth/refresh",
"/api/v1/auth/sessions/refresh",
json={"refresh_token": "invalid"},
)
assert response.status_code == 401
@@ -393,7 +388,7 @@ def test_refresh_invalid_returns_problem_details() -> None:
def test_logout_returns_no_content() -> None:
user = AuthUser(id="user-1", email="user@example.com")
token_response = AuthTokenResponse(
token_response = SessionResponse(
access_token="access",
refresh_token="refresh",
expires_in=3600,
@@ -406,8 +401,9 @@ def test_logout_returns_no_content() -> None:
client = TestClient(app)
try:
response = client.post(
"/api/v1/auth/logout",
response = client.request(
"DELETE",
"/api/v1/auth/sessions",
json={"refresh_token": "refresh"},
)
assert response.status_code == 204
@@ -418,7 +414,7 @@ def test_logout_returns_no_content() -> None:
def test_login_rate_limited_after_too_many_attempts() -> None:
user = AuthUser(id="user-1", email="user@example.com")
token_response = AuthTokenResponse(
token_response = SessionResponse(
access_token="access",
refresh_token="refresh",
expires_in=3600,
@@ -433,13 +429,13 @@ def test_login_rate_limited_after_too_many_attempts() -> None:
try:
for _ in range(10):
blocked = client.post(
"/api/v1/auth/login",
"/api/v1/auth/sessions",
json={"email": "user@example.com", "password": "wrongpw"},
)
assert blocked.status_code == 401
blocked = client.post(
"/api/v1/auth/login",
"/api/v1/auth/sessions",
json={"email": "user@example.com", "password": "wrongpw"},
)
assert blocked.status_code == 429
@@ -452,7 +448,7 @@ def test_login_rate_limited_after_too_many_attempts() -> None:
def test_refresh_rate_limited_after_too_many_attempts() -> None:
user = AuthUser(id="user-1", email="user@example.com")
token_response = AuthTokenResponse(
token_response = SessionResponse(
access_token="access",
refresh_token="refresh",
expires_in=3600,
@@ -467,13 +463,13 @@ def test_refresh_rate_limited_after_too_many_attempts() -> None:
try:
for _ in range(10):
blocked = client.post(
"/api/v1/auth/refresh",
"/api/v1/auth/sessions/refresh",
json={"refresh_token": "invalid"},
)
assert blocked.status_code == 401
blocked = client.post(
"/api/v1/auth/refresh",
"/api/v1/auth/sessions/refresh",
json={"refresh_token": "invalid"},
)
assert blocked.status_code == 429
@@ -486,7 +482,7 @@ def test_refresh_rate_limited_after_too_many_attempts() -> None:
def test_logout_rate_limited_after_too_many_attempts() -> None:
user = AuthUser(id="user-1", email="user@example.com")
token_response = AuthTokenResponse(
token_response = SessionResponse(
access_token="access",
refresh_token="refresh",
expires_in=3600,
@@ -500,14 +496,16 @@ def test_logout_rate_limited_after_too_many_attempts() -> None:
client = TestClient(app)
try:
for _ in range(10):
ok = client.post(
"/api/v1/auth/logout",
ok = client.request(
"DELETE",
"/api/v1/auth/sessions",
json={"refresh_token": "refresh"},
)
assert ok.status_code == 204
blocked = client.post(
"/api/v1/auth/logout",
blocked = client.request(
"DELETE",
"/api/v1/auth/sessions",
json={"refresh_token": "refresh"},
)
assert blocked.status_code == 429
@@ -520,7 +518,7 @@ def test_logout_rate_limited_after_too_many_attempts() -> None:
def test_signup_start_validation_error_returns_problem_details() -> None:
user = AuthUser(id="user-1", email="user@example.com")
token_response = AuthTokenResponse(
token_response = SessionResponse(
access_token="access",
refresh_token="refresh",
expires_in=3600,
@@ -533,7 +531,7 @@ def test_signup_start_validation_error_returns_problem_details() -> None:
client = TestClient(app)
try:
response = client.post("/api/v1/auth/signup/start", json={})
response = client.post("/api/v1/auth/verifications", json={})
assert response.status_code == 422
assert response.headers["content-type"].startswith("application/problem+json")
body = response.json()
@@ -546,7 +544,7 @@ def test_signup_start_validation_error_returns_problem_details() -> None:
def test_signup_start_missing_username_returns_problem_details() -> None:
user = AuthUser(id="user-1", email="user@example.com")
token_response = AuthTokenResponse(
token_response = SessionResponse(
access_token="access",
refresh_token="refresh",
expires_in=3600,
@@ -560,7 +558,7 @@ def test_signup_start_missing_username_returns_problem_details() -> None:
client = TestClient(app)
try:
response = client.post(
"/api/v1/auth/signup/start",
"/api/v1/auth/verifications",
json={"email": "user@example.com", "password": "secret123"},
)
assert response.status_code == 422
@@ -575,7 +573,7 @@ def test_signup_start_missing_username_returns_problem_details() -> None:
def test_get_user_by_email_returns_user() -> None:
user = AuthUser(id="user-1", email="user@example.com")
token_response = AuthTokenResponse(
token_response = SessionResponse(
access_token="access",
refresh_token="refresh",
expires_in=3600,
@@ -593,7 +591,7 @@ def test_get_user_by_email_returns_user() -> None:
client = TestClient(app)
try:
response = client.get(
"/api/v1/auth/users/by-email",
"/api/v1/auth/users",
params={"email": "user@example.com"},
)
assert response.status_code == 200
@@ -606,7 +604,7 @@ def test_get_user_by_email_returns_user() -> None:
def test_get_user_by_email_not_found_returns_problem_details() -> None:
user = AuthUser(id="user-1", email="user@example.com")
token_response = AuthTokenResponse(
token_response = SessionResponse(
access_token="access",
refresh_token="refresh",
expires_in=3600,
@@ -624,7 +622,7 @@ def test_get_user_by_email_not_found_returns_problem_details() -> None:
client = TestClient(app)
try:
response = client.get(
"/api/v1/auth/users/by-email",
"/api/v1/auth/users",
params={"email": "missing@example.com"},
)
assert response.status_code == 404
@@ -639,7 +637,7 @@ def test_get_user_by_email_not_found_returns_problem_details() -> None:
def test_get_user_by_email_forbidden_when_querying_other_user() -> None:
user = AuthUser(id="user-1", email="user@example.com")
token_response = AuthTokenResponse(
token_response = SessionResponse(
access_token="access",
refresh_token="refresh",
expires_in=3600,
@@ -657,7 +655,7 @@ def test_get_user_by_email_forbidden_when_querying_other_user() -> None:
client = TestClient(app)
try:
response = client.get(
"/api/v1/auth/users/by-email",
"/api/v1/auth/users",
params={"email": "target@example.com"},
)
assert response.status_code == 403
@@ -1,204 +0,0 @@
from __future__ import annotations
from typing import Callable
from uuid import UUID
from fastapi import HTTPException
from fastapi.testclient import TestClient
from app import app
from core.auth.models import CurrentUser
from v1.profile.dependencies import get_current_user, get_profile_service
from v1.profile.schemas import ProfileResponse, ProfileUpdateRequest
from v1.profile.service import ProfileService
class FakeProfileService:
"""Fake service for integration testing."""
def __init__(self, profile: ProfileResponse) -> None:
self._profile = profile
async def get_me(self) -> ProfileResponse:
if self._profile.id is None:
raise HTTPException(status_code=404, detail="Profile not found")
return self._profile
async def update_me(self, update: ProfileUpdateRequest) -> ProfileResponse:
if self._profile.id is None:
raise HTTPException(status_code=404, detail="Profile not found")
return ProfileResponse(
id=self._profile.id,
username=(
update.username
if update.username is not None
else self._profile.username
),
avatar_url=(
update.avatar_url
if update.avatar_url is not None
else self._profile.avatar_url
),
bio=update.bio if update.bio is not None else self._profile.bio,
)
async def get_by_username(self, username: str) -> ProfileResponse:
if username != self._profile.username:
raise HTTPException(status_code=404, detail="Profile not found")
return self._profile
def _override_profile_service(
service: FakeProfileService,
) -> Callable[[], ProfileService]:
def _get_service() -> ProfileService:
return service # type: ignore[return-value]
return _get_service
def _override_current_user(user_id: UUID) -> Callable[[], CurrentUser]:
def _get_user() -> CurrentUser:
return CurrentUser(id=user_id)
return _get_user
def test_get_me_returns_profile() -> None:
user_id = UUID("00000000-0000-0000-0000-000000000001")
profile = ProfileResponse(
id=str(user_id),
username="demo",
avatar_url=None,
bio=None,
)
app.dependency_overrides[get_profile_service] = _override_profile_service(
FakeProfileService(profile)
)
app.dependency_overrides[get_current_user] = _override_current_user(user_id)
client = TestClient(app)
try:
response = client.get("/api/v1/profile/me")
assert response.status_code == 200
body = response.json()
assert body["username"] == "demo"
finally:
app.dependency_overrides = {}
def test_patch_me_updates_profile() -> None:
user_id = UUID("00000000-0000-0000-0000-000000000001")
profile = ProfileResponse(
id=str(user_id),
username="demo",
avatar_url=None,
bio=None,
)
app.dependency_overrides[get_profile_service] = _override_profile_service(
FakeProfileService(profile)
)
app.dependency_overrides[get_current_user] = _override_current_user(user_id)
client = TestClient(app)
try:
response = client.patch(
"/api/v1/profile/me",
json={"username": "updated"},
)
assert response.status_code == 200
body = response.json()
assert body["username"] == "updated"
finally:
app.dependency_overrides = {}
def test_get_profile_by_username() -> None:
profile = ProfileResponse(
id="00000000-0000-0000-0000-000000000001",
username="demo",
avatar_url=None,
bio=None,
)
app.dependency_overrides[get_profile_service] = _override_profile_service(
FakeProfileService(profile)
)
client = TestClient(app)
try:
response = client.get("/api/v1/profile/demo")
assert response.status_code == 200
body = response.json()
assert body["username"] == "demo"
finally:
app.dependency_overrides = {}
def test_profile_not_found_returns_problem_details() -> None:
profile = ProfileResponse(
id="00000000-0000-0000-0000-000000000001",
username="demo",
avatar_url=None,
bio=None,
)
app.dependency_overrides[get_profile_service] = _override_profile_service(
FakeProfileService(profile)
)
client = TestClient(app)
try:
response = client.get("/api/v1/profile/unknown")
assert response.status_code == 404
assert response.headers["content-type"].startswith("application/problem+json")
body = response.json()
assert body["title"] == "Not Found"
assert body["status"] == 404
finally:
app.dependency_overrides = {}
def test_patch_me_validation_error_returns_problem_details() -> None:
user_id = UUID("00000000-0000-0000-0000-000000000001")
profile = ProfileResponse(
id=str(user_id),
username="demo",
avatar_url=None,
bio=None,
)
app.dependency_overrides[get_profile_service] = _override_profile_service(
FakeProfileService(profile)
)
app.dependency_overrides[get_current_user] = _override_current_user(user_id)
client = TestClient(app)
try:
response = client.patch("/api/v1/profile/me", json={})
assert response.status_code == 422
assert response.headers["content-type"].startswith("application/problem+json")
body = response.json()
assert body["title"] == "Unprocessable Content"
assert body["status"] == 422
finally:
app.dependency_overrides = {}
def test_patch_me_rejects_display_name_field() -> None:
user_id = UUID("00000000-0000-0000-0000-000000000001")
profile = ProfileResponse(
id=str(user_id),
username="demo",
avatar_url=None,
bio=None,
)
app.dependency_overrides[get_profile_service] = _override_profile_service(
FakeProfileService(profile)
)
app.dependency_overrides[get_current_user] = _override_current_user(user_id)
client = TestClient(app)
try:
response = client.patch("/api/v1/profile/me", json={"display_name": "x"})
assert response.status_code == 422
assert response.headers["content-type"].startswith("application/problem+json")
finally:
app.dependency_overrides = {}
@@ -0,0 +1,180 @@
from __future__ import annotations
from typing import Callable
from uuid import UUID
from fastapi import HTTPException
from fastapi.testclient import TestClient
from app import app
from core.auth.models import CurrentUser
from v1.users.dependencies import get_current_user, get_user_service
from v1.users.schemas import UserResponse, UserUpdateRequest
from v1.users.service import UserService
class FakeUserService:
"""Fake service for integration testing."""
def __init__(self, user: UserResponse) -> None:
self._user = user
async def get_me(self) -> UserResponse:
if self._user.id is None:
raise HTTPException(status_code=404, detail="User not found")
return self._user
async def update_me(self, update: UserUpdateRequest) -> UserResponse:
if self._user.id is None:
raise HTTPException(status_code=404, detail="User not found")
return UserResponse(
id=self._user.id,
username=(
update.username if update.username is not None else self._user.username
),
avatar_url=(
update.avatar_url
if update.avatar_url is not None
else self._user.avatar_url
),
bio=update.bio if update.bio is not None else self._user.bio,
)
async def get_by_username(self, username: str) -> UserResponse:
if username != self._user.username:
raise HTTPException(status_code=404, detail="User not found")
return self._user
def _override_user_service(
service: FakeUserService,
) -> Callable[[], UserService]:
def _get_service() -> UserService:
return service # type: ignore[return-value]
return _get_service
def _override_current_user(user_id: UUID) -> Callable[[], CurrentUser]:
def _get_user() -> CurrentUser:
return CurrentUser(id=user_id)
return _get_user
def test_get_me_returns_user() -> None:
user_id = UUID("00000000-0000-0000-0000-000000000001")
user = UserResponse(
id=str(user_id),
username="demo",
avatar_url=None,
bio=None,
)
app.dependency_overrides[get_user_service] = _override_user_service(
FakeUserService(user)
)
app.dependency_overrides[get_current_user] = _override_current_user(user_id)
client = TestClient(app)
try:
response = client.get("/api/v1/users/me")
assert response.status_code == 200
body = response.json()
assert body["username"] == "demo"
finally:
app.dependency_overrides = {}
def test_patch_me_updates_user() -> None:
user_id = UUID("00000000-0000-0000-0000-000000000001")
user = UserResponse(
id=str(user_id),
username="demo",
avatar_url=None,
bio=None,
)
app.dependency_overrides[get_user_service] = _override_user_service(
FakeUserService(user)
)
app.dependency_overrides[get_current_user] = _override_current_user(user_id)
client = TestClient(app)
try:
response = client.patch(
"/api/v1/users/me",
json={"username": "updated"},
)
assert response.status_code == 200
body = response.json()
assert body["username"] == "updated"
finally:
app.dependency_overrides = {}
def test_get_user_by_username() -> None:
user = UserResponse(
id="00000000-0000-0000-0000-000000000001",
username="demo",
avatar_url=None,
bio=None,
)
app.dependency_overrides[get_user_service] = _override_user_service(
FakeUserService(user)
)
client = TestClient(app)
try:
response = client.get("/api/v1/users/demo")
assert response.status_code == 200
body = response.json()
assert body["username"] == "demo"
finally:
app.dependency_overrides = {}
def test_user_not_found_returns_problem_details() -> None:
user = UserResponse(
id="00000000-0000-0000-0000-000000000001",
username="demo",
avatar_url=None,
bio=None,
)
app.dependency_overrides[get_user_service] = _override_user_service(
FakeUserService(user)
)
client = TestClient(app)
try:
response = client.get("/api/v1/users/unknown")
assert response.status_code == 404
assert response.headers["content-type"].startswith("application/problem+json")
body = response.json()
assert body["title"] == "Not Found"
assert body["status"] == 404
finally:
app.dependency_overrides = {}
def test_patch_me_validation_error_returns_problem_details() -> None:
user_id = UUID("00000000-0000-0000-0000-000000000001")
user = UserResponse(
id=str(user_id),
username="demo",
avatar_url=None,
bio=None,
)
app.dependency_overrides[get_user_service] = _override_user_service(
FakeUserService(user)
)
app.dependency_overrides[get_current_user] = _override_current_user(user_id)
client = TestClient(app)
try:
response = client.patch("/api/v1/users/me", json={})
assert response.status_code == 422
assert response.headers["content-type"].startswith("application/problem+json")
body = response.json()
assert body["title"] == "Unprocessable Content"
assert body["status"] == 422
finally:
app.dependency_overrides = {}
+16 -14
View File
@@ -4,51 +4,53 @@ import pytest
from pydantic import ValidationError
from v1.auth.schemas import (
AuthTokenResponse,
AuthUser,
LoginRequest,
RefreshRequest,
SignupStartRequest,
SignupVerifyRequest,
SignupResendRequest,
SessionCreateRequest,
SessionRefreshRequest,
SessionResponse,
VerificationCreateRequest,
VerificationVerifyRequest,
VerificationResendRequest,
)
def test_signup_requires_valid_email() -> None:
with pytest.raises(ValidationError):
SignupStartRequest(username="demo", email="not-an-email", password="secret123")
VerificationCreateRequest(
username="demo", email="not-an-email", password="secret123"
)
def test_signup_requires_username() -> None:
with pytest.raises(ValidationError):
SignupStartRequest.model_validate(
VerificationCreateRequest.model_validate(
{"email": "user@example.com", "password": "secret123"}
)
def test_signup_verify_requires_six_digit_token() -> None:
with pytest.raises(ValidationError):
SignupVerifyRequest(email="user@example.com", token="abc123")
VerificationVerifyRequest(email="user@example.com", token="abc123")
def test_signup_resend_requires_valid_email() -> None:
with pytest.raises(ValidationError):
SignupResendRequest(email="invalid")
VerificationResendRequest(email="invalid")
def test_login_requires_valid_email() -> None:
with pytest.raises(ValidationError):
LoginRequest(email="invalid", password="secret123")
SessionCreateRequest(email="invalid", password="secret123")
def test_refresh_requires_token() -> None:
with pytest.raises(ValidationError):
RefreshRequest(refresh_token="")
SessionRefreshRequest(refresh_token="")
def test_auth_token_response_maps_user() -> None:
def test_session_response_maps_user() -> None:
user = AuthUser(id="user-1", email="user@example.com")
response = AuthTokenResponse(
response = SessionResponse(
access_token="access",
refresh_token="refresh",
expires_in=3600,
+54 -54
View File
@@ -4,48 +4,47 @@ import pytest
import v1.auth.gateway as auth_gateway_module
from v1.auth.schemas import (
AuthTokenResponse,
AuthResendCodeResponse,
AuthSignupStartResponse,
AuthUserByEmailResponse,
AuthUser,
LoginRequest,
RefreshRequest,
SignupResendRequest,
SignupStartRequest,
SignupVerifyRequest,
SessionCreateRequest,
SessionRefreshRequest,
SessionResponse,
UserByEmailResponse,
VerificationCreateRequest,
VerificationCreateResponse,
VerificationResendRequest,
VerificationVerifyRequest,
)
from v1.auth.service import AuthService, AuthServiceGateway
class FakeGateway(AuthServiceGateway):
def __init__(self, response: AuthTokenResponse) -> None:
def __init__(self, response: SessionResponse) -> None:
self._response = response
async def signup_start(
self, request: SignupStartRequest
) -> AuthSignupStartResponse:
return AuthSignupStartResponse(email=request.email)
async def create_verification(
self, request: VerificationCreateRequest
) -> VerificationCreateResponse:
return VerificationCreateResponse(email=request.email)
async def signup_verify(self, request: SignupVerifyRequest) -> AuthTokenResponse:
async def verify_verification(
self, request: VerificationVerifyRequest
) -> SessionResponse:
return self._response
async def signup_resend(
self, request: SignupResendRequest
) -> AuthResendCodeResponse:
return AuthResendCodeResponse()
async def login(self, request: LoginRequest) -> AuthTokenResponse:
return self._response
async def refresh(self, request: RefreshRequest) -> AuthTokenResponse:
return self._response
async def logout(self, refresh_token: str | None) -> None:
async def resend_verification(self, request: VerificationResendRequest) -> None:
return None
async def get_user_by_email(self, email: str) -> AuthUserByEmailResponse:
return AuthUserByEmailResponse(
async def create_session(self, request: SessionCreateRequest) -> SessionResponse:
return self._response
async def refresh_session(self, request: SessionRefreshRequest) -> SessionResponse:
return self._response
async def delete_session(self, refresh_token: str | None) -> None:
return None
async def get_user_by_email(self, email: str) -> UserByEmailResponse:
return UserByEmailResponse(
id="user-1",
email=email,
created_at="2026-02-24T00:00:00Z",
@@ -56,7 +55,7 @@ class FakeGateway(AuthServiceGateway):
@pytest.mark.asyncio
async def test_signup_maps_response() -> None:
user = AuthUser(id="user-1", email="user@example.com")
token_response = AuthTokenResponse(
token_response = SessionResponse(
access_token="access",
refresh_token="refresh",
expires_in=3600,
@@ -65,16 +64,15 @@ async def test_signup_maps_response() -> None:
)
service = AuthService(gateway=FakeGateway(token_response))
start_result = await service.signup_start(
SignupStartRequest(
start_result = await service.create_verification(
VerificationCreateRequest(
username="demo", email="user@example.com", password="secret123"
)
)
assert start_result.status == "pending_verification"
assert start_result.email == "user@example.com"
result = await service.signup_verify(
SignupVerifyRequest(email="user@example.com", token="123456")
result = await service.verify_verification(
VerificationVerifyRequest(email="user@example.com", token="123456")
)
assert result.access_token == "access"
@@ -86,29 +84,29 @@ class LogoutAssertingGateway(AuthServiceGateway):
def __init__(self, expected_refresh_token: str) -> None:
self._expected_refresh_token = expected_refresh_token
async def signup_start(
self, request: SignupStartRequest
) -> AuthSignupStartResponse:
async def create_verification(
self, request: VerificationCreateRequest
) -> VerificationCreateResponse:
raise NotImplementedError
async def signup_verify(self, request: SignupVerifyRequest) -> AuthTokenResponse:
async def verify_verification(
self, request: VerificationVerifyRequest
) -> SessionResponse:
raise NotImplementedError
async def signup_resend(
self, request: SignupResendRequest
) -> AuthResendCodeResponse:
async def resend_verification(self, request: VerificationResendRequest) -> None:
raise NotImplementedError
async def login(self, request: LoginRequest) -> AuthTokenResponse:
async def create_session(self, request: SessionCreateRequest) -> SessionResponse:
raise NotImplementedError
async def refresh(self, request: RefreshRequest) -> AuthTokenResponse:
async def refresh_session(self, request: SessionRefreshRequest) -> SessionResponse:
raise NotImplementedError
async def logout(self, refresh_token: str | None) -> None:
async def delete_session(self, refresh_token: str | None) -> None:
assert refresh_token == self._expected_refresh_token
async def get_user_by_email(self, email: str) -> AuthUserByEmailResponse:
async def get_user_by_email(self, email: str) -> UserByEmailResponse:
raise NotImplementedError
@@ -116,13 +114,13 @@ class LogoutAssertingGateway(AuthServiceGateway):
async def test_logout_forwards_refresh_token() -> None:
service = AuthService(gateway=LogoutAssertingGateway("refresh-token"))
await service.logout("refresh-token")
await service.delete_session("refresh-token")
@pytest.mark.asyncio
async def test_get_user_by_email_forwards_to_gateway() -> None:
user = AuthUser(id="user-1", email="user@example.com")
token_response = AuthTokenResponse(
token_response = SessionResponse(
access_token="access",
refresh_token="refresh",
expires_in=3600,
@@ -137,9 +135,9 @@ async def test_get_user_by_email_forwards_to_gateway() -> None:
@pytest.mark.asyncio
async def test_signup_resend_returns_generic_message() -> None:
async def test_signup_resend_returns_none() -> None:
user = AuthUser(id="user-1", email="user@example.com")
token_response = AuthTokenResponse(
token_response = SessionResponse(
access_token="access",
refresh_token="refresh",
expires_in=3600,
@@ -148,9 +146,11 @@ async def test_signup_resend_returns_generic_message() -> None:
)
service = AuthService(gateway=FakeGateway(token_response))
result = await service.signup_resend(SignupResendRequest(email="user@example.com"))
result = await service.resend_verification(
VerificationResendRequest(email="user@example.com")
)
assert result.message == "If the email exists, a verification code has been sent"
assert result is None
@pytest.mark.asyncio
@@ -185,8 +185,8 @@ async def test_supabase_signup_passes_username_in_metadata(
monkeypatch.setattr(auth_gateway_module, "create_client", lambda *_: FakeClient())
gateway = auth_gateway_module.SupabaseAuthGateway()
await gateway.signup_start(
SignupStartRequest(
await gateway.create_verification(
VerificationCreateRequest(
username="demo",
email="user@example.com",
password="secret123",