3921a657e8
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Successful in 52s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m56s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m7s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m7s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 54s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 3m20s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 6m27s
CI/CD Pipeline / Unit Tests (push) Failing after 8m24s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 16m1s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m14s
CI/CD Pipeline / Integration Tests (push) Successful in 3m28s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 21s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m26s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m34s
435 lines
14 KiB
Python
Executable File
435 lines
14 KiB
Python
Executable File
"""用户注册 UseCase 单元测试."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from packages.application.auth.register_user_use_case import (
|
|
RegisterUserRequest,
|
|
RegisterUserUseCase,
|
|
VerifyEmailRequest,
|
|
VerifyEmailUseCase,
|
|
)
|
|
from packages.domain.entities import User
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_user_repo():
|
|
return MagicMock()
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_email_service():
|
|
svc = MagicMock()
|
|
svc.send_verification_email.return_value = (True, None)
|
|
return svc
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_user():
|
|
user = User(
|
|
id="user_001",
|
|
email="test@example.com",
|
|
username="testuser",
|
|
display_name="测试用户",
|
|
password_hash="hashed_pw",
|
|
)
|
|
user.email_verified = False
|
|
user.email_verification_token = "some_token"
|
|
return user
|
|
|
|
|
|
class TestRegisterUserRequest:
|
|
"""RegisterUserRequest 测试"""
|
|
|
|
def test_email_lowercased_stripped(self):
|
|
"""邮箱转小写并去空格"""
|
|
req = RegisterUserRequest(
|
|
email=" Test@Example.COM ",
|
|
password="TestPass1!",
|
|
username="testuser",
|
|
display_name="测试用户",
|
|
)
|
|
assert req.email == "test@example.com"
|
|
|
|
def test_username_stripped(self):
|
|
"""用户名去空格"""
|
|
req = RegisterUserRequest(
|
|
email="test@example.com",
|
|
password="TestPass1!",
|
|
username=" testuser ",
|
|
display_name="测试用户",
|
|
)
|
|
assert req.username == "testuser"
|
|
|
|
def test_display_name_stripped(self):
|
|
"""显示名去空格"""
|
|
req = RegisterUserRequest(
|
|
email="test@example.com",
|
|
password="TestPass1!",
|
|
username="testuser",
|
|
display_name=" 测试用户 ",
|
|
)
|
|
assert req.display_name == "测试用户"
|
|
|
|
|
|
class TestRegisterUserUseCase:
|
|
"""RegisterUserUseCase 测试"""
|
|
|
|
def test_register_success(self, mock_user_repo, mock_email_service):
|
|
"""注册成功"""
|
|
mock_user_repo.find_by_email.return_value = None
|
|
mock_user_repo.find_by_username.return_value = None
|
|
mock_user_repo.save.return_value = None
|
|
|
|
use_case = RegisterUserUseCase(
|
|
mock_user_repo,
|
|
base_url="https://example.com",
|
|
email_service=mock_email_service,
|
|
)
|
|
request = RegisterUserRequest(
|
|
email="newuser@example.com",
|
|
password="StrongPass1!",
|
|
username="newuser",
|
|
display_name="新用户",
|
|
)
|
|
response, error = use_case.execute(request)
|
|
|
|
assert error is None
|
|
assert response is not None
|
|
assert response.email == "newuser@example.com"
|
|
assert response.username == "newuser"
|
|
assert response.display_name == "新用户"
|
|
assert response.email_verification_sent is True
|
|
assert response.user_id is not None
|
|
mock_user_repo.save.assert_called_once()
|
|
mock_email_service.send_verification_email.assert_called_once()
|
|
|
|
def test_register_empty_email(self, mock_user_repo, mock_email_service):
|
|
"""空邮箱返回错误"""
|
|
use_case = RegisterUserUseCase(
|
|
mock_user_repo,
|
|
base_url="https://example.com",
|
|
email_service=mock_email_service,
|
|
)
|
|
request = RegisterUserRequest(
|
|
email="",
|
|
password="TestPass1!",
|
|
username="testuser",
|
|
display_name="测试",
|
|
)
|
|
response, error = use_case.execute(request)
|
|
|
|
assert response is None
|
|
assert "Email is required" in error
|
|
mock_user_repo.save.assert_not_called()
|
|
|
|
def test_register_empty_username(self, mock_user_repo, mock_email_service):
|
|
"""空用户名返回错误"""
|
|
use_case = RegisterUserUseCase(
|
|
mock_user_repo,
|
|
base_url="https://example.com",
|
|
email_service=mock_email_service,
|
|
)
|
|
request = RegisterUserRequest(
|
|
email="test@example.com",
|
|
password="TestPass1!",
|
|
username="",
|
|
display_name="测试",
|
|
)
|
|
response, error = use_case.execute(request)
|
|
|
|
assert response is None
|
|
assert "Username is required" in error
|
|
|
|
def test_register_empty_display_name(self, mock_user_repo, mock_email_service):
|
|
"""空显示名返回错误"""
|
|
use_case = RegisterUserUseCase(
|
|
mock_user_repo,
|
|
base_url="https://example.com",
|
|
email_service=mock_email_service,
|
|
)
|
|
request = RegisterUserRequest(
|
|
email="test@example.com",
|
|
password="TestPass1!",
|
|
username="testuser",
|
|
display_name="",
|
|
)
|
|
response, error = use_case.execute(request)
|
|
|
|
assert response is None
|
|
assert "Display name is required" in error
|
|
|
|
def test_register_weak_password(self, mock_user_repo, mock_email_service):
|
|
"""弱密码返回错误"""
|
|
mock_user_repo.find_by_email.return_value = None
|
|
mock_user_repo.find_by_username.return_value = None
|
|
|
|
use_case = RegisterUserUseCase(
|
|
mock_user_repo,
|
|
base_url="https://example.com",
|
|
email_service=mock_email_service,
|
|
)
|
|
request = RegisterUserRequest(
|
|
email="test@example.com",
|
|
password="weak",
|
|
username="testuser",
|
|
display_name="测试",
|
|
)
|
|
response, error = use_case.execute(request)
|
|
|
|
assert response is None
|
|
assert error is not None
|
|
mock_user_repo.save.assert_not_called()
|
|
|
|
def test_register_email_already_exists(self, mock_user_repo, mock_email_service, sample_user):
|
|
"""邮箱已被注册"""
|
|
mock_user_repo.find_by_email.return_value = sample_user
|
|
|
|
use_case = RegisterUserUseCase(
|
|
mock_user_repo,
|
|
base_url="https://example.com",
|
|
email_service=mock_email_service,
|
|
)
|
|
request = RegisterUserRequest(
|
|
email="test@example.com",
|
|
password="TestPass1!",
|
|
username="testuser",
|
|
display_name="测试",
|
|
)
|
|
response, error = use_case.execute(request)
|
|
|
|
assert response is None
|
|
assert "Email already registered" in error
|
|
mock_user_repo.save.assert_not_called()
|
|
|
|
def test_register_username_already_taken(self, mock_user_repo, mock_email_service, sample_user):
|
|
"""用户名已被占用"""
|
|
mock_user_repo.find_by_email.return_value = None
|
|
mock_user_repo.find_by_username.return_value = sample_user
|
|
|
|
use_case = RegisterUserUseCase(
|
|
mock_user_repo,
|
|
base_url="https://example.com",
|
|
email_service=mock_email_service,
|
|
)
|
|
request = RegisterUserRequest(
|
|
email="new@example.com",
|
|
password="TestPass1!",
|
|
username="existinguser",
|
|
display_name="测试",
|
|
)
|
|
response, error = use_case.execute(request)
|
|
|
|
assert response is None
|
|
assert "Username already taken" in error
|
|
mock_user_repo.save.assert_not_called()
|
|
|
|
def test_register_password_is_hashed(self, mock_user_repo, mock_email_service):
|
|
"""用户密码被哈希存储,不是明文"""
|
|
mock_user_repo.find_by_email.return_value = None
|
|
mock_user_repo.find_by_username.return_value = None
|
|
saved_user = None
|
|
|
|
def capture_save(user):
|
|
nonlocal saved_user
|
|
saved_user = user
|
|
|
|
mock_user_repo.save.side_effect = capture_save
|
|
|
|
use_case = RegisterUserUseCase(
|
|
mock_user_repo,
|
|
base_url="https://example.com",
|
|
email_service=mock_email_service,
|
|
)
|
|
request = RegisterUserRequest(
|
|
email="test@example.com",
|
|
password="MySecretPass1!",
|
|
username="testuser",
|
|
display_name="测试",
|
|
)
|
|
use_case.execute(request)
|
|
|
|
assert saved_user is not None
|
|
assert saved_user.password_hash != "MySecretPass1!"
|
|
assert len(saved_user.password_hash) > 0
|
|
|
|
def test_register_verification_token_generated(self, mock_user_repo, mock_email_service):
|
|
"""生成邮箱验证令牌"""
|
|
mock_user_repo.find_by_email.return_value = None
|
|
mock_user_repo.find_by_username.return_value = None
|
|
saved_user = None
|
|
|
|
def capture_save(user):
|
|
nonlocal saved_user
|
|
saved_user = user
|
|
|
|
mock_user_repo.save.side_effect = capture_save
|
|
|
|
use_case = RegisterUserUseCase(
|
|
mock_user_repo,
|
|
base_url="https://example.com",
|
|
email_service=mock_email_service,
|
|
)
|
|
request = RegisterUserRequest(
|
|
email="test@example.com",
|
|
password="TestPass1!",
|
|
username="testuser",
|
|
display_name="测试",
|
|
)
|
|
use_case.execute(request)
|
|
|
|
assert saved_user.email_verification_token is not None
|
|
assert len(saved_user.email_verification_token) > 0
|
|
assert saved_user.email_verified is False
|
|
|
|
def test_register_verification_email_contains_url(self, mock_user_repo, mock_email_service):
|
|
"""验证邮件包含正确的验证链接"""
|
|
mock_user_repo.find_by_email.return_value = None
|
|
mock_user_repo.find_by_username.return_value = None
|
|
|
|
use_case = RegisterUserUseCase(
|
|
mock_user_repo,
|
|
base_url="https://app.example.com",
|
|
email_service=mock_email_service,
|
|
)
|
|
request = RegisterUserRequest(
|
|
email="test@example.com",
|
|
password="TestPass1!",
|
|
username="testuser",
|
|
display_name="测试",
|
|
)
|
|
use_case.execute(request)
|
|
|
|
call_args = mock_email_service.send_verification_email.call_args
|
|
verif_url = call_args[1].get("verification_url", "") or ""
|
|
assert "https://app.example.com/verify-email?token=" in verif_url
|
|
|
|
def test_register_email_failure_still_creates_user(self, mock_user_repo, mock_email_service):
|
|
"""邮件发送失败但用户仍被创建"""
|
|
mock_user_repo.find_by_email.return_value = None
|
|
mock_user_repo.find_by_username.return_value = None
|
|
mock_email_service.send_verification_email.return_value = (False, "SMTP error")
|
|
|
|
use_case = RegisterUserUseCase(
|
|
mock_user_repo,
|
|
base_url="https://example.com",
|
|
email_service=mock_email_service,
|
|
)
|
|
request = RegisterUserRequest(
|
|
email="test@example.com",
|
|
password="TestPass1!",
|
|
username="testuser",
|
|
display_name="测试",
|
|
)
|
|
response, error = use_case.execute(request)
|
|
|
|
assert error is None
|
|
assert response is not None
|
|
assert response.email_verification_sent is False
|
|
mock_user_repo.save.assert_called_once()
|
|
|
|
def test_register_generates_user_id(self, mock_user_repo, mock_email_service):
|
|
"""新用户有 id"""
|
|
mock_user_repo.find_by_email.return_value = None
|
|
mock_user_repo.find_by_username.return_value = None
|
|
|
|
use_case = RegisterUserUseCase(
|
|
mock_user_repo,
|
|
base_url="https://example.com",
|
|
email_service=mock_email_service,
|
|
)
|
|
request = RegisterUserRequest(
|
|
email="test@example.com",
|
|
password="TestPass1!",
|
|
username="testuser",
|
|
display_name="测试",
|
|
)
|
|
response, _ = use_case.execute(request)
|
|
|
|
assert response.user_id is not None
|
|
assert len(response.user_id) > 0
|
|
|
|
def test_register_two_users_different_ids(self, mock_user_repo, mock_email_service):
|
|
"""两个用户的 id 不同"""
|
|
mock_user_repo.find_by_email.return_value = None
|
|
mock_user_repo.find_by_username.return_value = None
|
|
|
|
use_case = RegisterUserUseCase(
|
|
mock_user_repo,
|
|
base_url="https://example.com",
|
|
email_service=mock_email_service,
|
|
)
|
|
r1 = RegisterUserRequest(
|
|
email="user1@example.com",
|
|
password="TestPass1!",
|
|
username="user1",
|
|
display_name="用户1",
|
|
)
|
|
r2 = RegisterUserRequest(
|
|
email="user2@example.com",
|
|
password="TestPass1!",
|
|
username="user2",
|
|
display_name="用户2",
|
|
)
|
|
|
|
resp1, _ = use_case.execute(r1)
|
|
resp2, _ = use_case.execute(r2)
|
|
|
|
assert resp1.user_id != resp2.user_id
|
|
|
|
|
|
class TestVerifyEmailUseCase:
|
|
"""VerifyEmailUseCase 测试"""
|
|
|
|
def test_verify_success(self, mock_user_repo, sample_user):
|
|
"""邮箱验证成功"""
|
|
mock_user_repo.find_by_verification_token.return_value = sample_user
|
|
mock_user_repo.save.return_value = None
|
|
|
|
use_case = VerifyEmailUseCase(mock_user_repo)
|
|
request = VerifyEmailRequest(token="some_token")
|
|
success, error = use_case.execute(request)
|
|
|
|
assert success is True
|
|
assert error is None
|
|
assert sample_user.email_verified is True
|
|
assert sample_user.email_verification_token is None
|
|
mock_user_repo.save.assert_called_once()
|
|
|
|
def test_verify_empty_token(self, mock_user_repo):
|
|
"""空 token 返回错误"""
|
|
use_case = VerifyEmailUseCase(mock_user_repo)
|
|
request = VerifyEmailRequest(token="")
|
|
success, error = use_case.execute(request)
|
|
|
|
assert success is False
|
|
assert "Verification token is required" in error
|
|
mock_user_repo.save.assert_not_called()
|
|
|
|
def test_verify_invalid_token(self, mock_user_repo):
|
|
"""无效 token 返回错误"""
|
|
mock_user_repo.find_by_verification_token.return_value = None
|
|
|
|
use_case = VerifyEmailUseCase(mock_user_repo)
|
|
request = VerifyEmailRequest(token="invalid_token")
|
|
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_verify_already_verified(self, mock_user_repo, sample_user):
|
|
"""已验证的用户再次验证也返回成功"""
|
|
sample_user.email_verified = True
|
|
mock_user_repo.find_by_verification_token.return_value = sample_user
|
|
|
|
use_case = VerifyEmailUseCase(mock_user_repo)
|
|
request = VerifyEmailRequest(token="some_token")
|
|
success, error = use_case.execute(request)
|
|
|
|
assert success is True
|
|
assert error is None
|