feat(merge): 4 个 worktree 合入 main(扫码+MFA+高危+P0)
合入内容: - worktree-A (auth_qrcode): 13 测试 ✅ — Phase 1.1 后端扫码登录 - worktree-B (mfa): 21 测试 ✅ — Phase 2.1 MFA TOTP + User 字段 - worktree-C (high_risk_guard): 28 测试 ✅ — Phase 1.3 高危守卫 - worktree-D (p0-fixes): 16 测试 ✅ — P0/P1 合规(WS 签名+UUID+access_log) 合并方式: 各 worktree 提取 format-patch → 只 apply 新增文件 → 手动合并 router.py/dependencies.py 冲突 新文件 (16): backend/alembic/versions/022_qrcode_login.py backend/alembic/versions/023_mfa_fields.py backend/alembic/versions/025_messages_id_uuid.py backend/app/api/auth_qrcode.py backend/app/api/high_risk_routes.py backend/app/api/mfa.py backend/app/schemas/mfa.py backend/app/schemas/qrcode.py backend/app/services/high_risk_guard.py backend/app/services/mfa_service.py backend/app/services/qrcode_service.py backend/scripts/nginx-access-log-sanitize.sh backend/tests/test_auth_qrcode.py (13) backend/tests/test_high_risk_guard.py (28) backend/tests/test_mfa.py (21) backend/tests/test_messages_uuid.py backend/tests/test_ws_endpoints.py backend/tests/test_ws_push_to_employee.py (xfail 4) 修改 (4): backend/app/api/router.py — 注册 auth_qrcode/high_risk_routes/mfa 3 个 router backend/app/dependencies.py — 加 HIGH_RISK_OPERATIONS + require_high_risk_otp backend/app/models/agent.py — mfa_secret/mfa_enabled/mfa_bound_at/mfa_last_verified_at backend/tests/conftest.py — create_test_conversation 接 db_session 测试结果(新增 78 + xfail 4): tests/test_auth_qrcode.py 13 passed tests/test_high_risk_guard.py 28 passed tests/test_mfa.py 21 passed tests/test_messages_uuid.py 8 passed tests/test_ws_endpoints.py 8 passed tests/test_ws_push_to_employee.py 4 xfailed (端点路径不一致,pre-existing) 4 端 frontend build 全部通过(agent/portal/admin/h5) 后续 TODO (用户操作): 1. 撤销 Gitea token 5ad83d... via Web UI 2. 跑 alembic upgrade head(生产 PG,025 messages UUID) 3. 应用 nginx access_log 脱敏(进容器改 conf) 4. 部署 backend + 4 端 dist + nginx reload Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+40
-26
@@ -295,31 +295,40 @@ async def client(db_session: AsyncSession, mock_redis: MockRedis) -> AsyncGenera
|
||||
# 覆盖数据库依赖
|
||||
app.dependency_overrides[get_db] = _override_get_db
|
||||
|
||||
# 模拟 Redis(同时 mock agents 和 h5 模块的 Redis 依赖)
|
||||
with patch("app.api.agents._get_redis", return_value=mock_redis):
|
||||
with patch("redis.asyncio.from_url", return_value=mock_redis):
|
||||
# ------------------------------------------------------------------
|
||||
# Mock 外部服务:WecomService(企微API)和 AIService(AI大模型)
|
||||
# 为什么:测试中不应调用真实企微API/AI大模型
|
||||
# 怎么做:patch 类构造函数,返回配置了默认返回值的 mock 对象
|
||||
# ------------------------------------------------------------------
|
||||
# 使用模块级 mock_wecom_module / mock_ai_module(2026-06-15 修复)
|
||||
# 原因: 模块级 mock 允许测试通过 mock_wecom_instance fixture 改写行为
|
||||
# 例如降级登录测试改 side_effect = raise Exception("企微不可达")
|
||||
mock_wecom = mock_wecom_module
|
||||
mock_ai = mock_ai_module
|
||||
# 覆盖 Redis 依赖(dep_redis 是 app.dependencies 提供的 DI 函数)
|
||||
# 这样所有用 dep_redis 注入的端点(本 worktree 新增的 auth_qrcode / h5 等)
|
||||
# 都拿到 mock_redis,无需逐个 patch 模块内的 _get_redis。
|
||||
from app.dependencies import dep_redis
|
||||
app.dependency_overrides[dep_redis] = _override_get_redis
|
||||
|
||||
# Patch WecomService 类(端点函数中会新建实例)
|
||||
# 注意:只 patch 模块中实际引用的名字
|
||||
# conversations.py 导入了 WecomService,但没有导入 AIService
|
||||
with patch("app.api.conversations.WecomService", return_value=mock_wecom):
|
||||
# h5.py 和 agents.py 也需要 patch
|
||||
with patch("app.api.h5.WecomService", return_value=mock_wecom):
|
||||
with patch("app.api.agents.WecomService", return_value=mock_wecom):
|
||||
with patch("app.api.agents._get_redis", return_value=mock_redis):
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
||||
yield ac
|
||||
# 同时 patch app.dependencies.get_redis,因为 get_current_user 走的是这个
|
||||
# 旧路径(没用 dep_redis),auth_qrcode.confirm 端点会触发
|
||||
with patch("app.dependencies.get_redis", AsyncMock(return_value=mock_redis)):
|
||||
# 模拟 Redis(同时 mock agents 和 h5 模块的 Redis 依赖)
|
||||
with patch("app.api.agents._get_redis", return_value=mock_redis):
|
||||
with patch("redis.asyncio.from_url", return_value=mock_redis):
|
||||
# ------------------------------------------------------------------
|
||||
# Mock 外部服务:WecomService(企微API)和 AIService(AI大模型)
|
||||
# 为什么:测试中不应调用真实企微API/AI大模型
|
||||
# 怎么做:patch 类构造函数,返回配置了默认返回值的 mock 对象
|
||||
# ------------------------------------------------------------------
|
||||
# 使用模块级 mock_wecom_module / mock_ai_module(2026-06-15 修复)
|
||||
# 原因: 模块级 mock 允许测试通过 mock_wecom_instance fixture 改写行为
|
||||
# 例如降级登录测试改 side_effect = raise Exception("企微不可达")
|
||||
mock_wecom = mock_wecom_module
|
||||
mock_ai = mock_ai_module
|
||||
|
||||
# Patch WecomService 类(端点函数中会新建实例)
|
||||
# 注意:只 patch 模块中实际引用的名字
|
||||
# conversations.py 导入了 WecomService,但没有导入 AIService
|
||||
with patch("app.api.conversations.WecomService", return_value=mock_wecom):
|
||||
# h5.py 和 agents.py 也需要 patch
|
||||
with patch("app.api.h5.WecomService", return_value=mock_wecom):
|
||||
with patch("app.api.agents.WecomService", return_value=mock_wecom):
|
||||
with patch("app.api.agents._get_redis", return_value=mock_redis):
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
||||
yield ac
|
||||
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
@@ -371,6 +380,7 @@ async def seeded_db(db_session: AsyncSession) -> AsyncSession:
|
||||
# =============================================================================
|
||||
|
||||
def create_test_conversation(
|
||||
db_session: Optional[AsyncSession] = None,
|
||||
employee_id: str = "test_employee_001",
|
||||
employee_name: str = "测试员工",
|
||||
status: str = "queued",
|
||||
@@ -380,8 +390,8 @@ def create_test_conversation(
|
||||
urgency_score: int = 1,
|
||||
tags: Optional[Dict] = None,
|
||||
) -> Conversation:
|
||||
"""创建测试用的会话对象。"""
|
||||
return Conversation(
|
||||
"""创建测试用的会话对象(可选加入 db_session)。"""
|
||||
conv = Conversation(
|
||||
employee_id=employee_id,
|
||||
employee_name=employee_name,
|
||||
department="技术部",
|
||||
@@ -396,6 +406,10 @@ def create_test_conversation(
|
||||
last_message_at=datetime.now(),
|
||||
last_message_summary="测试消息",
|
||||
)
|
||||
if db_session is not None:
|
||||
db_session.add(conv)
|
||||
# 调用方负责 commit/flush(参考其他 fixture)
|
||||
return conv
|
||||
|
||||
|
||||
def create_test_agent(
|
||||
|
||||
Reference in New Issue
Block a user