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