Files
xiaoxia-saas/tests/unit/test_bind_contact_use_case.py
CI Bot 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
style: auto-format with black + isort + prettier
2026-07-24 08:47:07 +00:00

479 lines
17 KiB
Python
Executable File

"""绑定联系方式 UseCase 单元测试."""
from __future__ import annotations
from unittest.mock import MagicMock
import pytest
from packages.application.auth.bind_contact_use_case import (
BindContactRequest,
BindContactUseCase,
SendVerificationCodeRequest,
SendVerificationCodeUseCase,
)
from packages.domain.entities import User
@pytest.fixture
def mock_user_repo():
return MagicMock()
@pytest.fixture
def mock_verification_service():
svc = MagicMock()
svc.verify.return_value = (True, None)
return svc
@pytest.fixture
def sample_user():
user = User(
id="user_001",
email="",
display_name="测试用户",
phone_verified=False,
email_verified=False,
)
user.phone = None
return user
class TestBindContactRequest:
"""BindContactRequest 测试"""
def test_phone_strips_plus86(self):
"""手机号 +86 前缀会被去掉"""
req = BindContactRequest(user_id="u1", phone="+8613800000001", phone_code="1234")
assert req.phone == "13800000001"
def test_email_lowercased(self):
"""邮箱会被转小写"""
req = BindContactRequest(user_id="u1", email="Test@Example.COM", email_code="1234")
assert req.email == "test@example.com"
def test_code_stripped(self):
"""验证码会被 strip"""
req = BindContactRequest(user_id="u1", phone="13800000001", phone_code=" 1234 ")
assert req.phone_code == "1234"
def test_empty_fields(self):
"""空字段处理"""
req = BindContactRequest(user_id="u1")
assert req.phone == ""
assert req.email == ""
assert req.phone_code == ""
assert req.email_code == ""
class TestBindContactUseCase:
"""BindContactUseCase 测试"""
def test_bind_phone_success(self, mock_user_repo, mock_verification_service, sample_user):
"""绑定手机号成功"""
mock_user_repo.find_by_id.return_value = sample_user
mock_user_repo.find_by_phone.return_value = None
mock_user_repo.save.return_value = sample_user
use_case = BindContactUseCase(mock_user_repo, mock_verification_service)
request = BindContactRequest(
user_id="user_001",
phone="13800000001",
phone_code="123456",
)
response, error = use_case.execute(request)
assert error is None
assert response is not None
assert response.user.phone == "13800000001"
assert response.user.phone_verified is True
mock_user_repo.save.assert_called_once()
def test_bind_email_success(self, mock_user_repo, mock_verification_service, sample_user):
"""绑定邮箱成功"""
mock_user_repo.find_by_id.return_value = sample_user
mock_user_repo.find_by_email.return_value = None
mock_user_repo.save.return_value = sample_user
use_case = BindContactUseCase(mock_user_repo, mock_verification_service)
request = BindContactRequest(
user_id="user_001",
email="test@example.com",
email_code="123456",
)
response, error = use_case.execute(request)
assert error is None
assert response is not None
assert response.user.email == "test@example.com"
assert response.user.email_verified is True
def test_bind_phone_and_email(self, mock_user_repo, mock_verification_service, sample_user):
"""同时绑定手机和邮箱"""
mock_user_repo.find_by_id.return_value = sample_user
mock_user_repo.find_by_phone.return_value = None
mock_user_repo.find_by_email.return_value = None
mock_user_repo.save.return_value = sample_user
use_case = BindContactUseCase(mock_user_repo, mock_verification_service)
request = BindContactRequest(
user_id="user_001",
phone="13800000001",
phone_code="123456",
email="test@example.com",
email_code="123456",
)
response, error = use_case.execute(request)
assert error is None
assert response.user.phone == "13800000001"
assert response.user.phone_verified is True
assert response.user.email == "test@example.com"
assert response.user.email_verified is True
# 两个都绑定完成,binding_completed_at 应该被设置
assert response.user.binding_completed_at is not None
def test_no_contact_info_returns_error(self, mock_user_repo, mock_verification_service):
"""既没填手机也没填邮箱返回错误"""
use_case = BindContactUseCase(mock_user_repo, mock_verification_service)
request = BindContactRequest(user_id="user_001")
response, error = use_case.execute(request)
assert response is None
assert "至少填写" in error
mock_user_repo.find_by_id.assert_not_called()
def test_user_not_found(self, mock_user_repo, mock_verification_service):
"""用户不存在返回错误"""
mock_user_repo.find_by_id.return_value = None
use_case = BindContactUseCase(mock_user_repo, mock_verification_service)
request = BindContactRequest(
user_id="nonexistent",
phone="13800000001",
phone_code="123456",
)
response, error = use_case.execute(request)
assert response is None
assert "用户不存在" in error
def test_phone_already_bound_by_other(self, mock_user_repo, mock_verification_service, sample_user):
"""手机号已被其他账号绑定"""
other_user = MagicMock()
other_user.id = "user_other"
mock_user_repo.find_by_id.return_value = sample_user
mock_user_repo.find_by_phone.return_value = other_user
use_case = BindContactUseCase(mock_user_repo, mock_verification_service)
request = BindContactRequest(
user_id="user_001",
phone="13800000001",
phone_code="123456",
)
response, error = use_case.execute(request)
assert response is None
assert "已被其他账号绑定" in error
mock_user_repo.save.assert_not_called()
def test_phone_bound_by_self_ok(self, mock_user_repo, mock_verification_service, sample_user):
"""手机号已被自己绑定,允许"""
sample_user.phone = "13800000001"
mock_user_repo.find_by_id.return_value = sample_user
mock_user_repo.find_by_phone.return_value = sample_user
mock_user_repo.save.return_value = sample_user
use_case = BindContactUseCase(mock_user_repo, mock_verification_service)
request = BindContactRequest(
user_id="user_001",
phone="13800000001",
phone_code="123456",
)
response, error = use_case.execute(request)
assert error is None
assert response is not None
def test_wrong_phone_code(self, mock_user_repo, mock_verification_service, sample_user):
"""手机验证码错误"""
mock_user_repo.find_by_id.return_value = sample_user
mock_user_repo.find_by_phone.return_value = None
mock_verification_service.verify.return_value = (False, "验证码过期")
use_case = BindContactUseCase(mock_user_repo, mock_verification_service)
request = BindContactRequest(
user_id="user_001",
phone="13800000001",
phone_code="000000",
)
response, error = use_case.execute(request)
assert response is None
assert "手机验证码错误" in error
mock_user_repo.save.assert_not_called()
def test_missing_phone_code(self, mock_user_repo, mock_verification_service, sample_user):
"""缺少手机验证码"""
mock_user_repo.find_by_id.return_value = sample_user
use_case = BindContactUseCase(mock_user_repo, mock_verification_service)
request = BindContactRequest(
user_id="user_001",
phone="13800000001",
phone_code="",
)
response, error = use_case.execute(request)
assert response is None
assert "请输入手机验证码" in error
def test_invalid_phone_format(self, mock_user_repo, mock_verification_service, sample_user):
"""手机号格式不正确"""
mock_user_repo.find_by_id.return_value = sample_user
use_case = BindContactUseCase(mock_user_repo, mock_verification_service)
request = BindContactRequest(
user_id="user_001",
phone="123", # 太短
phone_code="123456",
)
response, error = use_case.execute(request)
assert response is None
assert error is not None
def test_email_already_bound_by_other(self, mock_user_repo, mock_verification_service, sample_user):
"""邮箱已被其他账号绑定"""
other_user = MagicMock()
other_user.id = "user_other"
mock_user_repo.find_by_id.return_value = sample_user
mock_user_repo.find_by_email.return_value = other_user
use_case = BindContactUseCase(mock_user_repo, mock_verification_service)
request = BindContactRequest(
user_id="user_001",
email="test@example.com",
email_code="123456",
)
response, error = use_case.execute(request)
assert response is None
assert "已被其他账号绑定" in error
def test_missing_email_code(self, mock_user_repo, mock_verification_service, sample_user):
"""缺少邮箱验证码"""
mock_user_repo.find_by_id.return_value = sample_user
mock_user_repo.find_by_email.return_value = None
use_case = BindContactUseCase(mock_user_repo, mock_verification_service)
request = BindContactRequest(
user_id="user_001",
email="test@example.com",
email_code="",
)
response, error = use_case.execute(request)
assert response is None
assert "请输入邮箱验证码" in error
def test_invalid_email_format(self, mock_user_repo, mock_verification_service, sample_user):
"""邮箱格式不正确"""
mock_user_repo.find_by_id.return_value = sample_user
use_case = BindContactUseCase(mock_user_repo, mock_verification_service)
request = BindContactRequest(
user_id="user_001",
email="not_an_email",
email_code="123456",
)
response, error = use_case.execute(request)
assert response is None
assert error is not None
def test_response_to_dict(self, mock_user_repo, mock_verification_service, sample_user):
"""BindContactResponse.to_dict 返回正确格式"""
mock_user_repo.find_by_id.return_value = sample_user
mock_user_repo.find_by_phone.return_value = None
mock_user_repo.find_by_email.return_value = None
mock_user_repo.save.return_value = sample_user
use_case = BindContactUseCase(mock_user_repo, mock_verification_service)
request = BindContactRequest(
user_id="user_001",
phone="13800000001",
phone_code="123456",
email="test@example.com",
email_code="123456",
)
response, _ = use_case.execute(request)
data = response.to_dict()
assert "user" in data
assert data["user"]["id"] == "user_001"
assert "email" in data["user"]
assert "phone" in data["user"]
assert "phone_verified" in data["user"]
assert "display_name" in data["user"]
assert "binding_complete" in data["user"]
class TestSendVerificationCodeRequest:
"""SendVerificationCodeRequest 测试"""
def test_value_stripped(self):
"""value 会被 strip"""
req = SendVerificationCodeRequest(target="phone", value=" 13800000001 ", purpose="bind")
assert req.value == "13800000001"
class TestSendVerificationCodeUseCase:
"""SendVerificationCodeUseCase 测试"""
def test_send_phone_code_success(self, mock_verification_service):
"""发送手机验证码成功"""
from datetime import datetime, timedelta, timezone
code_obj = MagicMock()
code_obj.code = "123456"
code_obj.created_at = datetime.now(timezone.utc)
code_obj.expires_at = datetime.now(timezone.utc) + timedelta(minutes=5)
mock_verification_service.generate.return_value = (code_obj, None)
mock_sms = MagicMock()
use_case = SendVerificationCodeUseCase(
mock_verification_service,
sms_service=mock_sms,
)
request = SendVerificationCodeRequest(
target="phone",
value="13800000001",
purpose="bind",
)
response, error = use_case.execute(request)
assert error is None
assert response is not None
assert response.expires_in > 0
assert response.resend_after == 60
mock_sms.send_verification_code.assert_called_once()
def test_send_email_code_success(self, mock_verification_service):
"""发送邮箱验证码成功"""
from datetime import datetime, timedelta, timezone
code_obj = MagicMock()
code_obj.code = "654321"
code_obj.created_at = datetime.now(timezone.utc)
code_obj.expires_at = datetime.now(timezone.utc) + timedelta(minutes=5)
mock_verification_service.generate.return_value = (code_obj, None)
mock_email = MagicMock()
use_case = SendVerificationCodeUseCase(
mock_verification_service,
email_service=mock_email,
)
request = SendVerificationCodeRequest(
target="email",
value="test@example.com",
purpose="bind",
)
response, error = use_case.execute(request)
assert error is None
assert response is not None
mock_email.send_email.assert_called_once()
def test_invalid_target_returns_error(self, mock_verification_service):
"""不支持的目标类型返回错误"""
use_case = SendVerificationCodeUseCase(mock_verification_service)
request = SendVerificationCodeRequest(
target="wechat",
value="some_value",
purpose="bind",
)
response, error = use_case.execute(request)
assert response is None
assert "不支持的目标类型" in error
def test_invalid_phone_format(self, mock_verification_service):
"""手机号格式错误返回错误"""
use_case = SendVerificationCodeUseCase(mock_verification_service)
request = SendVerificationCodeRequest(
target="phone",
value="123",
purpose="bind",
)
response, error = use_case.execute(request)
assert response is None
assert error is not None
mock_verification_service.generate.assert_not_called()
def test_invalid_email_format(self, mock_verification_service):
"""邮箱格式错误返回错误"""
use_case = SendVerificationCodeUseCase(mock_verification_service)
request = SendVerificationCodeRequest(
target="email",
value="not_email",
purpose="bind",
)
response, error = use_case.execute(request)
assert response is None
assert error is not None
mock_verification_service.generate.assert_not_called()
def test_generate_failure_returns_error(self, mock_verification_service):
"""生成验证码失败返回错误"""
mock_verification_service.generate.return_value = (None, "发送太频繁")
use_case = SendVerificationCodeUseCase(mock_verification_service)
request = SendVerificationCodeRequest(
target="phone",
value="13800000001",
purpose="bind",
)
response, error = use_case.execute(request)
assert response is None
assert "发送太频繁" in error
def test_response_to_dict(self, mock_verification_service):
"""SendVerificationCodeResponse.to_dict 格式正确"""
from datetime import datetime, timedelta, timezone
code_obj = MagicMock()
code_obj.code = "123456"
code_obj.created_at = datetime.now(timezone.utc)
code_obj.expires_at = datetime.now(timezone.utc) + timedelta(seconds=300)
mock_verification_service.generate.return_value = (code_obj, None)
use_case = SendVerificationCodeUseCase(mock_verification_service)
request = SendVerificationCodeRequest(
target="phone",
value="13800000001",
purpose="bind",
)
response, _ = use_case.execute(request)
data = response.to_dict()
assert "expires_in" in data
assert "resend_after" in data