7e505a04c7
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / PR Build API Image (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (push) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
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 / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
361 lines
12 KiB
Python
Executable File
361 lines
12 KiB
Python
Executable File
"""微信同步登录 UseCase 单元测试."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from packages.application.auth.wechat_sync_use_case import (
|
|
WechatSyncRequest,
|
|
WechatSyncResponse,
|
|
WechatSyncUseCase,
|
|
)
|
|
from packages.domain.entities import User
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_user_repo():
|
|
return MagicMock()
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_session_store():
|
|
return MagicMock()
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_user():
|
|
user = User(
|
|
id="user_001",
|
|
email="test@wechat.local",
|
|
username="wx_test123",
|
|
display_name="微信用户",
|
|
password_hash="hashed",
|
|
email_verified=True,
|
|
)
|
|
user.wechat_openid = "openid_123"
|
|
user.wechat_unionid = "unionid_456"
|
|
user.last_login_at = None
|
|
user.last_login_ip = None
|
|
return user
|
|
|
|
|
|
class TestWechatSyncRequest:
|
|
"""WechatSyncRequest 测试"""
|
|
|
|
def test_openid_stripped(self):
|
|
"""openid 被 strip"""
|
|
req = WechatSyncRequest(openid=" openid_123 ")
|
|
assert req.openid == "openid_123"
|
|
|
|
def test_unionid_stripped(self):
|
|
"""unionid 被 strip"""
|
|
req = WechatSyncRequest(openid="o1", unionid=" unionid_456 ")
|
|
assert req.unionid == "unionid_456"
|
|
|
|
def test_default_nickname(self):
|
|
"""默认昵称"""
|
|
req = WechatSyncRequest(openid="o1")
|
|
assert req.nickname == "微信用户"
|
|
|
|
def test_default_source(self):
|
|
"""默认来源"""
|
|
req = WechatSyncRequest(openid="o1")
|
|
assert req.source == "miniapp"
|
|
|
|
def test_empty_unionid(self):
|
|
"""不传 unionid 默认为空字符串"""
|
|
req = WechatSyncRequest(openid="o1")
|
|
assert req.unionid == ""
|
|
|
|
|
|
class TestWechatSyncResponse:
|
|
"""WechatSyncResponse 测试"""
|
|
|
|
def test_to_dict_contains_fields(self):
|
|
"""to_dict 包含所有必要字段"""
|
|
resp = WechatSyncResponse(
|
|
access_token="access_123",
|
|
refresh_token="refresh_456",
|
|
user_id="user_001",
|
|
nickname="测试用户",
|
|
avatar_url="https://example.com/avatar.jpg",
|
|
is_new_user=False,
|
|
expires_in=1800,
|
|
)
|
|
data = resp.to_dict()
|
|
|
|
assert data["access_token"] == "access_123"
|
|
assert data["token"] == "access_123" # 兼容字段
|
|
assert data["refresh_token"] == "refresh_456"
|
|
assert data["user_id"] == "user_001"
|
|
assert data["is_new_user"] is False
|
|
assert data["expires_in"] == 1800
|
|
assert "user" in data
|
|
assert "user_info" in data
|
|
assert data["user"]["id"] == "user_001"
|
|
assert data["user"]["nickname"] == "测试用户"
|
|
assert data["user"]["display_name"] == "测试用户"
|
|
|
|
|
|
class TestWechatSyncUseCaseLoginExisting:
|
|
"""已有用户登录测试"""
|
|
|
|
def test_login_by_openid(self, mock_user_repo, mock_session_store, sample_user):
|
|
"""通过 openid 登录已有用户"""
|
|
mock_user_repo.find_by_wechat_openid.return_value = sample_user
|
|
mock_user_repo.find_by_wechat_unionid.return_value = None
|
|
mock_user_repo.save.return_value = sample_user
|
|
|
|
use_case = WechatSyncUseCase(
|
|
mock_user_repo,
|
|
session_store=mock_session_store,
|
|
jwt_secret_key="test-secret-key-for-jwt-12345",
|
|
)
|
|
request = WechatSyncRequest(openid="openid_123", nickname="测试")
|
|
response, error = use_case.execute(request)
|
|
|
|
assert error is None
|
|
assert response is not None
|
|
assert response.user_id == "user_001"
|
|
assert response.is_new_user is False
|
|
mock_user_repo.find_by_wechat_openid.assert_called_once_with("openid_123")
|
|
mock_session_store.save_session.assert_called_once()
|
|
|
|
def test_login_by_unionid(self, mock_user_repo, mock_session_store, sample_user):
|
|
"""openid 没找到,通过 unionid 找到并绑定 openid"""
|
|
sample_user.wechat_openid = None # 没有当前 openid
|
|
mock_user_repo.find_by_wechat_openid.return_value = None
|
|
mock_user_repo.find_by_wechat_unionid.return_value = sample_user
|
|
mock_user_repo.save.return_value = sample_user
|
|
|
|
use_case = WechatSyncUseCase(
|
|
mock_user_repo,
|
|
session_store=mock_session_store,
|
|
jwt_secret_key="test-secret-key-for-jwt-12345",
|
|
)
|
|
request = WechatSyncRequest(
|
|
openid="new_openid",
|
|
unionid="unionid_456",
|
|
nickname="测试",
|
|
)
|
|
response, error = use_case.execute(request)
|
|
|
|
assert error is None
|
|
assert response is not None
|
|
assert response.is_new_user is False
|
|
# 应该保存了新的 openid
|
|
assert sample_user.wechat_openid == "new_openid"
|
|
mock_user_repo.save.assert_called()
|
|
|
|
def test_updates_last_login(self, mock_user_repo, mock_session_store, sample_user):
|
|
"""登录时更新最后登录信息"""
|
|
mock_user_repo.find_by_wechat_openid.return_value = sample_user
|
|
mock_user_repo.save.return_value = sample_user
|
|
|
|
use_case = WechatSyncUseCase(
|
|
mock_user_repo,
|
|
session_store=mock_session_store,
|
|
jwt_secret_key="test-secret-key-for-jwt-12345",
|
|
)
|
|
request = WechatSyncRequest(openid="openid_123")
|
|
use_case.execute(request)
|
|
|
|
assert sample_user.last_login_at is not None
|
|
assert sample_user.last_login_ip == "bff_gateway"
|
|
|
|
def test_returns_tokens(self, mock_user_repo, mock_session_store, sample_user):
|
|
"""返回 access_token 和 refresh_token"""
|
|
mock_user_repo.find_by_wechat_openid.return_value = sample_user
|
|
mock_user_repo.save.return_value = sample_user
|
|
|
|
use_case = WechatSyncUseCase(
|
|
mock_user_repo,
|
|
session_store=mock_session_store,
|
|
jwt_secret_key="test-secret-key-for-jwt-12345",
|
|
)
|
|
request = WechatSyncRequest(openid="openid_123")
|
|
response, _ = use_case.execute(request)
|
|
|
|
assert response.access_token is not None
|
|
assert len(response.access_token) > 0
|
|
assert response.refresh_token is not None
|
|
assert len(response.refresh_token) > 0
|
|
assert response.expires_in > 0
|
|
|
|
|
|
class TestWechatSyncUseCaseNewUser:
|
|
"""新用户注册测试"""
|
|
|
|
def test_create_new_user(self, mock_user_repo, mock_session_store):
|
|
"""openid 和 unionid 都没找到,创建新用户"""
|
|
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 # username 不重复
|
|
|
|
saved_user = None
|
|
|
|
def capture_save(user):
|
|
nonlocal saved_user
|
|
saved_user = user
|
|
|
|
mock_user_repo.save.side_effect = capture_save
|
|
|
|
use_case = WechatSyncUseCase(
|
|
mock_user_repo,
|
|
session_store=mock_session_store,
|
|
jwt_secret_key="test-secret-key-for-jwt-12345",
|
|
)
|
|
request = WechatSyncRequest(
|
|
openid="new_openid_789",
|
|
unionid="new_union_789",
|
|
nickname="新用户",
|
|
avatar_url="https://example.com/avatar.jpg",
|
|
)
|
|
response, error = use_case.execute(request)
|
|
|
|
assert error is None
|
|
assert response is not None
|
|
assert response.is_new_user is True
|
|
assert saved_user is not None
|
|
assert saved_user.wechat_openid == "new_openid_789"
|
|
assert saved_user.wechat_unionid == "new_union_789"
|
|
assert saved_user.email.endswith("@wechat.local")
|
|
assert saved_user.username.startswith("wx_")
|
|
assert saved_user.email_verified is True
|
|
|
|
def test_new_user_email_based_on_openid(self, mock_user_repo, mock_session_store):
|
|
"""新用户邮箱基于 openid 生成"""
|
|
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
|
|
|
|
saved_user = None
|
|
|
|
def capture_save(user):
|
|
nonlocal saved_user
|
|
saved_user = user
|
|
|
|
mock_user_repo.save.side_effect = capture_save
|
|
|
|
use_case = WechatSyncUseCase(
|
|
mock_user_repo,
|
|
session_store=mock_session_store,
|
|
jwt_secret_key="test-secret-key-for-jwt-12345",
|
|
)
|
|
request = WechatSyncRequest(openid="abcdef1234567890")
|
|
use_case.execute(request)
|
|
|
|
assert "abcdef1234567890" in saved_user.email or "abcdef1234567890"[:20] in saved_user.email
|
|
assert saved_user.email.endswith("@wechat.local")
|
|
|
|
def test_username_conflict_adds_suffix(self, mock_user_repo, mock_session_store):
|
|
"""用户名冲突时加后缀"""
|
|
call_count = [0]
|
|
|
|
def mock_find_by_username(username):
|
|
# 前两次返回存在(模拟冲突),第三次返回 None(可用)
|
|
call_count[0] += 1
|
|
if call_count[0] <= 2:
|
|
return MagicMock()
|
|
return None
|
|
|
|
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.side_effect = mock_find_by_username
|
|
|
|
use_case = WechatSyncUseCase(
|
|
mock_user_repo,
|
|
session_store=mock_session_store,
|
|
jwt_secret_key="test-secret-key-for-jwt-12345",
|
|
)
|
|
request = WechatSyncRequest(openid="test_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
|
|
|
|
def test_new_user_has_password_hash(self, 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
|
|
|
|
saved_user = None
|
|
|
|
def capture_save(user):
|
|
nonlocal saved_user
|
|
saved_user = user
|
|
|
|
mock_user_repo.save.side_effect = capture_save
|
|
|
|
use_case = WechatSyncUseCase(
|
|
mock_user_repo,
|
|
session_store=mock_session_store,
|
|
jwt_secret_key="test-secret-key-for-jwt-12345",
|
|
)
|
|
request = WechatSyncRequest(openid="new_openid")
|
|
use_case.execute(request)
|
|
|
|
assert saved_user.password_hash is not None
|
|
assert len(saved_user.password_hash) > 0
|
|
|
|
|
|
class TestWechatSyncUseCaseErrors:
|
|
"""错误场景测试"""
|
|
|
|
def test_empty_openid(self, mock_user_repo, mock_session_store):
|
|
"""空 openid 返回错误"""
|
|
use_case = WechatSyncUseCase(
|
|
mock_user_repo,
|
|
session_store=mock_session_store,
|
|
jwt_secret_key="test-secret-key-for-jwt-12345",
|
|
)
|
|
request = WechatSyncRequest(openid="")
|
|
response, error = use_case.execute(request)
|
|
|
|
assert response is None
|
|
assert "openid is required" in error
|
|
|
|
def test_exception_returns_error(self, mock_user_repo, mock_session_store):
|
|
"""异常时返回友好错误"""
|
|
mock_user_repo.find_by_wechat_openid.side_effect = Exception("DB error")
|
|
|
|
use_case = WechatSyncUseCase(
|
|
mock_user_repo,
|
|
session_store=mock_session_store,
|
|
jwt_secret_key="test-secret-key-for-jwt-12345",
|
|
)
|
|
request = WechatSyncRequest(openid="openid_123")
|
|
response, error = use_case.execute(request)
|
|
|
|
assert response is None
|
|
assert "Internal error" in error
|
|
|
|
|
|
class TestWechatSyncSession:
|
|
"""Session 相关测试"""
|
|
|
|
def test_session_saved(self, mock_user_repo, mock_session_store, sample_user):
|
|
"""登录时保存 session"""
|
|
mock_user_repo.find_by_wechat_openid.return_value = sample_user
|
|
mock_user_repo.save.return_value = sample_user
|
|
|
|
use_case = WechatSyncUseCase(
|
|
mock_user_repo,
|
|
session_store=mock_session_store,
|
|
jwt_secret_key="test-secret-key-for-jwt-12345",
|
|
)
|
|
request = WechatSyncRequest(openid="openid_123", source="miniapp")
|
|
use_case.execute(request)
|
|
|
|
mock_session_store.save_session.assert_called_once()
|
|
call_kwargs = mock_session_store.save_session.call_args[1]
|
|
assert call_kwargs["user_id"] == "user_001"
|
|
assert "wechat_miniapp" in call_kwargs["device_info"]
|
|
assert call_kwargs["expires_in_seconds"] == 30 * 24 * 3600
|