diff --git a/tests/unit/test_password_reset_use_case.py b/tests/unit/test_password_reset_use_case.py index db6c30230..bd3f11e3c 100755 --- a/tests/unit/test_password_reset_use_case.py +++ b/tests/unit/test_password_reset_use_case.py @@ -1,6 +1,4 @@ -"""密码重置 UseCase 单元测试.""" - -from __future__ import annotations +"""密码重置 Use Case 单元测试.""" from datetime import datetime, timedelta, timezone from unittest.mock import MagicMock, patch @@ -15,285 +13,477 @@ from packages.application.auth.password_reset_use_case import ( ) from packages.domain.entities import User +# ── Test Fixtures ──────────────────────────────────────────────────────────── + + +def _make_user( + user_id="user-1", + email="user@example.com", + username="testuser", + display_name="Test User", + password_hash="hashed_password_123", +): + """创建一个测试用户.""" + return User( + id=user_id, + email=email, + display_name=display_name, + username=username, + password_hash=password_hash, + ) + @pytest.fixture def mock_user_repo(): - return MagicMock() + """mock 用户仓储.""" + repo = MagicMock() + repo.find_by_email.return_value = None + repo.find_by_password_reset_token.return_value = None + repo.save.return_value = None + return repo @pytest.fixture def mock_email_service(): + """mock 邮件服务.""" 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 +# ── RequestPasswordResetUseCase 测试 ──────────────────────────────────────── -class TestRequestPasswordResetRequest: - """RequestPasswordResetRequest 测试""" +class TestRequestPasswordReset: + """请求密码重置用例测试""" - def test_email_lowercased_and_stripped(self): - """邮箱转小写并去空格""" - req = RequestPasswordResetRequest(" User@Example.COM ") - assert req.email == "user@example.com" + def test_request_success_sends_email(self, mock_user_repo, mock_email_service): + """成功请求时发送重置邮件.""" + user = _make_user() + mock_user_repo.find_by_email.return_value = user - 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, + uc = RequestPasswordResetUseCase( + user_repository=mock_user_repo, base_url="https://app.example.com", email_service=mock_email_service, ) - request = RequestPasswordResetRequest("user@example.com") - use_case.execute(request) + req = RequestPasswordResetRequest(email="user@example.com") + ok, error = uc.execute(req) - 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 + assert ok is True + assert error is None + # 用户被更新了 reset_token + mock_user_repo.save.assert_called_once() + saved_user = mock_user_repo.save.call_args[0][0] + assert saved_user.password_reset_token is not None + assert len(saved_user.password_reset_token) > 0 + assert saved_user.password_reset_expires_at is not None + # 邮件发送了 + mock_email_service.send_password_reset_email.assert_called_once() + call_kwargs = mock_email_service.send_password_reset_email.call_args.kwargs + assert call_kwargs["to_email"] == "user@example.com" + assert "reset-password?token=" in call_kwargs["reset_url"] + assert "https://app.example.com" in call_kwargs["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 + def test_request_nonexistent_user_returns_success(self, mock_user_repo, mock_email_service): + """用户不存在时也返回成功(不暴露用户存在性).""" + mock_user_repo.find_by_email.return_value = None + + uc = RequestPasswordResetUseCase( + user_repository=mock_user_repo, + base_url="https://app.example.com", + email_service=mock_email_service, + ) + req = RequestPasswordResetRequest(email="nonexistent@example.com") + ok, error = uc.execute(req) + + assert ok 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(self, mock_user_repo, mock_email_service): + """空邮箱返回错误.""" + uc = RequestPasswordResetUseCase( + user_repository=mock_user_repo, + base_url="https://app.example.com", + email_service=mock_email_service, + ) + req = RequestPasswordResetRequest(email="") + ok, error = uc.execute(req) + + assert ok is False + assert "Email is required" in error + + def test_request_email_normalized(self, mock_user_repo, mock_email_service): + """邮箱会被规范化(小写+去空格).""" + user = _make_user() + mock_user_repo.find_by_email.return_value = user + + uc = RequestPasswordResetUseCase( + user_repository=mock_user_repo, + base_url="https://app.example.com", + email_service=mock_email_service, + ) + req = RequestPasswordResetRequest(email=" USER@Example.COM ") + ok, _ = uc.execute(req) + + assert ok is True + # find_by_email 收到的是小写的 + mock_user_repo.find_by_email.assert_called_with("user@example.com") + + def test_request_token_expiry_custom_hours(self, mock_user_repo, mock_email_service): + """自定义令牌过期时间.""" + user = _make_user() + mock_user_repo.find_by_email.return_value = user + + uc = RequestPasswordResetUseCase( + user_repository=mock_user_repo, + base_url="https://app.example.com", + token_expire_hours=6, + email_service=mock_email_service, + ) + req = RequestPasswordResetRequest(email="user@example.com") + before = datetime.now(timezone.utc) + ok, _ = uc.execute(req) + after = datetime.now(timezone.utc) + + assert ok is True + saved_user = mock_user_repo.save.call_args[0][0] + expires_at = saved_user.password_reset_expires_at + # 过期时间应该在 ~6 小时后 + expected_min = before + timedelta(hours=6) + expected_max = after + timedelta(hours=6) + assert expected_min <= expires_at <= expected_max + + def test_request_email_failure_returns_success(self, mock_user_repo, mock_email_service): + """邮件发送失败不影响返回结果(安全考虑).""" + user = _make_user() + mock_user_repo.find_by_email.return_value = user mock_email_service.send_password_reset_email.return_value = (False, "SMTP error") - use_case = RequestPasswordResetUseCase( - mock_user_repo, - base_url="https://example.com", + uc = RequestPasswordResetUseCase( + user_repository=mock_user_repo, + base_url="https://app.example.com", email_service=mock_email_service, ) - request = RequestPasswordResetRequest("user@example.com") - success, error = use_case.execute(request) + req = RequestPasswordResetRequest(email="user@example.com") + ok, error = uc.execute(req) - assert success is True + assert ok is True + assert error is None + # token 仍然保存了 + mock_user_repo.save.assert_called_once() + + def test_request_email_exception_does_not_propagate(self, mock_user_repo, mock_email_service): + """邮件服务异常不向外传播.""" + user = _make_user() + mock_user_repo.find_by_email.return_value = user + mock_email_service.send_password_reset_email.side_effect = Exception("SMTP down") + + uc = RequestPasswordResetUseCase( + user_repository=mock_user_repo, + base_url="https://app.example.com", + email_service=mock_email_service, + ) + req = RequestPasswordResetRequest(email="user@example.com") + ok, error = uc.execute(req) + + assert ok 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 + def test_request_username_uses_display_name_fallback(self, mock_user_repo, mock_email_service): + """用户名为空时用 display_name.""" + user = _make_user(username="", display_name="Display Name") + mock_user_repo.find_by_email.return_value = user - use_case = RequestPasswordResetUseCase( - mock_user_repo, - base_url="https://example.com", + uc = RequestPasswordResetUseCase( + user_repository=mock_user_repo, + base_url="https://app.example.com", email_service=mock_email_service, ) - request = RequestPasswordResetRequest("user@example.com") + req = RequestPasswordResetRequest(email="user@example.com") + uc.execute(req) - use_case.execute(request) - token1 = sample_user.password_reset_token + call_kwargs = mock_email_service.send_password_reset_email.call_args.kwargs + assert call_kwargs["username"] == "Display Name" - use_case.execute(request) - token2 = sample_user.password_reset_token + def test_request_uses_username_when_available(self, mock_user_repo, mock_email_service): + """有用户名时用用户名.""" + user = _make_user(username="myusername", display_name="Display Name") + mock_user_repo.find_by_email.return_value = user - assert token1 != token2 + uc = RequestPasswordResetUseCase( + user_repository=mock_user_repo, + base_url="https://app.example.com", + email_service=mock_email_service, + ) + req = RequestPasswordResetRequest(email="user@example.com") + uc.execute(req) + + call_kwargs = mock_email_service.send_password_reset_email.call_args.kwargs + assert call_kwargs["username"] == "myusername" + + def test_request_generates_unique_tokens(self, mock_user_repo, mock_email_service): + """每次请求生成不同的令牌.""" + user = _make_user() + mock_user_repo.find_by_email.return_value = user + + uc = RequestPasswordResetUseCase( + user_repository=mock_user_repo, + base_url="https://app.example.com", + email_service=mock_email_service, + ) + + tokens = [] + for _ in range(3): + req = RequestPasswordResetRequest(email="user@example.com") + uc.execute(req) + saved_user = mock_user_repo.save.call_args[0][0] + tokens.append(saved_user.password_reset_token) + + assert len(set(tokens)) == 3 # 三个不同的令牌 + + def test_request_general_exception_returns_error(self, mock_user_repo, mock_email_service): + """其他异常返回错误信息.""" + mock_user_repo.find_by_email.side_effect = Exception("DB connection error") + + uc = RequestPasswordResetUseCase( + user_repository=mock_user_repo, + base_url="https://app.example.com", + email_service=mock_email_service, + ) + req = RequestPasswordResetRequest(email="user@example.com") + ok, error = uc.execute(req) + + assert ok is False + assert "failed" in error.lower() + + +# ── ResetPasswordUseCase 测试 ─────────────────────────────────────────────── + + +class TestResetPassword: + """重置密码用例测试""" + + def test_reset_success(self, mock_user_repo): + """成功重置密码.""" + user = _make_user() + user.password_reset_token = "valid-token-123" + user.password_reset_expires_at = datetime.now(timezone.utc) + timedelta(hours=1) + mock_user_repo.find_by_password_reset_token.return_value = user + + uc = ResetPasswordUseCase(user_repository=mock_user_repo) + + # mock password_hasher 和 password_validator + with ( + patch("packages.application.auth.password_reset_use_case.password_hasher") as mock_hasher, + patch("packages.application.auth.password_reset_use_case.password_validator") as mock_validator, + ): + mock_validator.validate.return_value = (True, None) + mock_hasher.hash_password.return_value = "new_hashed_password" + + req = ResetPasswordRequest(token="valid-token-123", new_password="NewPass123!") + ok, error = uc.execute(req) + + assert ok is True + assert error is None + # 密码被更新 + mock_user_repo.save.assert_called_once() + saved_user = mock_user_repo.save.call_args[0][0] + assert saved_user.password_hash == "new_hashed_password" + # 令牌被清除 + assert saved_user.password_reset_token is None + assert saved_user.password_reset_expires_at is None + + def test_reset_empty_token(self, mock_user_repo): + """空令牌返回错误.""" + uc = ResetPasswordUseCase(user_repository=mock_user_repo) + req = ResetPasswordRequest(token="", new_password="NewPass123!") + ok, error = uc.execute(req) + + assert ok is False + assert "token is required" in error.lower() + + def test_reset_empty_password(self, mock_user_repo): + """空密码返回错误.""" + uc = ResetPasswordUseCase(user_repository=mock_user_repo) + req = ResetPasswordRequest(token="valid-token", new_password="") + ok, error = uc.execute(req) + + assert ok is False + assert "password is required" in error.lower() + + def test_reset_invalid_token(self, mock_user_repo): + """无效令牌返回错误.""" + mock_user_repo.find_by_password_reset_token.return_value = None + + uc = ResetPasswordUseCase(user_repository=mock_user_repo) + + with patch("packages.application.auth.password_reset_use_case.password_validator") as mock_validator: + mock_validator.validate.return_value = (True, None) + req = ResetPasswordRequest(token="invalid-token", new_password="NewPass123!") + ok, error = uc.execute(req) + + assert ok is False + assert "Invalid or expired" in error + + def test_reset_expired_token(self, mock_user_repo): + """过期令牌返回错误.""" + user = _make_user() + user.password_reset_token = "expired-token" + user.password_reset_expires_at = datetime.now(timezone.utc) - timedelta(hours=1) + mock_user_repo.find_by_password_reset_token.return_value = user + + uc = ResetPasswordUseCase(user_repository=mock_user_repo) + + with patch("packages.application.auth.password_reset_use_case.password_validator") as mock_validator: + mock_validator.validate.return_value = (True, None) + req = ResetPasswordRequest(token="expired-token", new_password="NewPass123!") + ok, error = uc.execute(req) + + assert ok is False + assert "expired" in error.lower() + + def test_reset_naive_datetime_treated_as_utc(self, mock_user_repo): + """不带时区的过期时间被当作 UTC 处理.""" + user = _make_user() + user.password_reset_token = "token-123" + # 用 naive datetime(无时区),应该被当作 UTC + user.password_reset_expires_at = datetime.utcnow() - timedelta(hours=1) # type: ignore + mock_user_repo.find_by_password_reset_token.return_value = user + + uc = ResetPasswordUseCase(user_repository=mock_user_repo) + + with patch("packages.application.auth.password_reset_use_case.password_validator") as mock_validator: + mock_validator.validate.return_value = (True, None) + req = ResetPasswordRequest(token="token-123", new_password="NewPass123!") + ok, error = uc.execute(req) + + assert ok is False + assert "expired" in error.lower() + + def test_reset_weak_password_fails(self, mock_user_repo): + """弱密码被拒绝.""" + user = _make_user() + user.password_reset_token = "valid-token" + user.password_reset_expires_at = datetime.now(timezone.utc) + timedelta(hours=1) + mock_user_repo.find_by_password_reset_token.return_value = user + + uc = ResetPasswordUseCase(user_repository=mock_user_repo) + + with patch("packages.application.auth.password_reset_use_case.password_validator") as mock_validator: + mock_validator.validate.return_value = (False, "Password too short") + req = ResetPasswordRequest(token="valid-token", new_password="123") + ok, error = uc.execute(req) + + assert ok is False + assert "too short" in error.lower() + # 密码没被更新 + mock_user_repo.save.assert_not_called() + + def test_reset_no_expires_at_still_works(self, mock_user_repo): + """没有过期时间时视为不过期.""" + user = _make_user() + user.password_reset_token = "valid-token" + user.password_reset_expires_at = None + mock_user_repo.find_by_password_reset_token.return_value = user + + uc = ResetPasswordUseCase(user_repository=mock_user_repo) + + with ( + patch("packages.application.auth.password_reset_use_case.password_hasher") as mock_hasher, + patch("packages.application.auth.password_reset_use_case.password_validator") as mock_validator, + ): + mock_validator.validate.return_value = (True, None) + mock_hasher.hash_password.return_value = "newhash" + req = ResetPasswordRequest(token="valid-token", new_password="NewPass123!") + ok, error = uc.execute(req) + + assert ok is True + assert error is None + + def test_reset_exception_returns_error(self, mock_user_repo): + """异常情况返回错误信息.""" + mock_user_repo.find_by_password_reset_token.side_effect = Exception("DB error") + + uc = ResetPasswordUseCase(user_repository=mock_user_repo) + req = ResetPasswordRequest(token="token", new_password="NewPass123!") + ok, error = uc.execute(req) + + assert ok is False + assert "failed" in error.lower() + + def test_reset_clears_token_on_success(self, mock_user_repo): + """成功重置后令牌被清除,防止重复使用.""" + user = _make_user() + user.password_reset_token = "valid-token" + user.password_reset_expires_at = datetime.now(timezone.utc) + timedelta(hours=1) + mock_user_repo.find_by_password_reset_token.return_value = user + + uc = ResetPasswordUseCase(user_repository=mock_user_repo) + + with ( + patch("packages.application.auth.password_reset_use_case.password_hasher") as mock_hasher, + patch("packages.application.auth.password_reset_use_case.password_validator") as mock_validator, + ): + mock_validator.validate.return_value = (True, None) + mock_hasher.hash_password.return_value = "newhash" + req = ResetPasswordRequest(token="valid-token", new_password="NewPass123!") + ok, _ = uc.execute(req) + + assert ok is True + saved_user = mock_user_repo.save.call_args[0][0] + assert saved_user.password_reset_token is None + assert saved_user.password_reset_expires_at is None + + def test_reset_hashes_new_password(self, mock_user_repo): + """密码被哈希后保存.""" + user = _make_user() + user.password_reset_token = "valid-token" + user.password_reset_expires_at = datetime.now(timezone.utc) + timedelta(hours=1) + mock_user_repo.find_by_password_reset_token.return_value = user + + uc = ResetPasswordUseCase(user_repository=mock_user_repo) + + with ( + patch("packages.application.auth.password_reset_use_case.password_hasher") as mock_hasher, + patch("packages.application.auth.password_reset_use_case.password_validator") as mock_validator, + ): + mock_validator.validate.return_value = (True, None) + mock_hasher.hash_password.return_value = "hashed_abcdef" + req = ResetPasswordRequest(token="valid-token", new_password="MyNewPass123!") + uc.execute(req) + + mock_hasher.hash_password.assert_called_once_with("MyNewPass123!") + saved_user = mock_user_repo.save.call_args[0][0] + assert saved_user.password_hash == "hashed_abcdef" + + +# ── RequestPasswordResetRequest 测试 ──────────────────────────────────────── + + +class TestRequestPasswordResetRequest: + """请求数据类测试""" + + def test_email_stripped_and_lowercased(self): + req = RequestPasswordResetRequest(email=" USER@Example.COM ") + assert req.email == "user@example.com" + + def test_email_already_lowercase(self): + req = RequestPasswordResetRequest(email="user@example.com") + assert req.email == "user@example.com" + + +# ── ResetPasswordRequest 测试 ─────────────────────────────────────────────── 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 + req = ResetPasswordRequest(token="token123", new_password="password123") + assert req.token == "token123" + assert req.new_password == "password123"