"""绑定联系方式 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