test: update integration tests for RESTful routes
This commit is contained in:
@@ -10,19 +10,18 @@ from fastapi.testclient import TestClient
|
|||||||
from app import app
|
from app import app
|
||||||
from core.auth.models import CurrentUser
|
from core.auth.models import CurrentUser
|
||||||
from v1.auth.dependencies import get_auth_service
|
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.rate_limit import reset_rate_limit_state
|
||||||
from v1.auth.schemas import (
|
from v1.auth.schemas import (
|
||||||
AuthResendCodeResponse,
|
|
||||||
AuthSignupStartResponse,
|
|
||||||
AuthTokenResponse,
|
|
||||||
AuthUserByEmailResponse,
|
|
||||||
AuthUser,
|
AuthUser,
|
||||||
LoginRequest,
|
SessionCreateRequest,
|
||||||
RefreshRequest,
|
SessionRefreshRequest,
|
||||||
SignupResendRequest,
|
SessionResponse,
|
||||||
SignupStartRequest,
|
UserByEmailResponse,
|
||||||
SignupVerifyRequest,
|
VerificationCreateRequest,
|
||||||
|
VerificationCreateResponse,
|
||||||
|
VerificationResendRequest,
|
||||||
|
VerificationVerifyRequest,
|
||||||
)
|
)
|
||||||
from v1.auth.service import AuthService
|
from v1.auth.service import AuthService
|
||||||
|
|
||||||
@@ -33,39 +32,39 @@ def reset_auth_rate_limit_state() -> None:
|
|||||||
|
|
||||||
|
|
||||||
class FakeAuthService(AuthService):
|
class FakeAuthService(AuthService):
|
||||||
def __init__(self, token_response: AuthTokenResponse) -> None:
|
def __init__(self, token_response: SessionResponse) -> None:
|
||||||
self._token_response = token_response
|
self._token_response = token_response
|
||||||
|
|
||||||
async def signup_start(
|
async def create_verification(
|
||||||
self, request: SignupStartRequest
|
self, request: VerificationCreateRequest
|
||||||
) -> AuthSignupStartResponse:
|
) -> VerificationCreateResponse:
|
||||||
if request.email == "exists@example.com":
|
if request.email == "exists@example.com":
|
||||||
raise HTTPException(status_code=422, detail="Invalid signup request")
|
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":
|
if request.token == "000000":
|
||||||
raise HTTPException(status_code=401, detail="Invalid verification code")
|
raise HTTPException(status_code=401, detail="Invalid verification code")
|
||||||
return self._token_response
|
return self._token_response
|
||||||
|
|
||||||
async def signup_resend(
|
async def resend_verification(self, request: VerificationResendRequest) -> None:
|
||||||
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:
|
|
||||||
return 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":
|
if email == "missing@example.com":
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
return AuthUserByEmailResponse(
|
return UserByEmailResponse(
|
||||||
id="user-1",
|
id="user-1",
|
||||||
email=email,
|
email=email,
|
||||||
created_at="2026-02-24T00:00:00Z",
|
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:
|
def test_signup_start_returns_pending_response() -> None:
|
||||||
user = AuthUser(id="user-1", email="user@example.com")
|
user = AuthUser(id="user-1", email="user@example.com")
|
||||||
token_response = AuthTokenResponse(
|
token_response = SessionResponse(
|
||||||
access_token="access",
|
access_token="access",
|
||||||
refresh_token="refresh",
|
refresh_token="refresh",
|
||||||
expires_in=3600,
|
expires_in=3600,
|
||||||
@@ -96,7 +95,7 @@ def test_signup_start_returns_pending_response() -> None:
|
|||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
try:
|
try:
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/auth/signup/start",
|
"/api/v1/auth/verifications",
|
||||||
json={
|
json={
|
||||||
"username": "demo",
|
"username": "demo",
|
||||||
"email": "user@example.com",
|
"email": "user@example.com",
|
||||||
@@ -105,7 +104,6 @@ def test_signup_start_returns_pending_response() -> None:
|
|||||||
)
|
)
|
||||||
assert response.status_code == 202
|
assert response.status_code == 202
|
||||||
body = response.json()
|
body = response.json()
|
||||||
assert body["status"] == "pending_verification"
|
|
||||||
assert body["email"] == "user@example.com"
|
assert body["email"] == "user@example.com"
|
||||||
finally:
|
finally:
|
||||||
app.dependency_overrides = {}
|
app.dependency_overrides = {}
|
||||||
@@ -113,7 +111,7 @@ def test_signup_start_returns_pending_response() -> None:
|
|||||||
|
|
||||||
def test_signup_verify_returns_token_response() -> None:
|
def test_signup_verify_returns_token_response() -> None:
|
||||||
user = AuthUser(id="user-1", email="user@example.com")
|
user = AuthUser(id="user-1", email="user@example.com")
|
||||||
token_response = AuthTokenResponse(
|
token_response = SessionResponse(
|
||||||
access_token="access",
|
access_token="access",
|
||||||
refresh_token="refresh",
|
refresh_token="refresh",
|
||||||
expires_in=3600,
|
expires_in=3600,
|
||||||
@@ -127,7 +125,7 @@ def test_signup_verify_returns_token_response() -> None:
|
|||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
try:
|
try:
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/auth/signup/verify",
|
"/api/v1/auth/verifications/verify",
|
||||||
json={"email": "user@example.com", "token": "123456"},
|
json={"email": "user@example.com", "token": "123456"},
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
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:
|
def test_signup_resend_returns_generic_message() -> None:
|
||||||
user = AuthUser(id="user-1", email="user@example.com")
|
user = AuthUser(id="user-1", email="user@example.com")
|
||||||
token_response = AuthTokenResponse(
|
token_response = SessionResponse(
|
||||||
access_token="access",
|
access_token="access",
|
||||||
refresh_token="refresh",
|
refresh_token="refresh",
|
||||||
expires_in=3600,
|
expires_in=3600,
|
||||||
@@ -155,21 +153,18 @@ def test_signup_resend_returns_generic_message() -> None:
|
|||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
try:
|
try:
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/auth/signup/resend",
|
"/api/v1/auth/verifications/resend",
|
||||||
json={"email": "user@example.com"},
|
json={"email": "user@example.com"},
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 204
|
||||||
body = response.json()
|
assert response.content == b""
|
||||||
assert (
|
|
||||||
body["message"] == "If the email exists, a verification code has been sent"
|
|
||||||
)
|
|
||||||
finally:
|
finally:
|
||||||
app.dependency_overrides = {}
|
app.dependency_overrides = {}
|
||||||
|
|
||||||
|
|
||||||
def test_signup_verify_invalid_token_returns_problem_details() -> None:
|
def test_signup_verify_invalid_token_returns_problem_details() -> None:
|
||||||
user = AuthUser(id="user-1", email="user@example.com")
|
user = AuthUser(id="user-1", email="user@example.com")
|
||||||
token_response = AuthTokenResponse(
|
token_response = SessionResponse(
|
||||||
access_token="access",
|
access_token="access",
|
||||||
refresh_token="refresh",
|
refresh_token="refresh",
|
||||||
expires_in=3600,
|
expires_in=3600,
|
||||||
@@ -183,7 +178,7 @@ def test_signup_verify_invalid_token_returns_problem_details() -> None:
|
|||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
try:
|
try:
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/auth/signup/verify",
|
"/api/v1/auth/verifications/verify",
|
||||||
json={"email": "user@example.com", "token": "000000"},
|
json={"email": "user@example.com", "token": "000000"},
|
||||||
)
|
)
|
||||||
assert response.status_code == 401
|
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:
|
def test_signup_start_existing_email_returns_problem_details() -> None:
|
||||||
user = AuthUser(id="user-1", email="user@example.com")
|
user = AuthUser(id="user-1", email="user@example.com")
|
||||||
token_response = AuthTokenResponse(
|
token_response = SessionResponse(
|
||||||
access_token="access",
|
access_token="access",
|
||||||
refresh_token="refresh",
|
refresh_token="refresh",
|
||||||
expires_in=3600,
|
expires_in=3600,
|
||||||
@@ -212,7 +207,7 @@ def test_signup_start_existing_email_returns_problem_details() -> None:
|
|||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
try:
|
try:
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/auth/signup/start",
|
"/api/v1/auth/verifications",
|
||||||
json={
|
json={
|
||||||
"username": "demo",
|
"username": "demo",
|
||||||
"email": "exists@example.com",
|
"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:
|
def test_signup_verify_rate_limited_after_too_many_attempts() -> None:
|
||||||
user = AuthUser(id="user-1", email="user@example.com")
|
user = AuthUser(id="user-1", email="user@example.com")
|
||||||
token_response = AuthTokenResponse(
|
token_response = SessionResponse(
|
||||||
access_token="access",
|
access_token="access",
|
||||||
refresh_token="refresh",
|
refresh_token="refresh",
|
||||||
expires_in=3600,
|
expires_in=3600,
|
||||||
@@ -246,13 +241,13 @@ def test_signup_verify_rate_limited_after_too_many_attempts() -> None:
|
|||||||
try:
|
try:
|
||||||
for _ in range(10):
|
for _ in range(10):
|
||||||
ok = client.post(
|
ok = client.post(
|
||||||
"/api/v1/auth/signup/verify",
|
"/api/v1/auth/verifications/verify",
|
||||||
json={"email": "user@example.com", "token": "123456"},
|
json={"email": "user@example.com", "token": "123456"},
|
||||||
)
|
)
|
||||||
assert ok.status_code == 200
|
assert ok.status_code == 200
|
||||||
|
|
||||||
blocked = client.post(
|
blocked = client.post(
|
||||||
"/api/v1/auth/signup/verify",
|
"/api/v1/auth/verifications/verify",
|
||||||
json={"email": "user@example.com", "token": "123456"},
|
json={"email": "user@example.com", "token": "123456"},
|
||||||
)
|
)
|
||||||
assert blocked.status_code == 429
|
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:
|
def test_signup_resend_rate_limited_after_too_many_attempts() -> None:
|
||||||
user = AuthUser(id="user-1", email="user@example.com")
|
user = AuthUser(id="user-1", email="user@example.com")
|
||||||
token_response = AuthTokenResponse(
|
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(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(
|
|
||||||
access_token="access",
|
access_token="access",
|
||||||
refresh_token="refresh",
|
refresh_token="refresh",
|
||||||
expires_in=3600,
|
expires_in=3600,
|
||||||
@@ -310,7 +273,39 @@ def test_signup_start_rate_limited_after_too_many_attempts() -> None:
|
|||||||
try:
|
try:
|
||||||
for _ in range(5):
|
for _ in range(5):
|
||||||
ok = client.post(
|
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={
|
json={
|
||||||
"username": "demo",
|
"username": "demo",
|
||||||
"email": "user@example.com",
|
"email": "user@example.com",
|
||||||
@@ -320,7 +315,7 @@ def test_signup_start_rate_limited_after_too_many_attempts() -> None:
|
|||||||
assert ok.status_code == 202
|
assert ok.status_code == 202
|
||||||
|
|
||||||
blocked = client.post(
|
blocked = client.post(
|
||||||
"/api/v1/auth/signup/start",
|
"/api/v1/auth/verifications",
|
||||||
json={
|
json={
|
||||||
"username": "demo",
|
"username": "demo",
|
||||||
"email": "user@example.com",
|
"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:
|
def test_login_invalid_returns_problem_details() -> None:
|
||||||
user = AuthUser(id="user-1", email="user@example.com")
|
user = AuthUser(id="user-1", email="user@example.com")
|
||||||
token_response = AuthTokenResponse(
|
token_response = SessionResponse(
|
||||||
access_token="access",
|
access_token="access",
|
||||||
refresh_token="refresh",
|
refresh_token="refresh",
|
||||||
expires_in=3600,
|
expires_in=3600,
|
||||||
@@ -349,7 +344,7 @@ def test_login_invalid_returns_problem_details() -> None:
|
|||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
try:
|
try:
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/auth/login",
|
"/api/v1/auth/sessions",
|
||||||
json={"email": "user@example.com", "password": "wrongpw"},
|
json={"email": "user@example.com", "password": "wrongpw"},
|
||||||
)
|
)
|
||||||
assert response.status_code == 401
|
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:
|
def test_refresh_invalid_returns_problem_details() -> None:
|
||||||
user = AuthUser(id="user-1", email="user@example.com")
|
user = AuthUser(id="user-1", email="user@example.com")
|
||||||
token_response = AuthTokenResponse(
|
token_response = SessionResponse(
|
||||||
access_token="access",
|
access_token="access",
|
||||||
refresh_token="refresh",
|
refresh_token="refresh",
|
||||||
expires_in=3600,
|
expires_in=3600,
|
||||||
@@ -378,7 +373,7 @@ def test_refresh_invalid_returns_problem_details() -> None:
|
|||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
try:
|
try:
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/auth/refresh",
|
"/api/v1/auth/sessions/refresh",
|
||||||
json={"refresh_token": "invalid"},
|
json={"refresh_token": "invalid"},
|
||||||
)
|
)
|
||||||
assert response.status_code == 401
|
assert response.status_code == 401
|
||||||
@@ -393,7 +388,7 @@ def test_refresh_invalid_returns_problem_details() -> None:
|
|||||||
|
|
||||||
def test_logout_returns_no_content() -> None:
|
def test_logout_returns_no_content() -> None:
|
||||||
user = AuthUser(id="user-1", email="user@example.com")
|
user = AuthUser(id="user-1", email="user@example.com")
|
||||||
token_response = AuthTokenResponse(
|
token_response = SessionResponse(
|
||||||
access_token="access",
|
access_token="access",
|
||||||
refresh_token="refresh",
|
refresh_token="refresh",
|
||||||
expires_in=3600,
|
expires_in=3600,
|
||||||
@@ -406,8 +401,9 @@ def test_logout_returns_no_content() -> None:
|
|||||||
|
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
try:
|
try:
|
||||||
response = client.post(
|
response = client.request(
|
||||||
"/api/v1/auth/logout",
|
"DELETE",
|
||||||
|
"/api/v1/auth/sessions",
|
||||||
json={"refresh_token": "refresh"},
|
json={"refresh_token": "refresh"},
|
||||||
)
|
)
|
||||||
assert response.status_code == 204
|
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:
|
def test_login_rate_limited_after_too_many_attempts() -> None:
|
||||||
user = AuthUser(id="user-1", email="user@example.com")
|
user = AuthUser(id="user-1", email="user@example.com")
|
||||||
token_response = AuthTokenResponse(
|
token_response = SessionResponse(
|
||||||
access_token="access",
|
access_token="access",
|
||||||
refresh_token="refresh",
|
refresh_token="refresh",
|
||||||
expires_in=3600,
|
expires_in=3600,
|
||||||
@@ -433,13 +429,13 @@ def test_login_rate_limited_after_too_many_attempts() -> None:
|
|||||||
try:
|
try:
|
||||||
for _ in range(10):
|
for _ in range(10):
|
||||||
blocked = client.post(
|
blocked = client.post(
|
||||||
"/api/v1/auth/login",
|
"/api/v1/auth/sessions",
|
||||||
json={"email": "user@example.com", "password": "wrongpw"},
|
json={"email": "user@example.com", "password": "wrongpw"},
|
||||||
)
|
)
|
||||||
assert blocked.status_code == 401
|
assert blocked.status_code == 401
|
||||||
|
|
||||||
blocked = client.post(
|
blocked = client.post(
|
||||||
"/api/v1/auth/login",
|
"/api/v1/auth/sessions",
|
||||||
json={"email": "user@example.com", "password": "wrongpw"},
|
json={"email": "user@example.com", "password": "wrongpw"},
|
||||||
)
|
)
|
||||||
assert blocked.status_code == 429
|
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:
|
def test_refresh_rate_limited_after_too_many_attempts() -> None:
|
||||||
user = AuthUser(id="user-1", email="user@example.com")
|
user = AuthUser(id="user-1", email="user@example.com")
|
||||||
token_response = AuthTokenResponse(
|
token_response = SessionResponse(
|
||||||
access_token="access",
|
access_token="access",
|
||||||
refresh_token="refresh",
|
refresh_token="refresh",
|
||||||
expires_in=3600,
|
expires_in=3600,
|
||||||
@@ -467,13 +463,13 @@ def test_refresh_rate_limited_after_too_many_attempts() -> None:
|
|||||||
try:
|
try:
|
||||||
for _ in range(10):
|
for _ in range(10):
|
||||||
blocked = client.post(
|
blocked = client.post(
|
||||||
"/api/v1/auth/refresh",
|
"/api/v1/auth/sessions/refresh",
|
||||||
json={"refresh_token": "invalid"},
|
json={"refresh_token": "invalid"},
|
||||||
)
|
)
|
||||||
assert blocked.status_code == 401
|
assert blocked.status_code == 401
|
||||||
|
|
||||||
blocked = client.post(
|
blocked = client.post(
|
||||||
"/api/v1/auth/refresh",
|
"/api/v1/auth/sessions/refresh",
|
||||||
json={"refresh_token": "invalid"},
|
json={"refresh_token": "invalid"},
|
||||||
)
|
)
|
||||||
assert blocked.status_code == 429
|
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:
|
def test_logout_rate_limited_after_too_many_attempts() -> None:
|
||||||
user = AuthUser(id="user-1", email="user@example.com")
|
user = AuthUser(id="user-1", email="user@example.com")
|
||||||
token_response = AuthTokenResponse(
|
token_response = SessionResponse(
|
||||||
access_token="access",
|
access_token="access",
|
||||||
refresh_token="refresh",
|
refresh_token="refresh",
|
||||||
expires_in=3600,
|
expires_in=3600,
|
||||||
@@ -500,14 +496,16 @@ def test_logout_rate_limited_after_too_many_attempts() -> None:
|
|||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
try:
|
try:
|
||||||
for _ in range(10):
|
for _ in range(10):
|
||||||
ok = client.post(
|
ok = client.request(
|
||||||
"/api/v1/auth/logout",
|
"DELETE",
|
||||||
|
"/api/v1/auth/sessions",
|
||||||
json={"refresh_token": "refresh"},
|
json={"refresh_token": "refresh"},
|
||||||
)
|
)
|
||||||
assert ok.status_code == 204
|
assert ok.status_code == 204
|
||||||
|
|
||||||
blocked = client.post(
|
blocked = client.request(
|
||||||
"/api/v1/auth/logout",
|
"DELETE",
|
||||||
|
"/api/v1/auth/sessions",
|
||||||
json={"refresh_token": "refresh"},
|
json={"refresh_token": "refresh"},
|
||||||
)
|
)
|
||||||
assert blocked.status_code == 429
|
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:
|
def test_signup_start_validation_error_returns_problem_details() -> None:
|
||||||
user = AuthUser(id="user-1", email="user@example.com")
|
user = AuthUser(id="user-1", email="user@example.com")
|
||||||
token_response = AuthTokenResponse(
|
token_response = SessionResponse(
|
||||||
access_token="access",
|
access_token="access",
|
||||||
refresh_token="refresh",
|
refresh_token="refresh",
|
||||||
expires_in=3600,
|
expires_in=3600,
|
||||||
@@ -533,7 +531,7 @@ def test_signup_start_validation_error_returns_problem_details() -> None:
|
|||||||
|
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
try:
|
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.status_code == 422
|
||||||
assert response.headers["content-type"].startswith("application/problem+json")
|
assert response.headers["content-type"].startswith("application/problem+json")
|
||||||
body = response.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:
|
def test_signup_start_missing_username_returns_problem_details() -> None:
|
||||||
user = AuthUser(id="user-1", email="user@example.com")
|
user = AuthUser(id="user-1", email="user@example.com")
|
||||||
token_response = AuthTokenResponse(
|
token_response = SessionResponse(
|
||||||
access_token="access",
|
access_token="access",
|
||||||
refresh_token="refresh",
|
refresh_token="refresh",
|
||||||
expires_in=3600,
|
expires_in=3600,
|
||||||
@@ -560,7 +558,7 @@ def test_signup_start_missing_username_returns_problem_details() -> None:
|
|||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
try:
|
try:
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/auth/signup/start",
|
"/api/v1/auth/verifications",
|
||||||
json={"email": "user@example.com", "password": "secret123"},
|
json={"email": "user@example.com", "password": "secret123"},
|
||||||
)
|
)
|
||||||
assert response.status_code == 422
|
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:
|
def test_get_user_by_email_returns_user() -> None:
|
||||||
user = AuthUser(id="user-1", email="user@example.com")
|
user = AuthUser(id="user-1", email="user@example.com")
|
||||||
token_response = AuthTokenResponse(
|
token_response = SessionResponse(
|
||||||
access_token="access",
|
access_token="access",
|
||||||
refresh_token="refresh",
|
refresh_token="refresh",
|
||||||
expires_in=3600,
|
expires_in=3600,
|
||||||
@@ -593,7 +591,7 @@ def test_get_user_by_email_returns_user() -> None:
|
|||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
try:
|
try:
|
||||||
response = client.get(
|
response = client.get(
|
||||||
"/api/v1/auth/users/by-email",
|
"/api/v1/auth/users",
|
||||||
params={"email": "user@example.com"},
|
params={"email": "user@example.com"},
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
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:
|
def test_get_user_by_email_not_found_returns_problem_details() -> None:
|
||||||
user = AuthUser(id="user-1", email="user@example.com")
|
user = AuthUser(id="user-1", email="user@example.com")
|
||||||
token_response = AuthTokenResponse(
|
token_response = SessionResponse(
|
||||||
access_token="access",
|
access_token="access",
|
||||||
refresh_token="refresh",
|
refresh_token="refresh",
|
||||||
expires_in=3600,
|
expires_in=3600,
|
||||||
@@ -624,7 +622,7 @@ def test_get_user_by_email_not_found_returns_problem_details() -> None:
|
|||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
try:
|
try:
|
||||||
response = client.get(
|
response = client.get(
|
||||||
"/api/v1/auth/users/by-email",
|
"/api/v1/auth/users",
|
||||||
params={"email": "missing@example.com"},
|
params={"email": "missing@example.com"},
|
||||||
)
|
)
|
||||||
assert response.status_code == 404
|
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:
|
def test_get_user_by_email_forbidden_when_querying_other_user() -> None:
|
||||||
user = AuthUser(id="user-1", email="user@example.com")
|
user = AuthUser(id="user-1", email="user@example.com")
|
||||||
token_response = AuthTokenResponse(
|
token_response = SessionResponse(
|
||||||
access_token="access",
|
access_token="access",
|
||||||
refresh_token="refresh",
|
refresh_token="refresh",
|
||||||
expires_in=3600,
|
expires_in=3600,
|
||||||
@@ -657,7 +655,7 @@ def test_get_user_by_email_forbidden_when_querying_other_user() -> None:
|
|||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
try:
|
try:
|
||||||
response = client.get(
|
response = client.get(
|
||||||
"/api/v1/auth/users/by-email",
|
"/api/v1/auth/users",
|
||||||
params={"email": "target@example.com"},
|
params={"email": "target@example.com"},
|
||||||
)
|
)
|
||||||
assert response.status_code == 403
|
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 = {}
|
||||||
@@ -4,51 +4,53 @@ import pytest
|
|||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from v1.auth.schemas import (
|
from v1.auth.schemas import (
|
||||||
AuthTokenResponse,
|
|
||||||
AuthUser,
|
AuthUser,
|
||||||
LoginRequest,
|
SessionCreateRequest,
|
||||||
RefreshRequest,
|
SessionRefreshRequest,
|
||||||
SignupStartRequest,
|
SessionResponse,
|
||||||
SignupVerifyRequest,
|
VerificationCreateRequest,
|
||||||
SignupResendRequest,
|
VerificationVerifyRequest,
|
||||||
|
VerificationResendRequest,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_signup_requires_valid_email() -> None:
|
def test_signup_requires_valid_email() -> None:
|
||||||
with pytest.raises(ValidationError):
|
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:
|
def test_signup_requires_username() -> None:
|
||||||
with pytest.raises(ValidationError):
|
with pytest.raises(ValidationError):
|
||||||
SignupStartRequest.model_validate(
|
VerificationCreateRequest.model_validate(
|
||||||
{"email": "user@example.com", "password": "secret123"}
|
{"email": "user@example.com", "password": "secret123"}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_signup_verify_requires_six_digit_token() -> None:
|
def test_signup_verify_requires_six_digit_token() -> None:
|
||||||
with pytest.raises(ValidationError):
|
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:
|
def test_signup_resend_requires_valid_email() -> None:
|
||||||
with pytest.raises(ValidationError):
|
with pytest.raises(ValidationError):
|
||||||
SignupResendRequest(email="invalid")
|
VerificationResendRequest(email="invalid")
|
||||||
|
|
||||||
|
|
||||||
def test_login_requires_valid_email() -> None:
|
def test_login_requires_valid_email() -> None:
|
||||||
with pytest.raises(ValidationError):
|
with pytest.raises(ValidationError):
|
||||||
LoginRequest(email="invalid", password="secret123")
|
SessionCreateRequest(email="invalid", password="secret123")
|
||||||
|
|
||||||
|
|
||||||
def test_refresh_requires_token() -> None:
|
def test_refresh_requires_token() -> None:
|
||||||
with pytest.raises(ValidationError):
|
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")
|
user = AuthUser(id="user-1", email="user@example.com")
|
||||||
response = AuthTokenResponse(
|
response = SessionResponse(
|
||||||
access_token="access",
|
access_token="access",
|
||||||
refresh_token="refresh",
|
refresh_token="refresh",
|
||||||
expires_in=3600,
|
expires_in=3600,
|
||||||
|
|||||||
@@ -4,48 +4,47 @@ import pytest
|
|||||||
|
|
||||||
import v1.auth.gateway as auth_gateway_module
|
import v1.auth.gateway as auth_gateway_module
|
||||||
from v1.auth.schemas import (
|
from v1.auth.schemas import (
|
||||||
AuthTokenResponse,
|
|
||||||
AuthResendCodeResponse,
|
|
||||||
AuthSignupStartResponse,
|
|
||||||
AuthUserByEmailResponse,
|
|
||||||
AuthUser,
|
AuthUser,
|
||||||
LoginRequest,
|
SessionCreateRequest,
|
||||||
RefreshRequest,
|
SessionRefreshRequest,
|
||||||
SignupResendRequest,
|
SessionResponse,
|
||||||
SignupStartRequest,
|
UserByEmailResponse,
|
||||||
SignupVerifyRequest,
|
VerificationCreateRequest,
|
||||||
|
VerificationCreateResponse,
|
||||||
|
VerificationResendRequest,
|
||||||
|
VerificationVerifyRequest,
|
||||||
)
|
)
|
||||||
from v1.auth.service import AuthService, AuthServiceGateway
|
from v1.auth.service import AuthService, AuthServiceGateway
|
||||||
|
|
||||||
|
|
||||||
class FakeGateway(AuthServiceGateway):
|
class FakeGateway(AuthServiceGateway):
|
||||||
def __init__(self, response: AuthTokenResponse) -> None:
|
def __init__(self, response: SessionResponse) -> None:
|
||||||
self._response = response
|
self._response = response
|
||||||
|
|
||||||
async def signup_start(
|
async def create_verification(
|
||||||
self, request: SignupStartRequest
|
self, request: VerificationCreateRequest
|
||||||
) -> AuthSignupStartResponse:
|
) -> VerificationCreateResponse:
|
||||||
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:
|
||||||
return self._response
|
return self._response
|
||||||
|
|
||||||
async def signup_resend(
|
async def resend_verification(self, request: VerificationResendRequest) -> None:
|
||||||
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:
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def get_user_by_email(self, email: str) -> AuthUserByEmailResponse:
|
async def create_session(self, request: SessionCreateRequest) -> SessionResponse:
|
||||||
return AuthUserByEmailResponse(
|
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",
|
id="user-1",
|
||||||
email=email,
|
email=email,
|
||||||
created_at="2026-02-24T00:00:00Z",
|
created_at="2026-02-24T00:00:00Z",
|
||||||
@@ -56,7 +55,7 @@ class FakeGateway(AuthServiceGateway):
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_signup_maps_response() -> None:
|
async def test_signup_maps_response() -> None:
|
||||||
user = AuthUser(id="user-1", email="user@example.com")
|
user = AuthUser(id="user-1", email="user@example.com")
|
||||||
token_response = AuthTokenResponse(
|
token_response = SessionResponse(
|
||||||
access_token="access",
|
access_token="access",
|
||||||
refresh_token="refresh",
|
refresh_token="refresh",
|
||||||
expires_in=3600,
|
expires_in=3600,
|
||||||
@@ -65,16 +64,15 @@ async def test_signup_maps_response() -> None:
|
|||||||
)
|
)
|
||||||
service = AuthService(gateway=FakeGateway(token_response))
|
service = AuthService(gateway=FakeGateway(token_response))
|
||||||
|
|
||||||
start_result = await service.signup_start(
|
start_result = await service.create_verification(
|
||||||
SignupStartRequest(
|
VerificationCreateRequest(
|
||||||
username="demo", email="user@example.com", password="secret123"
|
username="demo", email="user@example.com", password="secret123"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assert start_result.status == "pending_verification"
|
|
||||||
assert start_result.email == "user@example.com"
|
assert start_result.email == "user@example.com"
|
||||||
|
|
||||||
result = await service.signup_verify(
|
result = await service.verify_verification(
|
||||||
SignupVerifyRequest(email="user@example.com", token="123456")
|
VerificationVerifyRequest(email="user@example.com", token="123456")
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result.access_token == "access"
|
assert result.access_token == "access"
|
||||||
@@ -86,29 +84,29 @@ class LogoutAssertingGateway(AuthServiceGateway):
|
|||||||
def __init__(self, expected_refresh_token: str) -> None:
|
def __init__(self, expected_refresh_token: str) -> None:
|
||||||
self._expected_refresh_token = expected_refresh_token
|
self._expected_refresh_token = expected_refresh_token
|
||||||
|
|
||||||
async def signup_start(
|
async def create_verification(
|
||||||
self, request: SignupStartRequest
|
self, request: VerificationCreateRequest
|
||||||
) -> AuthSignupStartResponse:
|
) -> VerificationCreateResponse:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def signup_verify(self, request: SignupVerifyRequest) -> AuthTokenResponse:
|
async def verify_verification(
|
||||||
|
self, request: VerificationVerifyRequest
|
||||||
|
) -> SessionResponse:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def signup_resend(
|
async def resend_verification(self, request: VerificationResendRequest) -> None:
|
||||||
self, request: SignupResendRequest
|
|
||||||
) -> AuthResendCodeResponse:
|
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def login(self, request: LoginRequest) -> AuthTokenResponse:
|
async def create_session(self, request: SessionCreateRequest) -> SessionResponse:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def refresh(self, request: RefreshRequest) -> AuthTokenResponse:
|
async def refresh_session(self, request: SessionRefreshRequest) -> SessionResponse:
|
||||||
raise NotImplementedError
|
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
|
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
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
@@ -116,13 +114,13 @@ class LogoutAssertingGateway(AuthServiceGateway):
|
|||||||
async def test_logout_forwards_refresh_token() -> None:
|
async def test_logout_forwards_refresh_token() -> None:
|
||||||
service = AuthService(gateway=LogoutAssertingGateway("refresh-token"))
|
service = AuthService(gateway=LogoutAssertingGateway("refresh-token"))
|
||||||
|
|
||||||
await service.logout("refresh-token")
|
await service.delete_session("refresh-token")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_user_by_email_forwards_to_gateway() -> None:
|
async def test_get_user_by_email_forwards_to_gateway() -> None:
|
||||||
user = AuthUser(id="user-1", email="user@example.com")
|
user = AuthUser(id="user-1", email="user@example.com")
|
||||||
token_response = AuthTokenResponse(
|
token_response = SessionResponse(
|
||||||
access_token="access",
|
access_token="access",
|
||||||
refresh_token="refresh",
|
refresh_token="refresh",
|
||||||
expires_in=3600,
|
expires_in=3600,
|
||||||
@@ -137,9 +135,9 @@ async def test_get_user_by_email_forwards_to_gateway() -> None:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@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")
|
user = AuthUser(id="user-1", email="user@example.com")
|
||||||
token_response = AuthTokenResponse(
|
token_response = SessionResponse(
|
||||||
access_token="access",
|
access_token="access",
|
||||||
refresh_token="refresh",
|
refresh_token="refresh",
|
||||||
expires_in=3600,
|
expires_in=3600,
|
||||||
@@ -148,9 +146,11 @@ async def test_signup_resend_returns_generic_message() -> None:
|
|||||||
)
|
)
|
||||||
service = AuthService(gateway=FakeGateway(token_response))
|
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
|
@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())
|
monkeypatch.setattr(auth_gateway_module, "create_client", lambda *_: FakeClient())
|
||||||
|
|
||||||
gateway = auth_gateway_module.SupabaseAuthGateway()
|
gateway = auth_gateway_module.SupabaseAuthGateway()
|
||||||
await gateway.signup_start(
|
await gateway.create_verification(
|
||||||
SignupStartRequest(
|
VerificationCreateRequest(
|
||||||
username="demo",
|
username="demo",
|
||||||
email="user@example.com",
|
email="user@example.com",
|
||||||
password="secret123",
|
password="secret123",
|
||||||
|
|||||||
Reference in New Issue
Block a user