Files
wecom_it_smart_desk/backend/tests/test_asset_approval_urge.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

383 lines
14 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智能服务台 — 资产升级审批智能推送功能测试
# =============================================================================
# 测试覆盖:
# AssetService:
# 1. find_asset — 查找资产(存在/不存在/大小写/空格)
# 2. _parse_date — 多种日期格式解析
# 3. _format_years — 年限格式化
# 4. check_device_age — 完整核查流程
# 5. _format_opinion — 审批意见生成
# approval.py:
# 6. _extract_asset_code — 提取资产编号(Text/Selector/未找到)
# 7. _extract_current_approver — 提取审批人(list/dict/无审批中)
# 8. _build_urge_description — 构建卡片描述
# =============================================================================
import os
from datetime import date, datetime
import pytest
from app.services.asset_service import AssetService
from app.api.approval import (
_extract_asset_code,
_extract_current_approver,
_build_urge_description,
)
# =============================================================================
# 测试常量
# =============================================================================
EXCEL_PATH = r"D:\资料\00-工作文件\03-资产管理\固定资产清单\资产记录\2025资产\2025资产1~12.xlsx"
KNOWN_ASSET_CODE = "01011801-02012-041698"
# 检查 Excel 文件是否存在(不存在则跳过依赖真实文件的测试)
excel_exists = os.path.isfile(EXCEL_PATH)
# =============================================================================
# AssetService — find_asset 测试
# =============================================================================
class TestFindAsset:
"""find_asset 方法测试"""
@pytest.fixture(scope="class")
def asset_service(self):
"""创建 AssetService 实例(类级共享,避免重复加载 18000 行 Excel"""
if not excel_exists:
pytest.skip(f"Excel 文件不存在: {EXCEL_PATH}")
service = AssetService(excel_path=EXCEL_PATH)
yield service
service.close()
def test_find_asset_found(self, asset_service):
"""查找已知存在的资产编号,验证返回dict包含正确字段"""
result = asset_service.find_asset(KNOWN_ASSET_CODE)
assert result is not None, f"未找到资产编号: {KNOWN_ASSET_CODE}"
assert isinstance(result, dict)
# 验证关键字段存在
assert result.get("固定资产编码") is not None
assert "sheet_name" in result
assert "固定资产名称" in result
assert "开始使用日期" in result
def test_find_asset_not_found(self, asset_service):
"""查找不存在的资产编号,验证返回 None"""
result = asset_service.find_asset("NONEXISTENT-CODE-99999")
assert result is None
def test_find_asset_case_insensitive(self, asset_service):
"""大小写不敏感匹配测试"""
# 资产编号全小写搜索(编号本身无字母,验证 lower() 逻辑不报错)
result = asset_service.find_asset(KNOWN_ASSET_CODE.lower())
assert result is not None, "大小写不敏感匹配失败"
def test_find_asset_trim_whitespace(self, asset_service):
"""前后空格trim测试"""
result = asset_service.find_asset(f" {KNOWN_ASSET_CODE} ")
assert result is not None, "trim 空格后匹配失败"
# =============================================================================
# AssetService — _parse_date 测试
# =============================================================================
class TestParseDate:
"""_parse_date 方法测试(纯逻辑,不需要 Excel 文件)"""
@pytest.fixture
def service(self):
"""创建 AssetService 实例(不触发 Excel 加载)"""
return AssetService(excel_path=EXCEL_PATH)
def test_parse_date_formats(self, service):
"""测试多种日期格式解析:datetime/date/字符串/Excel序列号"""
# datetime 对象 → 取 date 部分
dt = datetime(2025, 1, 23, 10, 30, 0)
assert service._parse_date(dt) == date(2025, 1, 23)
# date 对象 → 直接返回
d = date(2025, 1, 23)
assert service._parse_date(d) == date(2025, 1, 23)
# 字符串 — 4 种格式
assert service._parse_date("2025-01-23") == date(2025, 1, 23)
assert service._parse_date("2025/01/23") == date(2025, 1, 23)
assert service._parse_date("2025.01.23") == date(2025, 1, 23)
assert service._parse_date("2025年01月23日") == date(2025, 1, 23)
# Excel 序列号(date(1899,12,30) + N days = 目标日期)
excel_serial = (date(2025, 1, 23) - date(1899, 12, 30)).days
assert service._parse_date(excel_serial) == date(2025, 1, 23)
# 浮点数序列号也能解析
assert service._parse_date(float(excel_serial)) == date(2025, 1, 23)
def test_parse_date_none(self, service):
"""None 输入返回 None"""
assert service._parse_date(None) is None
# =============================================================================
# AssetService — _format_years 测试
# =============================================================================
class TestFormatYears:
"""_format_years 方法测试(纯逻辑)"""
@pytest.fixture
def service(self):
return AssetService(excel_path=EXCEL_PATH)
def test_format_years(self, service):
"""测试浮点年限格式化"""
# 5.17 年 → int(5.17*12)=62 → 5年2个月
assert service._format_years(5.17) == "5年2个月"
# 0.5 年 → int(0.5*12)=6 → 0年6个月
assert service._format_years(0.5) == "0年6个月"
# 整数年限 5.0 → 60个月 → 5年0个月
assert service._format_years(5.0) == "5年0个月"
# 0 年
assert service._format_years(0.0) == "0年0个月"
# =============================================================================
# AssetService — check_device_age 测试
# =============================================================================
class TestCheckDeviceAge:
"""check_device_age 方法测试"""
@pytest.fixture(scope="class")
def asset_service(self):
if not excel_exists:
pytest.skip(f"Excel 文件不存在: {EXCEL_PATH}")
service = AssetService(excel_path=EXCEL_PATH)
yield service
service.close()
def test_check_device_age_found(self, asset_service):
"""测试完整核查流程(真实Excel),验证返回dict结构正确"""
result = asset_service.check_device_age(KNOWN_ASSET_CODE, threshold_years=5)
assert isinstance(result, dict)
assert result["found"] is True
assert result["asset_code"] == KNOWN_ASSET_CODE
assert "asset_name" in result
assert "start_date" in result
assert "years_used" in result
assert "years_display" in result
assert "meets_threshold" in result
assert "opinion" in result
assert isinstance(result["meets_threshold"], bool)
def test_check_device_age_not_found(self, asset_service):
"""资产不存在时的核查结果"""
result = asset_service.check_device_age("NONEXISTENT-CODE-99999")
assert isinstance(result, dict)
assert result["found"] is False
assert "opinion" in result
assert "未在资产清单中找到" in result["opinion"]
# =============================================================================
# AssetService — _format_opinion 测试
# =============================================================================
class TestFormatOpinion:
"""_format_opinion 方法测试(纯逻辑)"""
@pytest.fixture
def service(self):
return AssetService(excel_path=EXCEL_PATH)
def test_format_opinion_meets(self, service):
"""满足5年条件的意见文本格式"""
opinion = service._format_opinion(
asset_name="电脑笔记本",
start_date=date(2020, 1, 1),
years_used=5.5,
threshold=5,
meets=True,
)
assert "电脑笔记本" in opinion
assert "2020-01-01" in opinion
assert "" in opinion
assert "已满5年" in opinion
assert "符合更换条件" in opinion
def test_format_opinion_not_meets(self, service):
"""不满足5年条件的意见文本格式"""
opinion = service._format_opinion(
asset_name="显示器",
start_date=date(2023, 6, 15),
years_used=2.0,
threshold=5,
meets=False,
)
assert "显示器" in opinion
assert "2023-06-15" in opinion
assert "" in opinion
assert "未满5年" in opinion
assert "不符合更换条件" in opinion
# =============================================================================
# approval.py — _extract_asset_code 测试
# =============================================================================
class TestExtractAssetCode:
"""_extract_asset_code 函数测试"""
def test_extract_asset_code_text(self):
"""Text 控件类型的资产编号提取"""
detail = {
"info": {
"apply_data": {
"contents": [
{
"control": "Text",
"id": "Text-1",
"title": [{"text": "资产编号", "lang": "zh_CN"}],
"value": {"text": "01011801-02012-041698"}
}
]
}
}
}
result = _extract_asset_code(detail)
assert result == "01011801-02012-041698"
def test_extract_asset_code_selector(self):
"""Selector 控件类型的资产编号提取"""
detail = {
"info": {
"apply_data": {
"contents": [
{
"control": "Selector",
"id": "Selector-1",
"title": [{"text": "固定资产编号", "lang": "zh_CN"}],
"value": {"value": "01011801-02012-041698"}
}
]
}
}
}
result = _extract_asset_code(detail)
assert result == "01011801-02012-041698"
def test_extract_asset_code_not_found(self):
"""表单中无资产编号字段时返回 None"""
detail = {
"info": {
"apply_data": {
"contents": [
{
"control": "Text",
"id": "Text-1",
"title": [{"text": "申请理由", "lang": "zh_CN"}],
"value": {"text": "电脑太旧了"}
}
]
}
}
}
result = _extract_asset_code(detail)
assert result is None
# =============================================================================
# approval.py — _extract_current_approver 测试
# =============================================================================
class TestExtractCurrentApprover:
"""_extract_current_approver 函数测试"""
def test_extract_current_approver_list(self):
"""approver 为列表格式时的提取(企微API标准格式)"""
detail = {
"info": {
"sp_record": [
{
"status": 1,
"type": 1,
"approverattr": 1,
"approver": [
{"userid": "zhangsan", "partyid": "2"}
]
}
]
}
}
result = _extract_current_approver(detail)
assert result == "zhangsan"
def test_extract_current_approver_dict(self):
"""approver 为 dict 格式时的兼容提取"""
detail = {
"info": {
"sp_record": [
{
"status": 1,
"type": 1,
"approverattr": 1,
"approver": {"userid": "lisi", "partyid": "3"}
}
]
}
}
result = _extract_current_approver(detail)
assert result == "lisi"
def test_extract_current_approver_no_pending(self):
"""无审批中节点时返回 None"""
detail = {
"info": {
"sp_record": [
{
"status": 2, # 已通过,非审批中
"type": 1,
"approver": [{"userid": "zhangsan", "partyid": "2"}]
}
]
}
}
result = _extract_current_approver(detail)
assert result is None
# =============================================================================
# approval.py — _build_urge_description 测试
# =============================================================================
class TestBuildUrgeDescription:
"""_build_urge_description 函数测试"""
def test_build_urge_description(self):
"""验证卡片描述文本包含所有必要字段"""
check_result = {
"found": True,
"asset_code": "01011801-02012-041698",
"asset_name": "电脑笔记本",
"start_date": "2020-01-23",
"years_display": "5年6个月",
"meets_threshold": True,
}
applyer_userid = "sxn"
desc = _build_urge_description(check_result, applyer_userid)
# 验证所有关键字段都在描述中
assert "01011801-02012-041698" in desc
assert "电脑笔记本" in desc
assert "2020-01-23" in desc
assert "5年6个月" in desc
assert "" in desc
assert "sxn" in desc
assert "请点击查看审批详情" in desc