Files
xiaoxia-saas/tests/unit/test_password_reset_use_case.py
T
xiaoxia 7e505a04c7
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / PR Build API Image (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (push) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
test: P3-1 第40波单元测试(wechat_oauth + wechat_sync) (#821)
2026-07-24 16:44:35 +08:00

300 lines
11 KiB
Python
Executable File

"""密码重置 UseCase 单元测试."""
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from unittest.mock import MagicMock, patch
import pytest
from packages.application.auth.password_reset_use_case import (
RequestPasswordResetRequest,
RequestPasswordResetUseCase,
ResetPasswordRequest,
ResetPasswordUseCase,
)
from packages.domain.entities import User
@pytest.fixture
def mock_user_repo():
return MagicMock()
@pytest.fixture
def mock_email_service():
svc = MagicMock()
svc.send_password_reset_email.return_value = (True, None)
return svc
@pytest.fixture
def sample_user():
user = User(
id="user_001",
email="user@example.com",
display_name="测试用户",
username="testuser",
password_hash="old_hash",
)
user.password_reset_token = None
user.password_reset_expires_at = None
return user
class TestRequestPasswordResetRequest:
"""RequestPasswordResetRequest 测试"""
def test_email_lowercased_and_stripped(self):
"""邮箱转小写并去空格"""
req = RequestPasswordResetRequest(" User@Example.COM ")
assert req.email == "user@example.com"
def test_empty_email(self):
"""空邮箱"""
req = RequestPasswordResetRequest("")
assert req.email == ""
class TestRequestPasswordResetUseCase:
"""RequestPasswordResetUseCase 测试"""
def test_request_success(self, mock_user_repo, mock_email_service, sample_user):
"""请求重置成功"""
mock_user_repo.find_by_email.return_value = sample_user
mock_user_repo.save.return_value = sample_user
use_case = RequestPasswordResetUseCase(
mock_user_repo,
base_url="https://example.com",
email_service=mock_email_service,
)
request = RequestPasswordResetRequest("user@example.com")
success, error = use_case.execute(request)
assert success is True
assert error is None
assert sample_user.password_reset_token is not None
assert len(sample_user.password_reset_token) > 0
assert sample_user.password_reset_expires_at is not None
mock_user_repo.save.assert_called_once()
mock_email_service.send_password_reset_email.assert_called_once()
def test_request_user_not_found_returns_success(self, mock_user_repo, mock_email_service):
"""用户不存在也返回成功(安全考虑,不暴露用户存在性)"""
mock_user_repo.find_by_email.return_value = None
use_case = RequestPasswordResetUseCase(
mock_user_repo,
base_url="https://example.com",
email_service=mock_email_service,
)
request = RequestPasswordResetRequest("nonexistent@example.com")
success, error = use_case.execute(request)
assert success is True
assert error is None
mock_user_repo.save.assert_not_called()
mock_email_service.send_password_reset_email.assert_not_called()
def test_request_empty_email_returns_error(self, mock_user_repo, mock_email_service):
"""空邮箱返回错误"""
use_case = RequestPasswordResetUseCase(
mock_user_repo,
base_url="https://example.com",
email_service=mock_email_service,
)
request = RequestPasswordResetRequest("")
success, error = use_case.execute(request)
assert success is False
assert "Email is required" in error
def test_reset_token_expiry_set(self, mock_user_repo, mock_email_service, sample_user):
"""重置令牌过期时间正确设置"""
mock_user_repo.find_by_email.return_value = sample_user
mock_user_repo.save.return_value = sample_user
use_case = RequestPasswordResetUseCase(
mock_user_repo,
base_url="https://example.com",
token_expire_hours=2,
email_service=mock_email_service,
)
request = RequestPasswordResetRequest("user@example.com")
use_case.execute(request)
assert sample_user.password_reset_expires_at is not None
# 过期时间应该在约2小时后
expected = datetime.now(timezone.utc) + timedelta(hours=2)
diff = abs((sample_user.password_reset_expires_at - expected).total_seconds())
assert diff < 10 # 允许10秒误差
def test_email_contains_reset_url(self, mock_user_repo, mock_email_service, sample_user):
"""重置邮件包含正确的重置链接"""
mock_user_repo.find_by_email.return_value = sample_user
mock_user_repo.save.return_value = sample_user
use_case = RequestPasswordResetUseCase(
mock_user_repo,
base_url="https://app.example.com",
email_service=mock_email_service,
)
request = RequestPasswordResetRequest("user@example.com")
use_case.execute(request)
call_args = mock_email_service.send_password_reset_email.call_args
reset_url = call_args[1]["reset_url"] if "reset_url" in call_args[1] else call_args[0][2]
assert "https://app.example.com/reset-password?token=" in reset_url
def test_email_failure_does_not_affect_result(self, mock_user_repo, mock_email_service, sample_user):
"""邮件发送失败不影响返回结果(安全考虑)"""
mock_user_repo.find_by_email.return_value = sample_user
mock_user_repo.save.return_value = sample_user
mock_email_service.send_password_reset_email.return_value = (False, "SMTP error")
use_case = RequestPasswordResetUseCase(
mock_user_repo,
base_url="https://example.com",
email_service=mock_email_service,
)
request = RequestPasswordResetRequest("user@example.com")
success, error = use_case.execute(request)
assert success is True
assert error is None
def test_different_tokens_each_time(self, mock_user_repo, mock_email_service, sample_user):
"""每次请求生成不同的 token"""
mock_user_repo.find_by_email.return_value = sample_user
mock_user_repo.save.return_value = sample_user
use_case = RequestPasswordResetUseCase(
mock_user_repo,
base_url="https://example.com",
email_service=mock_email_service,
)
request = RequestPasswordResetRequest("user@example.com")
use_case.execute(request)
token1 = sample_user.password_reset_token
use_case.execute(request)
token2 = sample_user.password_reset_token
assert token1 != token2
class TestResetPasswordRequest:
"""ResetPasswordRequest 测试"""
def test_stores_token_and_password(self):
"""正确存储 token 和新密码"""
req = ResetPasswordRequest(token="abc123", new_password="NewPass1!")
assert req.token == "abc123"
assert req.new_password == "NewPass1!"
class TestResetPasswordUseCase:
"""ResetPasswordUseCase 测试"""
def test_reset_success(self, mock_user_repo, sample_user):
"""重置密码成功"""
sample_user.password_reset_token = "valid_token"
sample_user.password_reset_expires_at = datetime.now(timezone.utc) + timedelta(hours=1)
mock_user_repo.find_by_password_reset_token.return_value = sample_user
mock_user_repo.save.return_value = sample_user
use_case = ResetPasswordUseCase(mock_user_repo)
request = ResetPasswordRequest(token="valid_token", new_password="NewSecurePass1!")
success, error = use_case.execute(request)
assert success is True
assert error is None
assert sample_user.password_reset_token is None
assert sample_user.password_reset_expires_at is None
assert sample_user.password_hash != "old_hash"
mock_user_repo.save.assert_called_once()
def test_reset_empty_token(self, mock_user_repo):
"""空 token 返回错误"""
use_case = ResetPasswordUseCase(mock_user_repo)
request = ResetPasswordRequest(token="", new_password="NewPass1!")
success, error = use_case.execute(request)
assert success is False
assert "Reset token is required" in error
mock_user_repo.save.assert_not_called()
def test_reset_empty_password(self, mock_user_repo):
"""空密码返回错误"""
use_case = ResetPasswordUseCase(mock_user_repo)
request = ResetPasswordRequest(token="sometoken", new_password="")
success, error = use_case.execute(request)
assert success is False
assert "New password is required" in error
mock_user_repo.save.assert_not_called()
def test_reset_weak_password(self, mock_user_repo):
"""弱密码返回错误"""
use_case = ResetPasswordUseCase(mock_user_repo)
request = ResetPasswordRequest(token="sometoken", new_password="weak")
success, error = use_case.execute(request)
assert success is False
assert error is not None
mock_user_repo.save.assert_not_called()
def test_reset_invalid_token(self, mock_user_repo):
"""无效 token 返回错误"""
mock_user_repo.find_by_password_reset_token.return_value = None
use_case = ResetPasswordUseCase(mock_user_repo)
request = ResetPasswordRequest(token="invalid_token", new_password="NewPass1!")
success, error = use_case.execute(request)
assert success is False
assert "Invalid or expired" in error
mock_user_repo.save.assert_not_called()
def test_reset_expired_token(self, mock_user_repo, sample_user):
"""过期 token 返回错误"""
sample_user.password_reset_token = "expired_token"
sample_user.password_reset_expires_at = datetime.now(timezone.utc) - timedelta(hours=1)
mock_user_repo.find_by_password_reset_token.return_value = sample_user
use_case = ResetPasswordUseCase(mock_user_repo)
request = ResetPasswordRequest(token="expired_token", new_password="NewPass1!")
success, error = use_case.execute(request)
assert success is False
assert "expired" in error.lower()
mock_user_repo.save.assert_not_called()
def test_reset_naive_datetime_treated_as_utc(self, mock_user_repo, sample_user):
"""无时区的过期时间按 UTC 处理"""
sample_user.password_reset_token = "naive_token"
# 用无时区的时间,设置为过去
sample_user.password_reset_expires_at = datetime.utcnow() - timedelta(hours=1)
mock_user_repo.find_by_password_reset_token.return_value = sample_user
use_case = ResetPasswordUseCase(mock_user_repo)
request = ResetPasswordRequest(token="naive_token", new_password="NewPass1!")
success, error = use_case.execute(request)
assert success is False
assert "expired" in error.lower()
def test_reset_no_expiry_set(self, mock_user_repo, sample_user):
"""没有设置过期时间的 token 可以使用"""
sample_user.password_reset_token = "no_expiry_token"
sample_user.password_reset_expires_at = None
mock_user_repo.find_by_password_reset_token.return_value = sample_user
use_case = ResetPasswordUseCase(mock_user_repo)
request = ResetPasswordRequest(token="no_expiry_token", new_password="NewPass1!")
success, error = use_case.execute(request)
assert success is True