Files

383 lines
14 KiB
Python
Raw Permalink Normal View History

# =============================================================================
# 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