From 2709d88c680393dc406275ad290d0588051aa16e Mon Sep 17 00:00:00 2001 From: qzl Date: Thu, 26 Feb 2026 14:08:10 +0800 Subject: [PATCH] test: update integration tests for RESTful routes --- backend/tests/integration/test_auth_routes.py | 222 +++++++++--------- .../tests/integration/test_profile_routes.py | 204 ---------------- .../tests/integration/test_users_routes.py | 180 ++++++++++++++ .../tests/unit/v1/auth/test_auth_models.py | 30 +-- .../tests/unit/v1/auth/test_auth_service.py | 108 ++++----- 5 files changed, 360 insertions(+), 384 deletions(-) delete mode 100644 backend/tests/integration/test_profile_routes.py create mode 100644 backend/tests/integration/test_users_routes.py diff --git a/backend/tests/integration/test_auth_routes.py b/backend/tests/integration/test_auth_routes.py index 5e3ebb6..b0f9d32 100644 --- a/backend/tests/integration/test_auth_routes.py +++ b/backend/tests/integration/test_auth_routes.py @@ -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 diff --git a/backend/tests/integration/test_profile_routes.py b/backend/tests/integration/test_profile_routes.py deleted file mode 100644 index 15a08fa..0000000 --- a/backend/tests/integration/test_profile_routes.py +++ /dev/null @@ -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 = {} diff --git a/backend/tests/integration/test_users_routes.py b/backend/tests/integration/test_users_routes.py new file mode 100644 index 0000000..03e2d76 --- /dev/null +++ b/backend/tests/integration/test_users_routes.py @@ -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 = {} diff --git a/backend/tests/unit/v1/auth/test_auth_models.py b/backend/tests/unit/v1/auth/test_auth_models.py index 694beca..6f15305 100644 --- a/backend/tests/unit/v1/auth/test_auth_models.py +++ b/backend/tests/unit/v1/auth/test_auth_models.py @@ -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, diff --git a/backend/tests/unit/v1/auth/test_auth_service.py b/backend/tests/unit/v1/auth/test_auth_service.py index 76e99a8..1ae86ba 100644 --- a/backend/tests/unit/v1/auth/test_auth_service.py +++ b/backend/tests/unit/v1/auth/test_auth_service.py @@ -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",