Files
wecom_it_smart_desk/backend/tests/test_approval_detect_intent.py
Simon bea288e414 feat: 2026-07-11 全量更新 - 代办集成+会议室预定+知识迭代修复+UI统一+Bug修复
== 已部署上线 (9项) ==
- 代办事项真实数据源集成 (企微审批API 8bug修复链)
- H5/坐席端 Logo样式统一+绿色背景
- 视频引导页修复 (localStorage key v2)
- 坐席端 v9 Vue版本修复 (ElMessage._context)
- 截图按钮 v10 修复 (getDisplayMedia user gesture)
- 扫码样式恢复+H5扫码登录跳转修复
- H5截图快捷键提示

== 代码完成待部署 (3项) ==
- 知识迭代3Bug修复 (#8 POST端点/#7 MERGE幂等/#6 过期检查)
- 会议室预定-小鱼易联终端 (40文件, 40/40测试通过)
- IT资产升级审批推送 (asset_service.py)

== 需求文档 (2项) ==
- 坐席端AI辅助消息框-PRD (4项新功能确认)
- 坐席端布局优化建议 v2.0 (7天计划)

== 新增文档 ==
- 日报-2026-07-11.md
- 知识迭代Bug修复报告-20260711.md
- 会议室预定-部署指南.md
- CHANGELOG.md 更新

== 测试 ==
- test_todo_integration.py: 40/40
- test_meetingroom.py: 40/40
- test_bugfix_ki_suggestions.py: 21/21
2026-07-11 23:13:10 +08:00

726 lines
29 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# =============================================================================
# IT智能服务台 — 审批意图检测(/approval/detect-intent)测试
# =============================================================================
# 测试覆盖:
# 1. 关键词预过滤逻辑(_keyword_prefilter 单元测试)
# 2. 降级兜底逻辑(_fallback_detect 单元测试)
# 3. 端点集成测试:
# a. 关键词未命中 → 直接返回 false(不调 Dify)
# b. 关键词命中 + Dify 高置信度 → is_approval_request=true
# c. 关键词命中 + Dify 低置信度 → is_approval_request=false
# d. Dify 调用失败 → 降级兜底
# e. Dify 未配置 → 降级兜底
# f. Dify 返回 is_approval=false → is_approval_request=false
# 4. /approval/keywords 端点测试
# 5. /approval/jump 端点测试
#
# Round 2 变更:所有端点已改用 success_response() 包装,
# 响应格式为 {code: 0, data: {...}, message: "success"}
# 测试断言已适配新格式:data["data"]["field"] 而非 data["field"]
# =============================================================================
import pytest
from unittest.mock import AsyncMock, patch
from app.api.approval import (
_keyword_prefilter,
_fallback_detect,
KEYWORD_TO_APPROVAL_TYPE,
APPROVAL_PREFILTER_KEYWORDS,
)
from app.config import settings
# =============================================================================
# 单元测试:_keyword_prefilter
# =============================================================================
class TestKeywordPrefilter:
"""测试关键词预过滤函数。"""
def test_keyword_hit_device(self):
"""包含设备相关关键词时返回 True。"""
assert _keyword_prefilter("我需要申请一台电脑") is True
def test_keyword_hit_vpn(self):
"""包含 VPN 关键词时返回 True(大小写不敏感)。"""
assert _keyword_prefilter("VPN 连不上了") is True
assert _keyword_prefilter("vpn 连不上了") is True
def test_keyword_hit_software(self):
"""包含软件相关关键词时返回 True。"""
assert _keyword_prefilter("想申请一个软件") is True
def test_keyword_hit_generic(self):
"""包含通用关键词'申请'时返回 True。"""
assert _keyword_prefilter("我想申请一个东西") is True
def test_keyword_miss_normal_message(self):
"""普通对话消息不包含审批关键词时返回 False。"""
assert _keyword_prefilter("你好,今天天气怎么样") is False
assert _keyword_prefilter("谢谢,问题解决了") is False
def test_keyword_miss_empty_string(self):
"""空字符串返回 False。"""
assert _keyword_prefilter("") is False
def test_keyword_miss_none(self):
"""None 返回 False。"""
assert _keyword_prefilter(None) is False # type: ignore[arg-type]
def test_keyword_case_insensitive(self):
"""关键词匹配大小写不敏感。"""
assert _keyword_prefilter("我的VPN坏了") is True
assert _keyword_prefilter("我的vpn坏了") is True
assert _keyword_prefilter("我的Vpn坏了") is True
def test_keyword_partial_match(self):
"""关键词是子串匹配('设备''设备申请'中也能命中)。"""
assert _keyword_prefilter("设备申请流程是什么") is True
def test_keyword_all_prefilter_keywords_work(self):
"""验证 APPROVAL_PREFILTER_KEYWORDS 中的每个关键词都能被命中。"""
for kw in APPROVAL_PREFILTER_KEYWORDS:
# 构造包含该关键词的文本
text = f"测试文本包含{kw}关键词"
assert _keyword_prefilter(text) is True, f"关键词 '{kw}' 未被命中"
# =============================================================================
# 单元测试:_fallback_detect
# =============================================================================
class TestFallbackDetect:
"""测试降级兜底检测函数。"""
def test_fallback_returns_true_for_keyword_text(self):
"""包含关键词的文本,兜底返回 is_approval_request=True。"""
is_approval, confidence, approval_type = _fallback_detect("我需要申请电脑")
assert is_approval is True
assert confidence == 0.6
def test_fallback_returns_correct_type_device(self):
"""设备相关关键词映射到'设备申请'"""
_, _, approval_type = _fallback_detect("我要申请电脑")
assert approval_type == "设备申请"
def test_fallback_returns_correct_type_vpn(self):
"""VPN 关键词映射到'账号权限申请'"""
_, _, approval_type = _fallback_detect("VPN连不上了")
assert approval_type == "账号权限申请"
def test_fallback_returns_correct_type_software(self):
"""软件关键词映射到'软件服务申请'"""
_, _, approval_type = _fallback_detect("想安装一个软件")
assert approval_type == "软件服务申请"
def test_fallback_returns_correct_type_disposal(self):
"""资产处置关键词映射到'资产处置申请'
注意:测试文本不能同时包含'电脑'(→设备申请)等排在'报废'前面的关键词,
否则 _fallback_detect 会因字典遍历顺序返回先匹配的类型。
"""
_, _, approval_type = _fallback_detect("这台机器要报废")
assert approval_type == "资产处置申请"
def test_fallback_returns_correct_type_office_supplies(self):
"""办公用品关键词映射到'办公用品申请'"""
_, _, approval_type = _fallback_detect("办公用品超额了")
assert approval_type == "办公用品申请"
def test_fallback_confidence_is_0_6(self):
"""兜底置信度固定为 0.6(低于阈值 0.7)。"""
_, confidence, _ = _fallback_detect("申请设备")
assert confidence == 0.6
def test_fallback_empty_text_returns_true_no_type(self):
"""空文本兜底仍返回 True(预过滤已通过),但 approval_type 为 None。"""
is_approval, confidence, approval_type = _fallback_detect("")
assert is_approval is True
assert confidence == 0.6
assert approval_type is None
def test_fallback_generic_keyword_no_type_mapping(self):
"""'申请'在预过滤列表但不在 KEYWORD_TO_APPROVAL_TYPE 中,兜底 approval_type 为 None。"""
# "申请" 在 APPROVAL_PREFILTER_KEYWORDS 中,但不在 KEYWORD_TO_APPROVAL_TYPE 中
is_approval, _, approval_type = _fallback_detect("申请")
assert is_approval is True
assert approval_type is None
def test_fallback_first_match_wins(self):
"""兜底遍历 KEYWORD_TO_APPROVAL_TYPE,命中第一个关键词即返回(字典有序)。"""
# 包含多个关键词的文本,应返回第一个匹配的类型
_, _, approval_type = _fallback_detect("设备和VPN都坏了")
# KEYWORD_TO_APPROVAL_TYPE 中"设备"排在"VPN"前面
assert approval_type is not None
assert approval_type in KEYWORD_TO_APPROVAL_TYPE.values()
# =============================================================================
# 集成测试:/approval/detect-intent 端点
# =============================================================================
# Round 2: 响应格式已改为 {code: 0, data: {...}, message: "success"}
# 所有断言通过 data["data"]["field"] 访问业务字段
# =============================================================================
class TestDetectApprovalIntentEndpoint:
"""测试 /approval/detect-intent API 端点。"""
@pytest.mark.asyncio
async def test_keyword_not_hit_returns_false_no_dify(self, client):
"""关键词未命中 → 直接返回 false,不调 Difysource=keyword_prefilter。"""
response = await client.post(
"/approval/detect-intent",
json={"text": "你好,今天天气怎么样"},
)
assert response.status_code == 200
data = response.json()
# 统一响应格式验证
assert data["code"] == 0
assert data["message"] == "success"
# 业务字段验证
inner = data["data"]
assert inner["is_approval_request"] is False
assert inner["confidence"] == 0.0
assert inner["approval_type"] is None
assert inner["source"] == "keyword_prefilter"
@pytest.mark.asyncio
async def test_keyword_not_hit_empty_text(self, client):
"""空文本 → 关键词未命中,返回 false。"""
response = await client.post(
"/approval/detect-intent",
json={"text": ""},
)
assert response.status_code == 200
data = response.json()
assert data["code"] == 0
inner = data["data"]
assert inner["is_approval_request"] is False
assert inner["source"] == "keyword_prefilter"
@pytest.mark.asyncio
async def test_dify_high_confidence_returns_true(self, client, monkeypatch):
"""关键词命中 + Dify 返回高置信度(≥0.7) → is_approval_request=true, source=dify。"""
# 设置 Dify 配置(模拟已配置)
monkeypatch.setattr(settings, "approval_dify_base_url", "http://test-dify")
monkeypatch.setattr(settings, "approval_dify_api_key", "test-api-key")
monkeypatch.setattr(settings, "approval_confidence_threshold", 0.7)
# Mock Dify 返回高置信度
mock_dify = AsyncMock(return_value={
"is_approval_request": True,
"confidence": 0.95,
"approval_type": "设备申请",
})
with patch("app.api.approval._call_dify_approval_intent", mock_dify):
response = await client.post(
"/approval/detect-intent",
json={"text": "我需要申请一台笔记本电脑"},
)
assert response.status_code == 200
data = response.json()
assert data["code"] == 0
assert data["message"] == "success"
inner = data["data"]
assert inner["is_approval_request"] is True
assert inner["confidence"] == 0.95
assert inner["approval_type"] == "设备申请"
assert inner["source"] == "dify"
# 验证 Dify 被调用了一次
mock_dify.assert_called_once()
@pytest.mark.asyncio
async def test_dify_confidence_at_threshold_returns_true(self, client, monkeypatch):
"""置信度恰好等于阈值(0.7) → is_approval_request=true>= 判断)。"""
monkeypatch.setattr(settings, "approval_dify_base_url", "http://test-dify")
monkeypatch.setattr(settings, "approval_dify_api_key", "test-api-key")
monkeypatch.setattr(settings, "approval_confidence_threshold", 0.7)
mock_dify = AsyncMock(return_value={
"is_approval_request": True,
"confidence": 0.7,
"approval_type": "账号权限申请",
})
with patch("app.api.approval._call_dify_approval_intent", mock_dify):
response = await client.post(
"/approval/detect-intent",
json={"text": "VPN 账号申请"},
)
data = response.json()
assert data["code"] == 0
inner = data["data"]
assert inner["is_approval_request"] is True
assert inner["confidence"] == 0.7
assert inner["source"] == "dify"
@pytest.mark.asyncio
async def test_dify_low_confidence_returns_false(self, client, monkeypatch):
"""关键词命中 + Dify 返回低置信度(<0.7) → is_approval_request=false, source=dify。"""
monkeypatch.setattr(settings, "approval_dify_base_url", "http://test-dify")
monkeypatch.setattr(settings, "approval_dify_api_key", "test-api-key")
monkeypatch.setattr(settings, "approval_confidence_threshold", 0.7)
mock_dify = AsyncMock(return_value={
"is_approval_request": True,
"confidence": 0.5,
"approval_type": "设备申请",
})
with patch("app.api.approval._call_dify_approval_intent", mock_dify):
response = await client.post(
"/approval/detect-intent",
json={"text": "我想申请一台电脑"},
)
assert response.status_code == 200
data = response.json()
assert data["code"] == 0
inner = data["data"]
assert inner["is_approval_request"] is False
assert inner["confidence"] == 0.5
assert inner["approval_type"] == "设备申请"
assert inner["source"] == "dify"
@pytest.mark.asyncio
async def test_dify_returns_is_approval_false(self, client, monkeypatch):
"""Dify 明确返回 is_approval_request=false → 端点返回 false。"""
monkeypatch.setattr(settings, "approval_dify_base_url", "http://test-dify")
monkeypatch.setattr(settings, "approval_dify_api_key", "test-api-key")
mock_dify = AsyncMock(return_value={
"is_approval_request": False,
"confidence": 0.3,
"approval_type": None,
})
with patch("app.api.approval._call_dify_approval_intent", mock_dify):
response = await client.post(
"/approval/detect-intent",
json={"text": "我的电脑卡了"},
)
data = response.json()
assert data["code"] == 0
inner = data["data"]
assert inner["is_approval_request"] is False
assert inner["confidence"] == 0.3
assert inner["approval_type"] is None
assert inner["source"] == "dify"
@pytest.mark.asyncio
async def test_dify_failure_fallback(self, client, monkeypatch):
"""关键词命中 + Dify 调用失败 → 降级兜底, source=fallback。"""
monkeypatch.setattr(settings, "approval_dify_base_url", "http://test-dify")
monkeypatch.setattr(settings, "approval_dify_api_key", "test-api-key")
monkeypatch.setattr(settings, "approval_confidence_threshold", 0.7)
# Mock Dify 抛出异常(模拟网络错误/超时)
mock_dify = AsyncMock(side_effect=Exception("Dify 服务不可达"))
with patch("app.api.approval._call_dify_approval_intent", mock_dify):
response = await client.post(
"/approval/detect-intent",
json={"text": "我需要申请一台电脑"},
)
assert response.status_code == 200
data = response.json()
assert data["code"] == 0
inner = data["data"]
# 兜底返回 is_approval_request=True, confidence=0.6
assert inner["is_approval_request"] is True
assert inner["confidence"] == 0.6
assert inner["approval_type"] == "设备申请"
assert inner["source"] == "fallback"
@pytest.mark.asyncio
async def test_dify_not_configured_fallback(self, client, monkeypatch):
"""Dify 未配置(base_url/api_key 为空)→ 降级兜底。"""
# 确保配置为空
monkeypatch.setattr(settings, "approval_dify_base_url", "")
monkeypatch.setattr(settings, "approval_dify_api_key", "")
response = await client.post(
"/approval/detect-intent",
json={"text": "VPN 账号申请"},
)
assert response.status_code == 200
data = response.json()
assert data["code"] == 0
inner = data["data"]
assert inner["is_approval_request"] is True
assert inner["confidence"] == 0.6
assert inner["approval_type"] == "账号权限申请"
assert inner["source"] == "fallback"
@pytest.mark.asyncio
async def test_fallback_with_generic_keyword_no_type(self, client, monkeypatch):
"""兜底场景:'申请'在预过滤列表但无类型映射 → approval_type=None。"""
monkeypatch.setattr(settings, "approval_dify_base_url", "")
monkeypatch.setattr(settings, "approval_dify_api_key", "")
response = await client.post(
"/approval/detect-intent",
json={"text": "我要申请一个东西"},
)
data = response.json()
assert data["code"] == 0
inner = data["data"]
assert inner["is_approval_request"] is True
assert inner["confidence"] == 0.6
assert inner["approval_type"] is None
assert inner["source"] == "fallback"
@pytest.mark.asyncio
async def test_custom_threshold_override(self, client, monkeypatch):
"""自定义阈值:阈值设为 0.5Dify 返回 0.6 → is_approval_request=true。"""
monkeypatch.setattr(settings, "approval_dify_base_url", "http://test-dify")
monkeypatch.setattr(settings, "approval_dify_api_key", "test-api-key")
monkeypatch.setattr(settings, "approval_confidence_threshold", 0.5)
mock_dify = AsyncMock(return_value={
"is_approval_request": True,
"confidence": 0.6,
"approval_type": "设备申请",
})
with patch("app.api.approval._call_dify_approval_intent", mock_dify):
response = await client.post(
"/approval/detect-intent",
json={"text": "申请设备"},
)
data = response.json()
assert data["code"] == 0
inner = data["data"]
# 阈值 0.5,置信度 0.6 >= 0.5 → True
assert inner["is_approval_request"] is True
assert inner["source"] == "dify"
@pytest.mark.asyncio
async def test_employee_id_passed_to_dify(self, client, monkeypatch):
"""验证 employee_id 被传递给 Dify 调用。"""
monkeypatch.setattr(settings, "approval_dify_base_url", "http://test-dify")
monkeypatch.setattr(settings, "approval_dify_api_key", "test-api-key")
mock_dify = AsyncMock(return_value={
"is_approval_request": True,
"confidence": 0.9,
"approval_type": "设备申请",
})
with patch("app.api.approval._call_dify_approval_intent", mock_dify):
await client.post(
"/approval/detect-intent",
json={"text": "申请电脑", "employee_id": "test_emp_001"},
)
# 验证 employee_id 被传给 Dify
mock_dify.assert_called_once_with("申请电脑", "test_emp_001")
@pytest.mark.asyncio
async def test_employee_id_defaults_to_empty_string(self, client, monkeypatch):
"""未提供 employee_id 时,传给 Dify 的是空字符串。"""
monkeypatch.setattr(settings, "approval_dify_base_url", "http://test-dify")
monkeypatch.setattr(settings, "approval_dify_api_key", "test-api-key")
mock_dify = AsyncMock(return_value={
"is_approval_request": True,
"confidence": 0.9,
"approval_type": "设备申请",
})
with patch("app.api.approval._call_dify_approval_intent", mock_dify):
await client.post(
"/approval/detect-intent",
json={"text": "申请电脑"},
)
# 验证 employee_id 默认为空字符串
mock_dify.assert_called_once_with("申请电脑", "")
@pytest.mark.asyncio
async def test_response_has_all_required_fields(self, client):
"""验证响应包含 ApprovalDetectIntentResponse 的所有必需字段。"""
response = await client.post(
"/approval/detect-intent",
json={"text": "你好"},
)
data = response.json()
# 统一响应格式验证
assert data["code"] == 0
assert data["message"] == "success"
# 业务字段验证
inner = data["data"]
# ApprovalDetectIntentResponse 必需字段
assert "is_approval_request" in inner
assert "confidence" in inner
assert "source" in inner
# approval_type 是 Optional,可以为 None 但字段必须存在
assert "approval_type" in inner
@pytest.mark.asyncio
async def test_response_field_types(self, client):
"""验证响应字段类型正确。"""
response = await client.post(
"/approval/detect-intent",
json={"text": "你好"},
)
data = response.json()
assert data["code"] == 0
inner = data["data"]
assert isinstance(inner["is_approval_request"], bool)
assert isinstance(inner["confidence"], (int, float))
assert isinstance(inner["source"], str)
@pytest.mark.asyncio
async def test_dify_not_called_when_keyword_misses(self, client, monkeypatch):
"""关键词未命中时,Dify 不被调用。"""
monkeypatch.setattr(settings, "approval_dify_base_url", "http://test-dify")
monkeypatch.setattr(settings, "approval_dify_api_key", "test-api-key")
mock_dify = AsyncMock(return_value={
"is_approval_request": True,
"confidence": 0.9,
"approval_type": "设备申请",
})
with patch("app.api.approval._call_dify_approval_intent", mock_dify):
response = await client.post(
"/approval/detect-intent",
json={"text": "今天天气真好"},
)
# Dify 不应被调用
mock_dify.assert_not_called()
data = response.json()
assert data["code"] == 0
assert data["data"]["source"] == "keyword_prefilter"
# =============================================================================
# 集成测试:/approval/keywords 端点
# =============================================================================
# Round 2 新增:验证 /approval/keywords 端点使用 success_response() 包装
# =============================================================================
class TestApprovalKeywordsEndpoint:
"""测试 /approval/keywords API 端点。"""
@pytest.mark.asyncio
async def test_keywords_response_format(self, client):
"""验证 /approval/keywords 响应使用统一格式 {code, data, message}。"""
response = await client.get("/approval/keywords")
assert response.status_code == 200
data = response.json()
# 统一响应格式验证
assert data["code"] == 0
assert data["message"] == "success"
# data 应为列表
assert isinstance(data["data"], list)
@pytest.mark.asyncio
async def test_keywords_empty_when_no_templates(self, client):
"""无审批模板配置时,返回空列表。"""
# 测试环境未设置 APPROVAL_TEMPLATE_RESOURCE / APPROVAL_TEMPLATE_DEVICE
# APPROVAL_TEMPLATES 为空字典
response = await client.get("/approval/keywords")
assert response.status_code == 200
data = response.json()
assert data["code"] == 0
assert data["data"] == []
@pytest.mark.asyncio
async def test_keywords_with_mocked_templates(self, client):
"""模拟有审批模板时,返回关键词列表。"""
mock_templates = {
"tpl_resource_001": {
"id": "tpl_resource_001",
"name": "资源申请",
"type": "jump",
"keywords": ["申请资源", "要资源"],
},
"tpl_device_001": {
"id": "tpl_device_001",
"name": "设备申请",
"type": "api",
"keywords": ["申请设备", "要设备"],
},
}
with patch("app.api.approval.APPROVAL_TEMPLATES", mock_templates):
response = await client.get("/approval/keywords")
assert response.status_code == 200
data = response.json()
assert data["code"] == 0
assert data["message"] == "success"
keywords = data["data"]
assert len(keywords) == 4 # 2 模板 × 2 关键词
# 验证每个关键词条目的结构
for item in keywords:
assert "keyword" in item
assert "template_id" in item
assert "template_name" in item
assert "type" in item
# 验证包含预期的关键词
keyword_values = [item["keyword"] for item in keywords]
assert "申请资源" in keyword_values
assert "申请设备" in keyword_values
@pytest.mark.asyncio
async def test_keywords_data_is_list_type(self, client):
"""验证 data 字段始终为列表类型(即使为空)。"""
response = await client.get("/approval/keywords")
data = response.json()
assert isinstance(data["data"], list)
# =============================================================================
# 集成测试:/approval/jump 端点
# =============================================================================
# Round 2 新增:验证 /approval/jump 端点使用 success_response() 包装
# =============================================================================
class TestApprovalJumpEndpoint:
"""测试 /approval/jump API 端点。"""
@pytest.mark.asyncio
async def test_jump_template_not_found(self, client):
"""模板不存在时返回 404。"""
response = await client.post(
"/approval/jump",
json={"template_id": "non_existent_template"},
)
assert response.status_code == 404
assert "模板不存在" in response.json()["detail"]
@pytest.mark.asyncio
async def test_jump_type_mismatch(self, client):
"""模板类型不支持跳转(type != 'jump')时返回 400。"""
mock_templates = {
"tpl_api_001": {
"id": "tpl_api_001",
"name": "API审批",
"type": "api", # 非 jump 类型
"keywords": ["测试"],
},
}
with patch("app.api.approval.APPROVAL_TEMPLATES", mock_templates):
response = await client.post(
"/approval/jump",
json={"template_id": "tpl_api_001"},
)
assert response.status_code == 400
assert "不支持跳转方式" in response.json()["detail"]
@pytest.mark.asyncio
async def test_jump_success_response_format(self, client):
"""跳转成功时返回统一格式 {code: 0, data: {url, template_name}, message}。"""
mock_templates = {
"tpl_jump_001": {
"id": "tpl_jump_001",
"name": "资源申请",
"type": "jump",
"keywords": ["申请资源"],
},
}
with patch("app.api.approval.APPROVAL_TEMPLATES", mock_templates):
response = await client.post(
"/approval/jump",
json={"template_id": "tpl_jump_001"},
)
assert response.status_code == 200
data = response.json()
# 统一响应格式验证
assert data["code"] == 0
assert data["message"] == "success"
# 业务字段验证
inner = data["data"]
assert "url" in inner
assert "template_name" in inner
assert inner["template_name"] == "资源申请"
# URL 应包含 template_id
assert "tpl_jump_001" in inner["url"]
@pytest.mark.asyncio
async def test_jump_url_contains_template_id(self, client):
"""跳转 URL 中包含 template_id。"""
mock_templates = {
"tpl_jump_002": {
"id": "tpl_jump_002",
"name": "设备申请",
"type": "jump",
"keywords": ["申请设备"],
},
}
with patch("app.api.approval.APPROVAL_TEMPLATES", mock_templates):
response = await client.post(
"/approval/jump",
json={"template_id": "tpl_jump_002"},
)
data = response.json()
inner = data["data"]
assert "tpl_jump_002" in inner["url"]
@pytest.mark.asyncio
async def test_jump_with_employee_id(self, client):
"""携带 employee_id 时跳转正常返回(employee_id 可选字段)。"""
mock_templates = {
"tpl_jump_003": {
"id": "tpl_jump_003",
"name": "资源申请",
"type": "jump",
"keywords": ["申请资源"],
},
}
with patch("app.api.approval.APPROVAL_TEMPLATES", mock_templates):
response = await client.post(
"/approval/jump",
json={
"template_id": "tpl_jump_003",
"employee_id": "test_emp_001",
},
)
assert response.status_code == 200
data = response.json()
assert data["code"] == 0
assert data["data"]["template_name"] == "资源申请"
@pytest.mark.asyncio
async def test_jump_missing_template_id(self, client):
"""缺少 template_id 时返回 422Pydantic 校验失败)。"""
response = await client.post(
"/approval/jump",
json={},
)
assert response.status_code == 422