""" 微信同步登录/注册 Use Case 测试 """ from datetime import datetime, timezone from unittest.mock import Mock, patch import pytest from packages.application.auth.wechat_sync_use_case import ( WechatSyncRequest, WechatSyncUseCase, ) from packages.domain.entities import User class TestWechatSyncRequest: """微信同步请求对象测试""" def test_request_with_basic_fields(self): """测试基本字段初始化""" request = WechatSyncRequest(openid="openid123") assert request.openid == "openid123" assert request.unionid == "" assert request.nickname == "微信用户" assert request.avatar_url == "" assert request.source == "miniapp" def test_request_with_all_fields(self): """测试完整字段初始化""" request = WechatSyncRequest( openid=" openid123 ", unionid=" unionid456 ", nickname="测试用户", avatar_url="http://example.com/avatar.jpg", source="h5", ) assert request.openid == "openid123" # stripped assert request.unionid == "unionid456" # stripped assert request.nickname == "测试用户" assert request.avatar_url == "http://example.com/avatar.jpg" assert request.source == "h5" def test_request_empty_unionid_stays_empty(self): """测试空 unionid 处理""" request = WechatSyncRequest(openid="openid123", unionid="") assert request.unionid == "" def test_request_none_nickname_defaults(self): """测试空昵称使用默认值""" request = WechatSyncRequest(openid="openid123", nickname="") assert request.nickname == "微信用户" class TestWechatSyncUseCase: """微信同步登录/注册用例测试""" @pytest.fixture def mock_user_repo(self): """Mock 用户仓储""" repo = Mock() repo.find_by_wechat_openid = Mock(return_value=None) repo.find_by_wechat_unionid = Mock(return_value=None) repo.find_by_username = Mock(return_value=None) repo.find_by_email = Mock(return_value=None) repo.save = Mock() repo.get = Mock(return_value=None) return repo @pytest.fixture def mock_session_store(self): """Mock Session 存储""" store = Mock() store.save_session = Mock(return_value=True) store.get_refresh_token = Mock(return_value=None) store.get_session_by_refresh_token = Mock(return_value=None) store.delete_session = Mock(return_value=True) return store @pytest.fixture def test_user(self): """测试用户""" return User( id="user-123", email="test@example.com", username="testuser", display_name="测试用户", password_hash="hashed_password", wechat_openid="openid123", wechat_unionid="unionid456", ) @pytest.fixture def use_case(self, mock_user_repo, mock_session_store): """创建微信同步用例""" return WechatSyncUseCase( user_repository=mock_user_repo, session_store=mock_session_store, jwt_secret_key="test-secret-key-for-unit-tests", ) # ===== 登录场景:openid 找到用户 ===== def test_login_by_openid_success(self, use_case, mock_user_repo, mock_session_store, test_user): """测试通过 openid 登录成功""" mock_user_repo.find_by_wechat_openid.return_value = test_user request = WechatSyncRequest(openid="openid123") response, error = use_case.execute(request) assert error is None assert response is not None assert response.user_id == "user-123" assert response.nickname == "测试用户" assert response.is_new_user is False assert response.access_token != "" assert response.refresh_token != "" assert response.expires_in > 0 # 验证 session 已保存 mock_session_store.save_session.assert_called_once() save_kwargs = mock_session_store.save_session.call_args.kwargs assert save_kwargs["user_id"] == "user-123" assert "wechat_miniapp" in save_kwargs["device_info"] # 验证更新了最后登录信息 mock_user_repo.save.assert_called_once() saved_user = mock_user_repo.save.call_args[0][0] assert saved_user.last_login_at is not None assert saved_user.last_login_ip == "bff_gateway" # 验证 to_dict 包含兼容字段 data = response.to_dict() assert data["access_token"] == response.access_token assert data["token"] == response.access_token # 兼容字段 assert data["user"]["id"] == "user-123" assert data["user_info"]["id"] == "user-123" # ===== 登录场景:openid 没找到,通过 unionid 找到 ===== def test_login_by_unionid_binds_openid(self, use_case, mock_user_repo, mock_session_store, test_user): """测试通过 unionid 找到用户并绑定当前 openid""" # openid 没找到 mock_user_repo.find_by_wechat_openid.return_value = None # unionid 找到了(但 openid 字段为空) test_user.wechat_openid = None mock_user_repo.find_by_wechat_unionid.return_value = test_user request = WechatSyncRequest( openid="new_openid_789", unionid="unionid456", ) response, error = use_case.execute(request) assert error is None assert response is not None assert response.is_new_user is False assert response.user_id == "user-123" # 验证绑定了新的 openid(save 被调用了两次:一次绑定 openid,一次更新登录信息) assert mock_user_repo.save.call_count == 2 # 第一次 save 应该是绑定 openid first_save_user = mock_user_repo.save.call_args_list[0][0][0] assert first_save_user.wechat_openid == "new_openid_789" def test_login_by_unionid_no_binding_needed(self, use_case, mock_user_repo, mock_session_store, test_user): """测试通过 unionid 找到用户且 openid 已存在时(不需要额外绑定)""" mock_user_repo.find_by_wechat_openid.return_value = None mock_user_repo.find_by_wechat_unionid.return_value = test_user request = WechatSyncRequest( openid="openid123", # 跟用户已有的一样 unionid="unionid456", ) response, error = use_case.execute(request) assert error is None assert response is not None assert response.is_new_user is False # 还是会 save(绑定)+ save(更新登录信息)= 2次 assert mock_user_repo.save.call_count == 2 # ===== 注册场景:openid 和 unionid 都没找到,创建新用户 ===== def test_register_new_user(self, use_case, mock_user_repo, mock_session_store): """测试创建新微信用户""" mock_user_repo.find_by_wechat_openid.return_value = None mock_user_repo.find_by_wechat_unionid.return_value = None mock_user_repo.find_by_username.return_value = None request = WechatSyncRequest( openid="new_openid", unionid="new_unionid", nickname="新用户", avatar_url="http://example.com/avatar.jpg", source="miniapp", ) response, error = use_case.execute(request) assert error is None assert response is not None assert response.is_new_user is True assert response.nickname == "新用户" assert response.access_token != "" assert response.refresh_token != "" # 验证用户被创建并保存 assert mock_user_repo.save.call_count >= 1 # 找到 save 的用户(可能有多次save,找第一次即创建用户的那次) created_user = None for call in mock_user_repo.save.call_args_list: user = call[0][0] if user.wechat_openid == "new_openid": created_user = user break assert created_user is not None assert created_user.wechat_openid == "new_openid" assert created_user.wechat_unionid == "new_unionid" assert created_user.email_verified is True assert created_user.username.startswith("wx_") assert "@wechat.local" in created_user.email def test_register_new_user_without_unionid(self, use_case, mock_user_repo, mock_session_store): """测试创建无 unionid 的新用户""" mock_user_repo.find_by_wechat_openid.return_value = None mock_user_repo.find_by_username.return_value = None request = WechatSyncRequest(openid="openid_no_union") response, error = use_case.execute(request) assert error is None assert response is not None assert response.is_new_user is True created_user = mock_user_repo.save.call_args_list[0][0][0] assert created_user.wechat_unionid is None def test_register_username_conflict_adds_suffix(self, use_case, mock_user_repo, mock_session_store): """测试用户名冲突时自动加后缀""" mock_user_repo.find_by_wechat_openid.return_value = None mock_user_repo.find_by_wechat_unionid.return_value = None # 第一次 find_by_username 返回存在(冲突),第二次返回 None(生成了带后缀的新名) mock_user_repo.find_by_username.side_effect = [Mock(), None] request = WechatSyncRequest(openid="conflict_openid") response, error = use_case.execute(request) assert error is None assert response is not None assert response.is_new_user is True # find_by_username 应该被调用了两次 assert mock_user_repo.find_by_username.call_count == 2 # 第二个用户名应该带后缀 _1 second_call_username = mock_user_repo.find_by_username.call_args_list[1][0][0] assert "_1" in second_call_username def test_register_default_nickname_when_empty(self, use_case, mock_user_repo, mock_session_store): """测试新用户空昵称时使用默认值""" mock_user_repo.find_by_wechat_openid.return_value = None mock_user_repo.find_by_username.return_value = None request = WechatSyncRequest(openid="openid123", nickname="") response, error = use_case.execute(request) assert error is None assert response is not None assert response.nickname == "微信用户" # ===== 错误场景 ===== def test_missing_openid(self, use_case): """测试缺少 openid""" request = WechatSyncRequest(openid="") response, error = use_case.execute(request) assert response is None assert error == "openid is required" def test_exception_handling(self, use_case, mock_user_repo): """测试异常处理""" mock_user_repo.find_by_wechat_openid.side_effect = Exception("DB error") request = WechatSyncRequest(openid="openid123") response, error = use_case.execute(request) assert response is None assert "Internal error" in error assert "DB error" in error # ===== Session 保存验证 ===== def test_session_saved_with_correct_params(self, use_case, mock_user_repo, mock_session_store, test_user): """测试 session 保存参数正确""" mock_user_repo.find_by_wechat_openid.return_value = test_user request = WechatSyncRequest(openid="openid123", source="h5") use_case.execute(request) mock_session_store.save_session.assert_called_once() kwargs = mock_session_store.save_session.call_args.kwargs assert kwargs["user_id"] == "user-123" assert kwargs["refresh_token"] != "" assert "wechat_h5" in kwargs["device_info"] assert kwargs["ip_address"] == "bff_gateway" assert kwargs["expires_in_seconds"] == 30 * 24 * 3600 # 30天