Files
xiaoxia-saas/tests/unit/test_auth.py
T
2026-06-21 16:24:21 +08:00

258 lines
7.9 KiB
Python

import asyncio
import sys
from pathlib import Path
import jwt
import pytest
from fastapi import HTTPException
from fastapi.security import HTTPAuthorizationCredentials
ROOT = Path(__file__).resolve().parents[2]
API_ROOT = ROOT / "apps" / "api"
if str(API_ROOT) not in sys.path:
sys.path.insert(0, str(API_ROOT))
from app.api.routes.auth import (
PasswordResetRequestModel,
RegisterRequest,
ResetPasswordModel,
VerifyEmailRequestModel,
forgot_password,
register,
reset_password,
verify_email_post,
)
from app.auth import get_current_user
from app.config import settings
from packages.adapters.smtp import NoopEmailService
from packages.application.auth.login_use_case import LoginRequest, LoginUseCase
from packages.application.auth.register_user_use_case import RegisterUserRequest, RegisterUserUseCase
from packages.domain.auth import password_hasher
from packages.domain.entities import User
class InMemoryUserRepository:
def __init__(self):
self.users = {}
def save(self, user):
self.users[user.id] = user
def find_by_id(self, user_id):
return self.users.get(user_id)
def find_by_email(self, email):
return next((user for user in self.users.values() if user.email == email), None)
def find_by_username(self, username):
return next((user for user in self.users.values() if user.username == username), None)
def find_by_verification_token(self, token):
return next((user for user in self.users.values() if user.email_verification_token == token), None)
def find_by_password_reset_token(self, token):
return next((user for user in self.users.values() if user.password_reset_token == token), None)
def delete(self, user_id):
return self.users.pop(user_id, None) is not None
class DummySessionStore:
def __init__(self):
self.saved = []
def save_session(self, **kwargs):
self.saved.append(kwargs)
class DummyEmailService:
def send_verification_email(self, **kwargs):
return False, "disabled"
def test_register_use_case_hashes_password_and_normalizes_email():
repo = InMemoryUserRepository()
use_case = RegisterUserUseCase(repo, email_service=DummyEmailService())
response, error = use_case.execute(
RegisterUserRequest(
email="USER@EXAMPLE.COM",
password="Password1",
username="user",
display_name="User",
)
)
assert error is None
assert response is not None
user = repo.find_by_email("user@example.com")
assert user is not None
assert user.password_hash.startswith("$2")
assert password_hasher.verify_password("Password1", user.password_hash)
def test_register_route_defaults_display_name_to_username():
repo = InMemoryUserRepository()
response = asyncio.run(
register(
RegisterRequest(email="route@example.com", password="Password1", username="routeuser"),
repo,
DummyEmailService(),
)
)
assert response.display_name == "routeuser"
assert repo.find_by_email("route@example.com").display_name == "routeuser"
def test_login_use_case_returns_verifiable_jwt():
repo = InMemoryUserRepository()
session_store = DummySessionStore()
user = User(
id="user-1",
email="user@example.com",
username="user",
display_name="User",
password_hash=password_hasher.hash_password("Password1"),
)
repo.save(user)
response, error = LoginUseCase(repo, session_store=session_store, jwt_secret_key=settings.JWT_SECRET_KEY).execute(
LoginRequest("user@example.com", "Password1")
)
assert error is None
assert response is not None
payload = jwt.decode(response.access_token, settings.JWT_SECRET_KEY, algorithms=["HS256"])
assert response.expires_in == 1800
assert payload["sub"] == "user-1"
assert payload["type"] == "user_auth"
assert session_store.saved
def test_login_use_case_upgrades_legacy_sha256_hash():
repo = InMemoryUserRepository()
user = User(
id="user-1",
email="user@example.com",
username="user",
display_name="User",
password_hash="19513fdc9da4fb72a4a05eb66917548d3c90ff94d5419e1f2363eea89dfee1dd",
)
repo.save(user)
response, error = LoginUseCase(
repo, session_store=DummySessionStore(), jwt_secret_key=settings.JWT_SECRET_KEY
).execute(LoginRequest("user@example.com", "Password1"))
assert error is None
assert response is not None
assert user.password_hash.startswith("$2")
assert password_hasher.verify_password("Password1", user.password_hash)
def test_login_use_case_rejects_wrong_password():
repo = InMemoryUserRepository()
repo.save(
User(
id="user-1",
email="user@example.com",
username="user",
display_name="User",
password_hash=password_hasher.hash_password("Password1"),
)
)
response, error = LoginUseCase(repo, session_store=DummySessionStore()).execute(
LoginRequest("user@example.com", "WrongPassword1")
)
assert response is None
assert error == "Invalid email or password"
def test_current_user_dependency_resolves_bearer_user_token():
repo = InMemoryUserRepository()
repo.save(
User(
id="user-1",
email="user@example.com",
username="user",
display_name="User",
password_hash=password_hasher.hash_password("Password1"),
)
)
token = jwt.encode({"sub": "user-1", "sid": "session-1", "type": "user_auth"}, settings.JWT_SECRET_KEY, "HS256")
authenticated_user = asyncio.run(
get_current_user(HTTPAuthorizationCredentials(scheme="Bearer", credentials=token), repo)
)
assert authenticated_user.user.id == "user-1"
assert authenticated_user.session_id == "session-1"
def test_current_user_dependency_rejects_missing_token():
with pytest.raises(HTTPException) as error:
asyncio.run(get_current_user(None, InMemoryUserRepository()))
assert error.value.status_code == 401
def test_current_user_dependency_rejects_wrong_token_type():
token = jwt.encode({"sub": "user-1", "type": "refresh"}, settings.JWT_SECRET_KEY, "HS256")
with pytest.raises(HTTPException) as error:
asyncio.run(
get_current_user(HTTPAuthorizationCredentials(scheme="Bearer", credentials=token), InMemoryUserRepository())
)
assert error.value.status_code == 401
def test_verify_email_route_uses_use_case():
repo = InMemoryUserRepository()
user = User(
id="user-1",
email="user@example.com",
username="user",
display_name="User",
password_hash=password_hasher.hash_password("Password1"),
email_verification_token="verify-token",
)
repo.save(user)
response = asyncio.run(verify_email_post(VerifyEmailRequestModel(token="verify-token"), repo))
assert response.message == "邮箱验证成功"
assert user.email_verified is True
assert user.email_verification_token is None
def test_password_reset_routes_use_use_cases():
repo = InMemoryUserRepository()
user = User(
id="user-1",
email="user@example.com",
username="user",
display_name="User",
password_hash=password_hasher.hash_password("Password1"),
)
repo.save(user)
forgot_response = asyncio.run(
forgot_password(PasswordResetRequestModel(email="user@example.com"), repo, NoopEmailService())
)
assert forgot_response.message == "如果账户存在,密码重置邮件已发送"
assert user.password_reset_token
reset_response = asyncio.run(
reset_password(ResetPasswordModel(token=user.password_reset_token, new_password="NewPassword1"), repo)
)
assert reset_response.message == "密码重置成功"
assert password_hasher.verify_password("NewPassword1", user.password_hash)
assert user.password_reset_token is None