df38101bd9
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Production Web 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 / Build Staging Web Image (push) Successful in 40s
CI/CD Pipeline / Frontend Lint (push) Successful in 2m26s
CI/CD Pipeline / Validate Code Quality And Tests (push) Successful in 3m35s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m55s
CI/CD Pipeline / Unit Tests (push) Successful in 4m6s
CI/CD Pipeline / Integration Tests (push) Successful in 1m45s
CI/CD Pipeline / Build Staging API Image (push) Successful in 12m21s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 15m52s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
307 lines
12 KiB
Python
Executable File
307 lines
12 KiB
Python
Executable File
"""
|
|
微信同步登录/注册 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天
|