f36aaea374
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m27s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m15s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m3s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m22s
CI/CD Pipeline / Unit Tests (push) Successful in 7m55s
CI/CD Pipeline / Integration Tests (push) Successful in 3m4s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m42s
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 / Build Staging API Image (push) Successful in 19m15s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m35s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 8m9s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m38s
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 / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Successful in 2m28s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m49s
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
730 lines
28 KiB
Python
Executable File
730 lines
28 KiB
Python
Executable File
"""绑定联系方式 Use Case 单元测试."""
|
|
|
|
from datetime import datetime, timedelta, timezone
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from packages.application.auth.bind_contact_use_case import (
|
|
BindContactRequest,
|
|
BindContactResponse,
|
|
BindContactUseCase,
|
|
SendVerificationCodeRequest,
|
|
SendVerificationCodeResponse,
|
|
SendVerificationCodeUseCase,
|
|
)
|
|
from packages.domain.verification_code import VerificationCode
|
|
|
|
# ── Test Fixtures ────────────────────────────────────────────────────────────
|
|
|
|
|
|
def _make_user(
|
|
user_id="user-1",
|
|
email="wechat_user@wechat.local",
|
|
username="",
|
|
display_name="微信用户",
|
|
phone=None,
|
|
phone_verified=False,
|
|
email_verified=False,
|
|
binding_completed_at=None,
|
|
):
|
|
"""创建测试用户."""
|
|
from packages.domain.entities import User
|
|
|
|
user = User(
|
|
id=user_id,
|
|
email=email,
|
|
display_name=display_name,
|
|
username=username,
|
|
)
|
|
user.phone = phone
|
|
user.phone_verified = phone_verified
|
|
user.email_verified = email_verified
|
|
user.binding_completed_at = binding_completed_at
|
|
return user
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_user_repo():
|
|
repo = MagicMock()
|
|
repo.find_by_id.return_value = None
|
|
repo.find_by_phone.return_value = None
|
|
repo.find_by_email.return_value = None
|
|
repo.save.return_value = None
|
|
return repo
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_verification_service():
|
|
svc = MagicMock()
|
|
svc.verify.return_value = (True, None)
|
|
svc.generate.return_value = (None, None)
|
|
return svc
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_email_service():
|
|
return MagicMock()
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_sms_service():
|
|
return MagicMock()
|
|
|
|
|
|
# ── BindContactRequest 测试 ─────────────────────────────────────────────────
|
|
|
|
|
|
class TestBindContactRequest:
|
|
"""绑定请求数据类测试"""
|
|
|
|
def test_phone_normalized(self):
|
|
"""手机号被标准化."""
|
|
req = BindContactRequest(user_id="u1", phone="+8613800138000", phone_code="123456")
|
|
assert req.phone == "13800138000"
|
|
|
|
def test_phone_code_stripped(self):
|
|
"""手机验证码去除前后空格."""
|
|
req = BindContactRequest(user_id="u1", phone="13800138000", phone_code=" 123 ")
|
|
assert req.phone_code == "123"
|
|
|
|
def test_email_normalized(self):
|
|
"""邮箱被小写化+去空格."""
|
|
req = BindContactRequest(user_id="u1", email=" User@Example.COM ", email_code="abcdef")
|
|
assert req.email == "user@example.com"
|
|
|
|
def test_email_code_stripped(self):
|
|
"""邮箱验证码去除前后空格."""
|
|
req = BindContactRequest(user_id="u1", email="u@e.com", email_code=" abc ")
|
|
assert req.email_code == "abc"
|
|
|
|
def test_empty_phone_stays_empty(self):
|
|
"""空手机号保持空."""
|
|
req = BindContactRequest(user_id="u1", phone="", email="u@e.com")
|
|
assert req.phone == ""
|
|
|
|
def test_empty_email_stays_empty(self):
|
|
"""空邮箱保持空."""
|
|
req = BindContactRequest(user_id="u1", phone="13800138000", email="")
|
|
assert req.email == ""
|
|
|
|
def test_user_id_preserved(self):
|
|
"""user_id保持不变."""
|
|
req = BindContactRequest(user_id="user-abc-123", phone="13800138000")
|
|
assert req.user_id == "user-abc-123"
|
|
|
|
|
|
# ── BindContactResponse 测试 ────────────────────────────────────────────────
|
|
|
|
|
|
class TestBindContactResponse:
|
|
"""绑定响应数据类测试"""
|
|
|
|
def test_to_dict_structure(self):
|
|
"""to_dict 返回正确结构."""
|
|
user = _make_user(email="user@e.com", phone="13800138000", phone_verified=True)
|
|
resp = BindContactResponse(user=user)
|
|
d = resp.to_dict()
|
|
|
|
assert "user" in d
|
|
u = d["user"]
|
|
assert u["id"] == "user-1"
|
|
assert u["email"] == "user@e.com"
|
|
assert u["phone"] == "13800138000"
|
|
assert u["phone_verified"] is True
|
|
assert u["display_name"] == "微信用户"
|
|
assert u["binding_complete"] is False
|
|
|
|
def test_to_dict_binding_complete(self):
|
|
"""绑定完成时 binding_complete 为 True."""
|
|
user = _make_user(
|
|
email="user@e.com",
|
|
phone="13800138000",
|
|
phone_verified=True,
|
|
email_verified=True,
|
|
binding_completed_at=datetime.now(timezone.utc),
|
|
)
|
|
resp = BindContactResponse(user=user)
|
|
assert resp.to_dict()["user"]["binding_complete"] is True
|
|
|
|
|
|
# ── BindContactUseCase 手机绑定测试 ────────────────────────────────────────
|
|
|
|
|
|
class TestBindContactPhone:
|
|
"""手机绑定测试"""
|
|
|
|
def test_bind_phone_success(self, mock_user_repo, mock_verification_service):
|
|
"""成功绑定手机号."""
|
|
user = _make_user()
|
|
mock_user_repo.find_by_id.return_value = user
|
|
mock_user_repo.find_by_phone.return_value = None
|
|
mock_verification_service.verify.return_value = (True, None)
|
|
|
|
uc = BindContactUseCase(
|
|
user_repository=mock_user_repo,
|
|
verification_code_service=mock_verification_service,
|
|
)
|
|
req = BindContactRequest(user_id="user-1", phone="13800138000", phone_code="123456")
|
|
resp, error = uc.execute(req)
|
|
|
|
assert error is None
|
|
assert resp is not None
|
|
assert resp.user.phone == "13800138000"
|
|
assert resp.user.phone_verified is True
|
|
mock_user_repo.save.assert_called_once()
|
|
mock_verification_service.verify.assert_called_once()
|
|
|
|
def test_bind_phone_invalid_format(self, mock_user_repo, mock_verification_service):
|
|
"""手机号格式错误."""
|
|
user = _make_user()
|
|
mock_user_repo.find_by_id.return_value = user
|
|
|
|
uc = BindContactUseCase(
|
|
user_repository=mock_user_repo,
|
|
verification_code_service=mock_verification_service,
|
|
)
|
|
req = BindContactRequest(user_id="user-1", phone="123", phone_code="123456")
|
|
resp, error = uc.execute(req)
|
|
|
|
assert resp is None
|
|
assert "格式" in error or "不正确" in error
|
|
|
|
def test_bind_phone_missing_code(self, mock_user_repo, mock_verification_service):
|
|
"""缺少手机验证码."""
|
|
user = _make_user()
|
|
mock_user_repo.find_by_id.return_value = user
|
|
|
|
uc = BindContactUseCase(
|
|
user_repository=mock_user_repo,
|
|
verification_code_service=mock_verification_service,
|
|
)
|
|
req = BindContactRequest(user_id="user-1", phone="13800138000", phone_code="")
|
|
resp, error = uc.execute(req)
|
|
|
|
assert resp is None
|
|
assert "请输入手机验证码" in error
|
|
|
|
def test_bind_phone_already_used(self, mock_user_repo, mock_verification_service):
|
|
"""手机号已被其他账号绑定."""
|
|
from packages.domain.entities import User
|
|
|
|
user = _make_user()
|
|
other_user = User(id="user-2", email="o@e.com", display_name="Other")
|
|
other_user.phone = "13800138000"
|
|
|
|
mock_user_repo.find_by_id.return_value = user
|
|
mock_user_repo.find_by_phone.return_value = other_user
|
|
|
|
uc = BindContactUseCase(
|
|
user_repository=mock_user_repo,
|
|
verification_code_service=mock_verification_service,
|
|
)
|
|
req = BindContactRequest(user_id="user-1", phone="13800138000", phone_code="123456")
|
|
resp, error = uc.execute(req)
|
|
|
|
assert resp is None
|
|
assert "已被其他账号绑定" in error
|
|
|
|
def test_bind_phone_same_user_allowed(self, mock_user_repo, mock_verification_service):
|
|
"""同一用户绑定自己已有的手机号(重新验证)."""
|
|
user = _make_user(phone="13800138000", phone_verified=True)
|
|
mock_user_repo.find_by_id.return_value = user
|
|
mock_user_repo.find_by_phone.return_value = user # 同一个用户
|
|
mock_verification_service.verify.return_value = (True, None)
|
|
|
|
uc = BindContactUseCase(
|
|
user_repository=mock_user_repo,
|
|
verification_code_service=mock_verification_service,
|
|
)
|
|
req = BindContactRequest(user_id="user-1", phone="13800138000", phone_code="123456")
|
|
resp, error = uc.execute(req)
|
|
|
|
assert error is None
|
|
assert resp is not None
|
|
assert resp.user.phone == "13800138000"
|
|
|
|
def test_bind_phone_wrong_code(self, mock_user_repo, mock_verification_service):
|
|
"""手机验证码错误."""
|
|
user = _make_user()
|
|
mock_user_repo.find_by_id.return_value = user
|
|
mock_user_repo.find_by_phone.return_value = None
|
|
mock_verification_service.verify.return_value = (False, "验证码错误")
|
|
|
|
uc = BindContactUseCase(
|
|
user_repository=mock_user_repo,
|
|
verification_code_service=mock_verification_service,
|
|
)
|
|
req = BindContactRequest(user_id="user-1", phone="13800138000", phone_code="000000")
|
|
resp, error = uc.execute(req)
|
|
|
|
assert resp is None
|
|
assert "手机验证码错误" in error
|
|
|
|
def test_bind_phone_normalized(self, mock_user_repo, mock_verification_service):
|
|
"""+86前缀的手机号被标准化后再验证."""
|
|
user = _make_user()
|
|
mock_user_repo.find_by_id.return_value = user
|
|
mock_user_repo.find_by_phone.return_value = None
|
|
mock_verification_service.verify.return_value = (True, None)
|
|
|
|
uc = BindContactUseCase(
|
|
user_repository=mock_user_repo,
|
|
verification_code_service=mock_verification_service,
|
|
)
|
|
# BindContactRequest 会自动标准化 +86
|
|
req = BindContactRequest(user_id="user-1", phone="+8613800138000", phone_code="123456")
|
|
resp, error = uc.execute(req)
|
|
|
|
assert error is None
|
|
assert resp.user.phone == "13800138000" # 标准化后的号码
|
|
|
|
|
|
# ── BindContactUseCase 邮箱绑定测试 ────────────────────────────────────────
|
|
|
|
|
|
class TestBindContactEmail:
|
|
"""邮箱绑定测试"""
|
|
|
|
def test_bind_email_success(self, mock_user_repo, mock_verification_service):
|
|
"""成功绑定邮箱."""
|
|
user = _make_user()
|
|
mock_user_repo.find_by_id.return_value = user
|
|
mock_user_repo.find_by_email.return_value = None
|
|
mock_verification_service.verify.return_value = (True, None)
|
|
|
|
uc = BindContactUseCase(
|
|
user_repository=mock_user_repo,
|
|
verification_code_service=mock_verification_service,
|
|
)
|
|
req = BindContactRequest(user_id="user-1", email="new@example.com", email_code="abcdef")
|
|
resp, error = uc.execute(req)
|
|
|
|
assert error is None
|
|
assert resp is not None
|
|
assert resp.user.email == "new@example.com"
|
|
assert resp.user.email_verified is True
|
|
mock_user_repo.save.assert_called_once()
|
|
|
|
def test_bind_email_invalid_format(self, mock_user_repo, mock_verification_service):
|
|
"""邮箱格式错误."""
|
|
user = _make_user()
|
|
mock_user_repo.find_by_id.return_value = user
|
|
|
|
uc = BindContactUseCase(
|
|
user_repository=mock_user_repo,
|
|
verification_code_service=mock_verification_service,
|
|
)
|
|
req = BindContactRequest(user_id="user-1", email="invalid", email_code="abc")
|
|
resp, error = uc.execute(req)
|
|
|
|
assert resp is None
|
|
assert "格式" in error or "不正确" in error
|
|
|
|
def test_bind_email_missing_code(self, mock_user_repo, mock_verification_service):
|
|
"""缺少邮箱验证码."""
|
|
user = _make_user()
|
|
mock_user_repo.find_by_id.return_value = user
|
|
|
|
uc = BindContactUseCase(
|
|
user_repository=mock_user_repo,
|
|
verification_code_service=mock_verification_service,
|
|
)
|
|
req = BindContactRequest(user_id="user-1", email="u@e.com", email_code="")
|
|
resp, error = uc.execute(req)
|
|
|
|
assert resp is None
|
|
assert "请输入邮箱验证码" in error
|
|
|
|
def test_bind_email_already_used(self, mock_user_repo, mock_verification_service):
|
|
"""邮箱已被其他账号绑定."""
|
|
from packages.domain.entities import User
|
|
|
|
user = _make_user()
|
|
other_user = User(id="user-2", email="taken@e.com", display_name="Other")
|
|
mock_user_repo.find_by_id.return_value = user
|
|
mock_user_repo.find_by_email.return_value = other_user
|
|
|
|
uc = BindContactUseCase(
|
|
user_repository=mock_user_repo,
|
|
verification_code_service=mock_verification_service,
|
|
)
|
|
req = BindContactRequest(user_id="user-1", email="taken@e.com", email_code="abcdef")
|
|
resp, error = uc.execute(req)
|
|
|
|
assert resp is None
|
|
assert "已被其他账号绑定" in error
|
|
|
|
def test_bind_email_wrong_code(self, mock_user_repo, mock_verification_service):
|
|
"""邮箱验证码错误."""
|
|
user = _make_user()
|
|
mock_user_repo.find_by_id.return_value = user
|
|
mock_user_repo.find_by_email.return_value = None
|
|
mock_verification_service.verify.return_value = (False, "验证码错误")
|
|
|
|
uc = BindContactUseCase(
|
|
user_repository=mock_user_repo,
|
|
verification_code_service=mock_verification_service,
|
|
)
|
|
req = BindContactRequest(user_id="user-1", email="u@e.com", email_code="wrongcode")
|
|
resp, error = uc.execute(req)
|
|
|
|
assert resp is None
|
|
assert "邮箱验证码错误" in error
|
|
|
|
|
|
# ── BindContactUseCase 组合绑定测试 ────────────────────────────────────────
|
|
|
|
|
|
class TestBindContactCombined:
|
|
"""手机+邮箱组合绑定测试"""
|
|
|
|
def test_bind_both_phone_and_email(self, mock_user_repo, mock_verification_service):
|
|
"""同时绑定手机和邮箱."""
|
|
user = _make_user()
|
|
mock_user_repo.find_by_id.return_value = user
|
|
mock_user_repo.find_by_phone.return_value = None
|
|
mock_user_repo.find_by_email.return_value = None
|
|
mock_verification_service.verify.return_value = (True, None)
|
|
|
|
uc = BindContactUseCase(
|
|
user_repository=mock_user_repo,
|
|
verification_code_service=mock_verification_service,
|
|
)
|
|
req = BindContactRequest(
|
|
user_id="user-1",
|
|
phone="13800138000",
|
|
phone_code="123456",
|
|
email="user@e.com",
|
|
email_code="abcdef",
|
|
)
|
|
resp, error = uc.execute(req)
|
|
|
|
assert error is None
|
|
assert resp is not None
|
|
assert resp.user.phone == "13800138000"
|
|
assert resp.user.email == "user@e.com"
|
|
assert resp.user.phone_verified is True
|
|
assert resp.user.email_verified is True
|
|
# 两者都验证通过且邮箱不是wechat.local,应该标记绑定完成
|
|
assert resp.user.binding_completed_at is not None
|
|
|
|
def test_bind_both_phone_fails_email_not_attempted(self, mock_user_repo, mock_verification_service):
|
|
"""手机验证失败时不继续验证邮箱."""
|
|
user = _make_user()
|
|
mock_user_repo.find_by_id.return_value = user
|
|
mock_user_repo.find_by_phone.return_value = None
|
|
mock_verification_service.verify.return_value = (False, "验证码错误")
|
|
|
|
uc = BindContactUseCase(
|
|
user_repository=mock_user_repo,
|
|
verification_code_service=mock_verification_service,
|
|
)
|
|
req = BindContactRequest(
|
|
user_id="user-1",
|
|
phone="13800138000",
|
|
phone_code="wrong",
|
|
email="u@e.com",
|
|
email_code="abcdef",
|
|
)
|
|
resp, error = uc.execute(req)
|
|
|
|
assert resp is None
|
|
# verify 应该只被调用了一次(手机验证失败就返回了)
|
|
assert mock_verification_service.verify.call_count == 1
|
|
|
|
def test_binding_complete_only_with_real_email(self, mock_user_repo, mock_verification_service):
|
|
"""只有邮箱不是wechat.local时才算绑定完成."""
|
|
# 用户已有 @wechat.local 邮箱(微信登录的默认邮箱),只绑定手机
|
|
user = _make_user(
|
|
email="wx_123@wechat.local",
|
|
phone="13800138000",
|
|
phone_verified=True,
|
|
)
|
|
mock_user_repo.find_by_id.return_value = user
|
|
mock_user_repo.find_by_phone.return_value = None
|
|
mock_verification_service.verify.return_value = (True, None)
|
|
|
|
uc = BindContactUseCase(
|
|
user_repository=mock_user_repo,
|
|
verification_code_service=mock_verification_service,
|
|
)
|
|
req = BindContactRequest(user_id="user-1", phone="13800138000", phone_code="123456")
|
|
resp, _ = uc.execute(req)
|
|
|
|
# 只有手机验证通过,但邮箱还是wechat.local,不算绑定完成
|
|
assert resp.user.binding_completed_at is None
|
|
|
|
def test_binding_complete_with_both_verified(self, mock_user_repo, mock_verification_service):
|
|
"""手机+真实邮箱都验证通过后,标记绑定完成."""
|
|
user = _make_user(
|
|
email="wx_123@wechat.local",
|
|
phone="13800138000",
|
|
phone_verified=True,
|
|
)
|
|
mock_user_repo.find_by_id.return_value = user
|
|
mock_user_repo.find_by_phone.return_value = None
|
|
mock_user_repo.find_by_email.return_value = None
|
|
mock_verification_service.verify.return_value = (True, None)
|
|
|
|
uc = BindContactUseCase(
|
|
user_repository=mock_user_repo,
|
|
verification_code_service=mock_verification_service,
|
|
)
|
|
req = BindContactRequest(
|
|
user_id="user-1",
|
|
phone="13800138000",
|
|
phone_code="123456",
|
|
email="real@e.com",
|
|
email_code="abcdef",
|
|
)
|
|
resp, _ = uc.execute(req)
|
|
|
|
assert resp.user.binding_completed_at is not None
|
|
assert resp.user.email == "real@e.com"
|
|
|
|
|
|
# ── BindContactUseCase 通用/边界测试 ──────────────────────────────────────
|
|
|
|
|
|
class TestBindContactGeneral:
|
|
"""绑定通用/边界测试"""
|
|
|
|
def test_no_phone_no_email_error(self, mock_user_repo, mock_verification_service):
|
|
"""手机和邮箱都没填."""
|
|
user = _make_user()
|
|
mock_user_repo.find_by_id.return_value = user
|
|
|
|
uc = BindContactUseCase(
|
|
user_repository=mock_user_repo,
|
|
verification_code_service=mock_verification_service,
|
|
)
|
|
req = BindContactRequest(user_id="user-1")
|
|
resp, error = uc.execute(req)
|
|
|
|
assert resp is None
|
|
assert "至少填写" in error
|
|
|
|
def test_user_not_found(self, mock_user_repo, mock_verification_service):
|
|
"""用户不存在."""
|
|
mock_user_repo.find_by_id.return_value = None
|
|
|
|
uc = BindContactUseCase(
|
|
user_repository=mock_user_repo,
|
|
verification_code_service=mock_verification_service,
|
|
)
|
|
req = BindContactRequest(user_id="nonexistent", phone="13800138000")
|
|
resp, error = uc.execute(req)
|
|
|
|
assert resp is None
|
|
assert "用户不存在" in error
|
|
|
|
def test_exception_returns_error(self, mock_user_repo, mock_verification_service):
|
|
"""异常情况返回错误."""
|
|
mock_user_repo.find_by_id.side_effect = Exception("DB error")
|
|
|
|
uc = BindContactUseCase(
|
|
user_repository=mock_user_repo,
|
|
verification_code_service=mock_verification_service,
|
|
)
|
|
req = BindContactRequest(user_id="u1", phone="13800138000")
|
|
resp, error = uc.execute(req)
|
|
|
|
assert resp is None
|
|
assert "失败" in error
|
|
|
|
|
|
# ── SendVerificationCodeRequest 测试 ───────────────────────────────────────
|
|
|
|
|
|
class TestSendVerificationCodeRequest:
|
|
"""发送验证码请求测试"""
|
|
|
|
def test_value_stripped(self):
|
|
"""value 去除前后空格."""
|
|
req = SendVerificationCodeRequest(target="email", value=" u@e.com ", purpose="bind")
|
|
assert req.value == "u@e.com"
|
|
|
|
def test_target_purpose_preserved(self):
|
|
"""target 和 purpose 保持不变."""
|
|
req = SendVerificationCodeRequest(target="phone", value="13800138000", purpose="login")
|
|
assert req.target == "phone"
|
|
assert req.purpose == "login"
|
|
|
|
|
|
# ── SendVerificationCodeResponse 测试 ──────────────────────────────────────
|
|
|
|
|
|
class TestSendVerificationCodeResponse:
|
|
"""发送验证码响应测试"""
|
|
|
|
def test_to_dict_structure(self):
|
|
"""to_dict 返回正确结构."""
|
|
resp = SendVerificationCodeResponse(expires_in=300, resend_after=60)
|
|
d = resp.to_dict()
|
|
assert d["expires_in"] == 300
|
|
assert d["resend_after"] == 60
|
|
|
|
|
|
# ── SendVerificationCodeUseCase 测试 ───────────────────────────────────────
|
|
|
|
|
|
class TestSendVerificationCode:
|
|
"""发送验证码用例测试"""
|
|
|
|
def test_send_phone_code_success(self, mock_verification_service, mock_sms_service):
|
|
"""成功发送手机验证码."""
|
|
from packages.domain.verification_code import VerificationCode
|
|
|
|
code_obj = VerificationCode.create("13800138000", "phone_bind", ttl_seconds=300)
|
|
mock_verification_service.generate.return_value = (code_obj, None)
|
|
|
|
uc = SendVerificationCodeUseCase(
|
|
verification_code_service=mock_verification_service,
|
|
sms_service=mock_sms_service,
|
|
)
|
|
req = SendVerificationCodeRequest(target="phone", value="13800138000", purpose="bind")
|
|
resp, error = uc.execute(req)
|
|
|
|
assert error is None
|
|
assert resp is not None
|
|
assert resp.expires_in == 300
|
|
assert resp.resend_after == 60
|
|
mock_verification_service.generate.assert_called_once_with("13800138000", "phone_bind")
|
|
mock_sms_service.send_verification_code.assert_called_once()
|
|
|
|
def test_send_email_code_success(self, mock_verification_service, mock_email_service):
|
|
"""成功发送邮箱验证码."""
|
|
code_obj = VerificationCode.create("u@e.com", "email_bind", ttl_seconds=300)
|
|
mock_verification_service.generate.return_value = (code_obj, None)
|
|
|
|
uc = SendVerificationCodeUseCase(
|
|
verification_code_service=mock_verification_service,
|
|
email_service=mock_email_service,
|
|
)
|
|
req = SendVerificationCodeRequest(target="email", value="U@E.COM", purpose="bind")
|
|
resp, error = uc.execute(req)
|
|
|
|
assert error is None
|
|
# 邮箱被小写化
|
|
mock_verification_service.generate.assert_called_once_with("u@e.com", "email_bind")
|
|
mock_email_service.send_email.assert_called_once()
|
|
# 邮件内容包含验证码
|
|
call_args = mock_email_service.send_email.call_args[0]
|
|
assert code_obj.code in call_args[2] # body
|
|
|
|
def test_send_invalid_target(self, mock_verification_service):
|
|
"""不支持的目标类型."""
|
|
uc = SendVerificationCodeUseCase(
|
|
verification_code_service=mock_verification_service,
|
|
)
|
|
req = SendVerificationCodeRequest(target="wechat", value="xxx", purpose="bind")
|
|
resp, error = uc.execute(req)
|
|
|
|
assert resp is None
|
|
assert "不支持的目标类型" in error
|
|
|
|
def test_send_invalid_phone_format(self, mock_verification_service):
|
|
"""手机号格式错误."""
|
|
uc = SendVerificationCodeUseCase(
|
|
verification_code_service=mock_verification_service,
|
|
)
|
|
req = SendVerificationCodeRequest(target="phone", value="123", purpose="bind")
|
|
resp, error = uc.execute(req)
|
|
|
|
assert resp is None
|
|
assert "格式" in error or "不正确" in error
|
|
|
|
def test_send_invalid_email_format(self, mock_verification_service):
|
|
"""邮箱格式错误."""
|
|
uc = SendVerificationCodeUseCase(
|
|
verification_code_service=mock_verification_service,
|
|
)
|
|
req = SendVerificationCodeRequest(target="email", value="invalid", purpose="bind")
|
|
resp, error = uc.execute(req)
|
|
|
|
assert resp is None
|
|
assert "格式" in error or "不正确" in error
|
|
|
|
def test_send_code_generation_fails(self, mock_verification_service):
|
|
"""验证码生成失败(如频控)."""
|
|
mock_verification_service.generate.return_value = (None, "发送太频繁")
|
|
|
|
uc = SendVerificationCodeUseCase(
|
|
verification_code_service=mock_verification_service,
|
|
)
|
|
req = SendVerificationCodeRequest(target="email", value="u@e.com", purpose="bind")
|
|
resp, error = uc.execute(req)
|
|
|
|
assert resp is None
|
|
assert "发送太频繁" in error
|
|
|
|
def test_send_phone_normalizes_number(self, mock_verification_service, mock_sms_service):
|
|
"""手机号发送时被标准化."""
|
|
code_obj = VerificationCode.create("13800138000", "phone_bind", ttl_seconds=300)
|
|
mock_verification_service.generate.return_value = (code_obj, None)
|
|
|
|
uc = SendVerificationCodeUseCase(
|
|
verification_code_service=mock_verification_service,
|
|
sms_service=mock_sms_service,
|
|
)
|
|
req = SendVerificationCodeRequest(target="phone", value="+8613800138000", purpose="bind")
|
|
uc.execute(req)
|
|
|
|
# generate 收到的是标准化后的号码
|
|
call_args = mock_verification_service.generate.call_args[0]
|
|
assert call_args[0] == "13800138000"
|
|
|
|
def test_send_without_sms_service_still_generates(self, mock_verification_service):
|
|
"""没有短信服务时仍然生成验证码(但不发送)."""
|
|
code_obj = VerificationCode.create("13800138000", "phone_bind", ttl_seconds=300)
|
|
mock_verification_service.generate.return_value = (code_obj, None)
|
|
|
|
uc = SendVerificationCodeUseCase(
|
|
verification_code_service=mock_verification_service,
|
|
sms_service=None,
|
|
)
|
|
req = SendVerificationCodeRequest(target="phone", value="13800138000", purpose="bind")
|
|
resp, error = uc.execute(req)
|
|
|
|
assert error is None
|
|
assert resp is not None
|
|
|
|
def test_send_different_purposes(self, mock_verification_service, mock_email_service):
|
|
"""不同 purpose 对应不同的 code_type."""
|
|
code_obj = VerificationCode.create("u@e.com", "email_login", ttl_seconds=300)
|
|
mock_verification_service.generate.return_value = (code_obj, None)
|
|
|
|
uc = SendVerificationCodeUseCase(
|
|
verification_code_service=mock_verification_service,
|
|
email_service=mock_email_service,
|
|
)
|
|
|
|
for purpose, expected_type in [
|
|
("bind", "email_bind"),
|
|
("login", "email_login"),
|
|
("reset_password", "email_reset_password"),
|
|
]:
|
|
mock_verification_service.reset_mock()
|
|
code_obj = VerificationCode.create("u@e.com", expected_type, ttl_seconds=300)
|
|
mock_verification_service.generate.return_value = (code_obj, None)
|
|
|
|
req = SendVerificationCodeRequest(target="email", value="u@e.com", purpose=purpose)
|
|
resp, _ = uc.execute(req)
|
|
assert resp is not None
|
|
call_args = mock_verification_service.generate.call_args[0]
|
|
assert call_args[1] == expected_type
|
|
|
|
def test_send_exception_returns_error(self, mock_verification_service):
|
|
"""异常情况返回错误."""
|
|
mock_verification_service.generate.side_effect = Exception("Service down")
|
|
|
|
uc = SendVerificationCodeUseCase(
|
|
verification_code_service=mock_verification_service,
|
|
)
|
|
req = SendVerificationCodeRequest(target="email", value="u@e.com", purpose="bind")
|
|
resp, error = uc.execute(req)
|
|
|
|
assert resp is None
|
|
assert "失败" in error
|