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
479 lines
17 KiB
Python
Executable File
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
|