Files

835 lines
33 KiB
Python
Raw Permalink Normal View History

# =============================================================================
# IT智能服务台 — 自备电脑补贴(BYOD)资格查询 API 测试
# =============================================================================
# 测试覆盖:
# 1. 岗位匹配逻辑 _match_position(精确/包含/关键词/不匹配/空字符串/全部13岗位)
# 2. 关键词预过滤 _byod_keyword_prefilter(命中/未命中/空字符串)
# 3. 关键词兜底 _byod_fallback_detect(命中/未命中/空字符串)
# 4. API 端点 /byod/detect-intent(关键词命中→fallback / 关键词未命中→prefilter
# 5. API 端点 /byod/check-eligibility(空ID/有资格/无资格/企微异常/空岗位)
# 6. API 端点 /byod/eligible-positions13岗位/结构/URL/notes
# 7. 数据文件 byod_eligible_positions.json 验证
#
# 响应格式:{code: 0, data: {...}, message: "success"}success_response 包装)
# =============================================================================
import json
import os
from unittest.mock import AsyncMock, patch
import pytest
from app.api.byod import (
BYOD_APPLICATION_URL,
BYOD_ELIGIBLE_POSITIONS,
BYOD_NOTES,
BYOD_POSITION_MATCH_KEYWORDS,
BYOD_PREFILTER_KEYWORDS,
_byod_fallback_detect,
_byod_keyword_prefilter,
_match_position,
)
from app.config import settings
# =============================================================================
# 辅助:收集全部 13 个资格岗位(用于参数化测试)
# =============================================================================
ALL_ELIGIBLE_POSITIONS: list[tuple[str, str, str]] = []
for _seq, _cats in BYOD_ELIGIBLE_POSITIONS.items():
for _cat, _positions in _cats.items():
for _pos in _positions:
ALL_ELIGIBLE_POSITIONS.append((_pos, _seq, _cat))
# =============================================================================
# 单元测试:_match_position
# =============================================================================
class TestMatchPosition:
"""测试岗位匹配逻辑 _match_position。"""
def test_exact_match(self):
"""精确匹配:position 与资格岗位完全一致 → 匹配成功。"""
matched, pos, category = _match_position("前端开发岗")
assert matched is True
assert pos == "前端开发岗"
assert category == "技术序列 - 开发类"
def test_contains_match(self):
"""包含匹配:资格岗位是员工岗位的子串 → 匹配成功。"""
matched, pos, category = _match_position("高级前端开发岗")
assert matched is True
assert pos == "前端开发岗"
assert category == "技术序列 - 开发类"
def test_keyword_match(self):
"""关键词匹配:员工岗位包含核心关键词 → 匹配成功。"""
# "前端工程师" 不包含 "前端开发岗" 也不包含 "前端开发"
# 但包含关键词 "前端"
matched, pos, category = _match_position("前端工程师")
assert matched is True
assert pos == "前端开发岗"
assert category == "技术序列 - 开发类"
def test_no_match_it_support(self):
"""不匹配:IT支持组组长 → 匹配失败。"""
matched, pos, category = _match_position("IT支持组组长")
assert matched is False
assert pos == ""
assert category == ""
def test_no_match_sales(self):
"""不匹配:销售经理 → 匹配失败。"""
matched, pos, category = _match_position("销售经理")
assert matched is False
assert pos == ""
assert category == ""
def test_empty_string(self):
"""空字符串 → 匹配失败。"""
matched, pos, category = _match_position("")
assert matched is False
assert pos == ""
assert category == ""
def test_whitespace_only(self):
"""纯空白字符串 → 匹配失败。"""
matched, pos, category = _match_position(" ")
assert matched is False
assert pos == ""
assert category == ""
def test_stripped_match(self):
"""带前后空格的岗位 → 去空格后匹配成功。"""
matched, pos, category = _match_position(" 前端开发岗 ")
assert matched is True
assert pos == "前端开发岗"
assert category == "技术序列 - 开发类"
@pytest.mark.parametrize("position,sequence,category", ALL_ELIGIBLE_POSITIONS)
def test_all_eligible_positions_exact_match(self, position, sequence, category):
"""全部 13 个资格岗位逐一精确匹配测试。"""
matched, matched_pos, matched_cat = _match_position(position)
assert matched is True, f"岗位 '{position}' 应匹配但未匹配"
assert matched_pos == position
assert matched_cat == f"{sequence} - {category}"
def test_keyword_match_ux_design(self):
"""关键词匹配:用户体验设计岗的多种关键词变体。"""
# "UX设计师" 包含关键词 "UX设计"
matched, pos, _ = _match_position("UX设计师")
assert matched is True
assert pos == "用户体验设计岗"
# "交互设计专家" 包含关键词 "交互设计"
matched, pos, _ = _match_position("交互设计专家")
assert matched is True
assert pos == "用户体验设计岗"
def test_keyword_match_mobile(self):
"""关键词匹配:移动端开发岗的多种关键词变体。"""
# "移动开发组长" 包含关键词 "移动开发"
matched, pos, _ = _match_position("移动开发组长")
assert matched is True
assert pos == "移动端开发岗"
def test_no_match_partial_keyword(self):
"""不匹配:仅包含部分关键词(如""不匹配"客户端")。"""
matched, _, _ = _match_position("服务端架构师")
assert matched is False
def test_return_type_is_tuple(self):
"""返回值类型为三元组 (bool, str, str)。"""
result = _match_position("前端开发岗")
assert isinstance(result, tuple)
assert len(result) == 3
assert isinstance(result[0], bool)
assert isinstance(result[1], str)
assert isinstance(result[2], str)
# =============================================================================
# 单元测试:_byod_keyword_prefilter
# =============================================================================
class TestByodKeywordPrefilter:
"""测试 BYOD 关键词预过滤函数。"""
def test_hit_zibei_diannao(self):
"""包含 '自备电脑' 关键词 → True。"""
assert _byod_keyword_prefilter("我想申请自备电脑补贴") is True
def test_hit_byod_uppercase(self):
"""包含 'BYOD' 关键词(大写)→ True。"""
assert _byod_keyword_prefilter("BYOD政策是什么") is True
def test_hit_byod_lowercase(self):
"""包含 'byod' 关键词(小写)→ True(大小写不敏感)。"""
assert _byod_keyword_prefilter("byod政策是什么") is True
def test_hit_diannao_butie(self):
"""包含 '电脑补贴' 关键词 → True。"""
assert _byod_keyword_prefilter("电脑补贴资格") is True
def test_hit_butie_zige(self):
"""包含 '补贴资格' 关键词 → True。"""
assert _byod_keyword_prefilter("我有补贴资格吗") is True
def test_miss_normal_message(self):
"""普通消息不包含 BYOD 关键词 → False。"""
assert _byod_keyword_prefilter("打印机坏了") is False
def test_miss_empty_string(self):
"""空字符串 → False。"""
assert _byod_keyword_prefilter("") is False
def test_miss_unrelated_it_message(self):
"""IT 相关但非 BYOD 的消息 → False。"""
assert _byod_keyword_prefilter("VPN连不上了") is False
assert _byod_keyword_prefilter("我的电脑黑屏了") is False
def test_case_insensitive(self):
"""关键词匹配大小写不敏感。"""
assert _byod_keyword_prefilter("Byod") is True
assert _byod_keyword_prefilter("BYOD") is True
assert _byod_keyword_prefilter("byod") is True
def test_all_prefilter_keywords_work(self):
"""验证 BYOD_PREFILTER_KEYWORDS 中的每个关键词都能被命中。"""
for kw in BYOD_PREFILTER_KEYWORDS:
text = f"测试文本包含{kw}关键词"
assert _byod_keyword_prefilter(text) is True, f"关键词 '{kw}' 未被命中"
# =============================================================================
# 单元测试:_byod_fallback_detect
# =============================================================================
class TestByodFallbackDetect:
"""测试 BYOD 关键词兜底检测函数。"""
def test_fallback_hit_returns_true(self):
"""包含 BYOD 关键词的文本 → 兜底返回 True。"""
is_byod, confidence = _byod_fallback_detect("我想申请自备电脑补贴")
assert is_byod is True
assert confidence == 0.6
def test_fallback_hit_byod_keyword(self):
"""包含 BYOD 关键词 → True。"""
is_byod, _ = _byod_fallback_detect("BYOD政策")
assert is_byod is True
def test_fallback_miss_normal_message(self):
"""普通消息不包含 BYOD 关键词 → False。"""
is_byod, confidence = _byod_fallback_detect("打印机坏了")
assert is_byod is False
assert confidence == 0.0
def test_fallback_empty_string(self):
"""空字符串 → False。"""
is_byod, confidence = _byod_fallback_detect("")
assert is_byod is False
assert confidence == 0.0
def test_fallback_none_text(self):
"""None → False(安全处理)。"""
is_byod, confidence = _byod_fallback_detect(None) # type: ignore[arg-type]
assert is_byod is False
assert confidence == 0.0
def test_fallback_confidence_is_0_6(self):
"""兜底置信度固定为 0.6。"""
_, confidence = _byod_fallback_detect("自备电脑补贴怎么申请")
assert confidence == 0.6
# =============================================================================
# API 端点测试:POST /byod/detect-intent
# =============================================================================
# 响应格式:{code: 0, data: {is_byod_intent, eligible, source, ...}, message: "success"}
# Dify 未配置时:关键词命中 → _call_dify_byod_intent 抛 ValueError → 降级兜底 source=fallback
# =============================================================================
class TestDetectByodIntentEndpoint:
"""测试 /byod/detect-intent API 端点。"""
@pytest.mark.asyncio
async def test_byod_keyword_hit_fallback(self, client):
"""包含 BYOD 关键词 + Dify 未配置 → is_byod_intent=True, source=fallback。"""
# Dify 未配置(approval_dify_base_url 为空),会触发降级兜底
response = await client.post(
"/byod/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_byod_intent"] is True
assert inner["source"] == "fallback"
@pytest.mark.asyncio
async def test_byod_keyword_hit_byod_text(self, client):
"""包含 'BYOD' 关键词 + Dify 未配置 → is_byod_intent=True, source=fallback。"""
response = await client.post(
"/byod/detect-intent",
json={"text": "BYOD政策是什么"},
)
assert response.status_code == 200
data = response.json()
inner = data["data"]
assert inner["is_byod_intent"] is True
assert inner["source"] == "fallback"
@pytest.mark.asyncio
async def test_keyword_not_hit_returns_false(self, client):
"""不包含 BYOD 关键词 → is_byod_intent=False, source=keyword_prefilter。"""
response = await client.post(
"/byod/detect-intent",
json={"text": "打印机坏了,帮我修一下"},
)
assert response.status_code == 200
data = response.json()
assert data["code"] == 0
inner = data["data"]
assert inner["is_byod_intent"] is False
assert inner["source"] == "keyword_prefilter"
@pytest.mark.asyncio
async def test_empty_text_returns_false(self, client):
"""空文本 → 关键词未命中, is_byod_intent=False。"""
response = await client.post(
"/byod/detect-intent",
json={"text": ""},
)
assert response.status_code == 200
data = response.json()
inner = data["data"]
assert inner["is_byod_intent"] is False
assert inner["source"] == "keyword_prefilter"
@pytest.mark.asyncio
async def test_dify_configured_high_confidence(self, client, monkeypatch):
"""Dify 已配置 + 高置信度 → is_byod_intent=True, 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_byod_intent": True,
"confidence": 0.95,
})
with patch("app.api.byod._call_dify_byod_intent", mock_dify):
response = await client.post(
"/byod/detect-intent",
json={"text": "我想申请自备电脑补贴"},
)
assert response.status_code == 200
data = response.json()
inner = data["data"]
assert inner["is_byod_intent"] is True
assert inner["source"] == "dify"
mock_dify.assert_called_once()
@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")
mock_dify = AsyncMock(side_effect=Exception("Dify 服务不可达"))
with patch("app.api.byod._call_dify_byod_intent", mock_dify):
response = await client.post(
"/byod/detect-intent",
json={"text": "自备电脑补贴怎么申请"},
)
assert response.status_code == 200
data = response.json()
inner = data["data"]
assert inner["is_byod_intent"] is True
assert inner["source"] == "fallback"
@pytest.mark.asyncio
async def test_dify_low_confidence_returns_false(self, client, monkeypatch):
"""Dify 已配置 + 低置信度 → is_byod_intent=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_byod_intent": True,
"confidence": 0.5,
})
with patch("app.api.byod._call_dify_byod_intent", mock_dify):
response = await client.post(
"/byod/detect-intent",
json={"text": "电脑补贴资格"},
)
data = response.json()
inner = data["data"]
assert inner["is_byod_intent"] is False
assert inner["source"] == "dify"
@pytest.mark.asyncio
async def test_response_has_all_required_fields(self, client):
"""验证响应包含 ByodEligibilityResponse 的所有必需字段。"""
response = await client.post(
"/byod/detect-intent",
json={"text": "你好"},
)
data = response.json()
inner = data["data"]
assert "is_byod_intent" in inner
assert "eligible" in inner
assert "position" in inner
assert "matched_category" in inner
assert "application_url" in inner
assert "notes" in inner
assert "reason" in inner
assert "source" in inner
@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_byod_intent": True,
"confidence": 0.9,
})
with patch("app.api.byod._call_dify_byod_intent", mock_dify):
await client.post(
"/byod/detect-intent",
json={"text": "自备电脑补贴", "employee_id": "test_emp_001"},
)
mock_dify.assert_called_once_with("自备电脑补贴", "test_emp_001")
# =============================================================================
# API 端点测试:POST /byod/check-eligibility
# =============================================================================
# 需要 mock Redisget_redis 从 app.main 导入 redis_client)和 WecomService
# conftest 已 patch app.services.wecom_service.WecomService,但 byod.py 在模块
# 加载时通过 `from app.services.wecom_service import WecomService` 获得了自己的
# 引用,因此必须额外 patch app.api.byod.WecomService 才能让 mock 生效。
# =============================================================================
@pytest.fixture
def byod_wecom_mock(mock_wecom_instance):
"""Patch app.api.byod.WecomService 并保存/恢复 get_user_info 的 mock 状态。
conftest 在模块级设置了 mock_wecom_module.get_user_info.side_effect
BYOD 测试需要临时覆盖它以返回不同岗位,测试后恢复原值。
同时 patch byod 模块中的 WecomService 引用(conftest 未覆盖此模块)。
"""
original_se = mock_wecom_instance.get_user_info.side_effect
original_rv = mock_wecom_instance.get_user_info.return_value
# patch byod.py 模块中的 WecomService 引用(from ... import WecomService
with patch("app.api.byod.WecomService", return_value=mock_wecom_instance):
yield mock_wecom_instance
mock_wecom_instance.get_user_info.side_effect = original_se
mock_wecom_instance.get_user_info.return_value = original_rv
class TestCheckEligibilityEndpoint:
"""测试 /byod/check-eligibility API 端点。"""
@pytest.mark.asyncio
async def test_empty_employee_id(self, client, mock_redis):
"""employee_id 为空 → eligible=False, reason 包含 '缺少员工ID'"""
with patch("app.main.redis_client", mock_redis, create=True):
response = await client.post(
"/byod/check-eligibility",
json={"employee_id": ""},
)
assert response.status_code == 200
data = response.json()
assert data["code"] == 0
inner = data["data"]
assert inner["eligible"] is False
assert "缺少员工ID" in inner["reason"]
assert inner["source"] == "wecom"
@pytest.mark.asyncio
async def test_eligible_position(self, client, mock_redis, byod_wecom_mock):
"""岗位在资格清单中 → eligible=True, 返回申请链接和注意事项。"""
byod_wecom_mock.get_user_info.side_effect = None
byod_wecom_mock.get_user_info.return_value = {
"position": "前端开发岗",
"name": "张三",
}
with patch("app.main.redis_client", mock_redis, create=True):
response = await client.post(
"/byod/check-eligibility",
json={"employee_id": "zhangsan"},
)
assert response.status_code == 200
data = response.json()
assert data["code"] == 0
inner = data["data"]
assert inner["eligible"] is True
assert inner["position"] == "前端开发岗"
assert inner["matched_category"] == "技术序列 - 开发类"
assert inner["application_url"] == BYOD_APPLICATION_URL
assert inner["notes"] == BYOD_NOTES
assert inner["source"] == "wecom"
@pytest.mark.asyncio
async def test_eligible_position_contains_match(self, client, mock_redis, byod_wecom_mock):
"""包含匹配:岗位 '高级后端开发岗' → eligible=True。"""
byod_wecom_mock.get_user_info.side_effect = None
byod_wecom_mock.get_user_info.return_value = {
"position": "高级后端开发岗",
"name": "李四",
}
with patch("app.main.redis_client", mock_redis, create=True):
response = await client.post(
"/byod/check-eligibility",
json={"employee_id": "lisi"},
)
data = response.json()
inner = data["data"]
assert inner["eligible"] is True
assert inner["position"] == "高级后端开发岗"
assert inner["matched_category"] == "技术序列 - 开发类"
@pytest.mark.asyncio
async def test_eligible_position_keyword_match(self, client, mock_redis, byod_wecom_mock):
"""关键词匹配:岗位 '算法工程师' → eligible=True(关键词 '算法')。"""
byod_wecom_mock.get_user_info.side_effect = None
byod_wecom_mock.get_user_info.return_value = {
"position": "算法工程师",
"name": "王五",
}
with patch("app.main.redis_client", mock_redis, create=True):
response = await client.post(
"/byod/check-eligibility",
json={"employee_id": "wangwu"},
)
data = response.json()
inner = data["data"]
assert inner["eligible"] is True
assert inner["matched_category"] == "技术序列 - 开发类"
@pytest.mark.asyncio
async def test_ineligible_position(self, client, mock_redis, byod_wecom_mock):
"""岗位不在资格清单中 → eligible=False, reason 包含岗位名。"""
byod_wecom_mock.get_user_info.side_effect = None
byod_wecom_mock.get_user_info.return_value = {
"position": "销售经理",
"name": "赵六",
}
with patch("app.main.redis_client", mock_redis, create=True):
response = await client.post(
"/byod/check-eligibility",
json={"employee_id": "zhaoliu"},
)
assert response.status_code == 200
data = response.json()
inner = data["data"]
assert inner["eligible"] is False
assert inner["position"] == "销售经理"
assert "销售经理" in inner["reason"]
assert inner["application_url"] == ""
assert inner["notes"] == []
@pytest.mark.asyncio
async def test_wecom_service_failure(self, client, mock_redis, byod_wecom_mock):
"""企微 API 调用失败 → eligible=False, reason 包含错误信息。"""
async def _raise_error(user_id, **kwargs):
raise Exception("企微API不可达")
byod_wecom_mock.get_user_info.side_effect = _raise_error
with patch("app.main.redis_client", mock_redis, create=True):
response = await client.post(
"/byod/check-eligibility",
json={"employee_id": "test_emp"},
)
assert response.status_code == 200
data = response.json()
inner = data["data"]
assert inner["eligible"] is False
assert "获取员工信息失败" in inner["reason"]
assert inner["source"] == "wecom"
@pytest.mark.asyncio
async def test_empty_position(self, client, mock_redis, byod_wecom_mock):
"""企微返回空岗位 → eligible=False, reason 提示联系IT服务台。"""
byod_wecom_mock.get_user_info.side_effect = None
byod_wecom_mock.get_user_info.return_value = {
"position": "",
"name": "测试员工",
}
with patch("app.main.redis_client", mock_redis, create=True):
response = await client.post(
"/byod/check-eligibility",
json={"employee_id": "test_emp"},
)
assert response.status_code == 200
data = response.json()
inner = data["data"]
assert inner["eligible"] is False
assert "岗位信息" in inner["reason"]
assert inner["source"] == "wecom"
@pytest.mark.asyncio
async def test_product_manager_eligible(self, client, mock_redis, byod_wecom_mock):
"""产品经理岗位 → eligible=True(产品序列 - 产品策划与设计类)。"""
byod_wecom_mock.get_user_info.side_effect = None
byod_wecom_mock.get_user_info.return_value = {
"position": "产品经理",
"name": "产品",
}
with patch("app.main.redis_client", mock_redis, create=True):
response = await client.post(
"/byod/check-eligibility",
json={"employee_id": "pm001"},
)
data = response.json()
inner = data["data"]
assert inner["eligible"] is True
assert inner["matched_category"] == "产品序列 - 产品策划与设计类"
@pytest.mark.asyncio
async def test_response_has_all_required_fields(self, client, mock_redis, byod_wecom_mock):
"""验证响应包含所有必需字段。"""
byod_wecom_mock.get_user_info.side_effect = None
byod_wecom_mock.get_user_info.return_value = {
"position": "测试开发岗",
"name": "测试",
}
with patch("app.main.redis_client", mock_redis, create=True):
response = await client.post(
"/byod/check-eligibility",
json={"employee_id": "tester001"},
)
data = response.json()
inner = data["data"]
assert "is_byod_intent" in inner
assert "eligible" in inner
assert "position" in inner
assert "matched_category" in inner
assert "application_url" in inner
assert "notes" in inner
assert "reason" in inner
assert "source" in inner
# =============================================================================
# API 端点测试:GET /byod/eligible-positions
# =============================================================================
class TestEligiblePositionsEndpoint:
"""测试 /byod/eligible-positions API 端点。"""
@pytest.mark.asyncio
async def test_returns_13_positions(self, client):
"""返回 13 个岗位, total_count=13。"""
response = await client.get("/byod/eligible-positions")
assert response.status_code == 200
data = response.json()
assert data["code"] == 0
assert data["message"] == "success"
inner = data["data"]
assert inner["total_count"] == 13
@pytest.mark.asyncio
async def test_positions_structure(self, client):
"""岗位清单结构正确:序列 → 类别 → 岗位列表。"""
response = await client.get("/byod/eligible-positions")
data = response.json()
positions = data["data"]["positions"]
# 顶级 key 应为序列名称
assert "技术序列" in positions
assert "产品序列" in positions
# 技术序列下有 3 个类别
tech = positions["技术序列"]
assert "开发类" in tech
assert "数据类" in tech
assert "测试类" in tech
# 开发类有 6 个岗位
assert len(tech["开发类"]) == 6
assert "前端开发岗" in tech["开发类"]
assert "算法岗" in tech["开发类"]
# 数据类有 2 个岗位
assert len(tech["数据类"]) == 2
# 测试类有 2 个岗位
assert len(tech["测试类"]) == 2
# 产品序列下有 1 个类别,3 个岗位
product = positions["产品序列"]
assert "产品策划与设计类" in product
assert len(product["产品策划与设计类"]) == 3
@pytest.mark.asyncio
async def test_application_url_present(self, client):
"""返回 application_url 且不为空。"""
response = await client.get("/byod/eligible-positions")
data = response.json()
inner = data["data"]
assert inner["application_url"] == BYOD_APPLICATION_URL
assert "ehr.servyou.com.cn" in inner["application_url"]
@pytest.mark.asyncio
async def test_notes_present(self, client):
"""返回 notes 列表且不为空。"""
response = await client.get("/byod/eligible-positions")
data = response.json()
inner = data["data"]
assert isinstance(inner["notes"], list)
assert len(inner["notes"]) == len(BYOD_NOTES)
assert len(inner["notes"]) > 0
@pytest.mark.asyncio
async def test_total_count_matches_actual(self, client):
"""total_count 与实际岗位数一致。"""
response = await client.get("/byod/eligible-positions")
data = response.json()
inner = data["data"]
positions = inner["positions"]
actual_count = sum(
len(positions_list)
for categories in positions.values()
for positions_list in categories.values()
)
assert inner["total_count"] == actual_count
# =============================================================================
# 数据文件验证:data/byod_eligible_positions.json
# =============================================================================
class TestDataFileValidation:
"""验证 BYOD 资格岗位清单 JSON 数据文件。"""
# 数据文件路径(相对于 backend/ 目录)
DATA_FILE = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
"data",
"byod_eligible_positions.json",
)
@pytest.fixture
def data_content(self):
"""读取并解析数据文件。"""
with open(self.DATA_FILE, encoding="utf-8") as f:
return json.load(f)
def test_file_exists(self):
"""数据文件存在。"""
assert os.path.exists(self.DATA_FILE), f"数据文件不存在: {self.DATA_FILE}"
def test_contains_13_positions(self, data_content):
"""数据文件包含 13 个岗位。"""
positions = data_content["positions"]
total = sum(
len(positions_list)
for categories in positions.values()
for positions_list in categories.values()
)
assert total == 13
assert data_content["total_count"] == 13
def test_structure_correct(self, data_content):
"""结构正确:序列 → 类别 → 岗位列表。"""
positions = data_content["positions"]
assert isinstance(positions, dict)
for seq_name, categories in positions.items():
assert isinstance(seq_name, str)
assert isinstance(categories, dict)
for cat_name, pos_list in categories.items():
assert isinstance(cat_name, str)
assert isinstance(pos_list, list)
for pos in pos_list:
assert isinstance(pos, str)
assert len(pos) > 0
def test_application_url_correct(self, data_content):
"""application_url 正确且指向 eHR 系统。"""
url = data_content["application_url"]
assert isinstance(url, str)
assert url.startswith("https://")
assert "ehr.servyou.com.cn" in url
assert "flowid=7747" in url
def test_notes_not_empty(self, data_content):
"""notes 不为空且为字符串列表。"""
notes = data_content["notes"]
assert isinstance(notes, list)
assert len(notes) > 0
for note in notes:
assert isinstance(note, str)
assert len(note) > 0
def test_data_matches_code(self, data_content):
"""数据文件中的岗位与代码中 BYOD_ELIGIBLE_POSITIONS 一致。"""
data_positions = data_content["positions"]
code_positions = BYOD_ELIGIBLE_POSITIONS
# 逐序列、逐类别、逐岗位对比
for seq, categories in code_positions.items():
assert seq in data_positions, f"序列 '{seq}' 在数据文件中不存在"
for cat, pos_list in categories.items():
assert cat in data_positions[seq], f"类别 '{seq}/{cat}' 在数据文件中不存在"
for pos in pos_list:
assert pos in data_positions[seq][cat], (
f"岗位 '{pos}' 在数据文件中不存在"
)
def test_all_positions_have_keywords(self):
"""所有资格岗位都在 BYOD_POSITION_MATCH_KEYWORDS 中有对应关键词。"""
for seq, categories in BYOD_ELIGIBLE_POSITIONS.items():
for cat, pos_list in categories.items():
for pos in pos_list:
assert pos in BYOD_POSITION_MATCH_KEYWORDS, (
f"岗位 '{pos}' 在 BYOD_POSITION_MATCH_KEYWORDS 中没有对应关键词"
)
assert len(BYOD_POSITION_MATCH_KEYWORDS[pos]) > 0, (
f"岗位 '{pos}' 的关键词列表为空"
)