feat: P0/P1/P2全部功能 — 四层泳道/视角切换/KPI看板/预警/差异反打/预算/知识面板/回顾会/情景预测/Excel导入/角色权限

This commit is contained in:
Hermes CI Fix
2026-07-12 17:46:08 +08:00
parent cdf00efd69
commit ee25d5fa1d
8871 changed files with 1778433 additions and 0 deletions
+212
View File
@@ -0,0 +1,212 @@
"""工具层测试:notifier 推送逻辑(mock外部HTTP"""
import json
from unittest.mock import patch, MagicMock
import pytest
from app.utils.notifier import (
send_wecom_robot,
send_wecom_app,
send_mail,
push_alert,
_build_content,
push_pending_alerts,
)
class TestWecomRobot:
"""企业微信群机器人推送测试(mock HTTP)"""
@patch("urllib.request.urlopen")
def test_send_success(self, mock_urlopen):
"""推送成功"""
mock_resp = MagicMock()
mock_resp.read.return_value = json.dumps({"errcode": 0}).encode()
mock_urlopen.return_value.__enter__.return_value = mock_resp
result = send_wecom_robot("https://qyapi.weixin.qq.com/webhook/test",
"测试标题", "测试内容", "red")
assert result["success"] is True
assert "已推送" in result["message"]
@patch("urllib.request.urlopen")
def test_send_api_error(self, mock_urlopen):
"""企微API返回错误"""
mock_resp = MagicMock()
mock_resp.read.return_value = json.dumps({"errcode": 40001, "errmsg": "invalid webhook"}).encode()
mock_urlopen.return_value.__enter__.return_value = mock_resp
result = send_wecom_robot("https://qyapi.weixin.qq.com/webhook/test",
"测试", "内容", "yellow")
assert result["success"] is False
assert "推送失败" in result["message"]
@patch("urllib.request.urlopen")
def test_send_network_error(self, mock_urlopen):
"""网络异常"""
mock_urlopen.side_effect = Exception("Connection refused")
result = send_wecom_robot("https://qyapi.weixin.qq.com/webhook/test",
"测试", "内容", "green")
assert result["success"] is False
assert "推送异常" in result["message"]
def test_build_content(self):
"""_build_content 内容构建"""
alert = {
"kpi_name": "销售总额",
"period": "2026-06",
"actual_value": "800,000",
"target_value": "1,000,000",
"alert_level": "red",
"resolution": "加强销售推广",
}
content = _build_content(alert)
assert "销售总额" in content
assert "2026-06" in content
assert "800,000" in content
assert "加强销售推广" in content
assert "紧急" in content
class TestPushAlert:
"""主推送函数测试"""
@patch("app.utils.notifier.send_wecom_robot")
def test_push_to_wecom_robot(self, mock_send):
"""推送到企微机器人渠道"""
mock_send.return_value = {"success": True, "message": "ok"}
channels = [{
"name": "测试群", "channel_type": "wecom", "enabled": True,
"config": {"webhook_url": "https://qyapi.weixin.qq.com/webhook/abc"}
}]
results = push_alert({"alert_level": "yellow", "alert_message": "测试"}, channels)
assert len(results) == 1
assert results[0]["channel"] == "wecom"
assert results[0]["success"] is True
@patch("app.utils.notifier.send_wecom_app")
def test_push_to_wecom_app(self, mock_send):
"""推送到企微应用消息"""
mock_send.return_value = {"success": True, "message": "ok"}
channels = [{
"name": "应用通知", "channel_type": "wecom_app", "enabled": True,
"config": {"corp_id": "xxx", "corp_secret": "yyy", "agent_id": "1000002", "touser": "@all"}
}]
results = push_alert({"alert_level": "yellow", "alert_message": "测试"}, channels)
assert len(results) == 1
assert results[0]["channel"] == "wecom_app"
@patch("app.utils.notifier.send_mail")
def test_push_to_mail(self, mock_send):
"""推送到邮件"""
mock_send.return_value = {"success": True, "message": "ok"}
channels = [{
"name": "通知邮箱", "channel_type": "mail", "enabled": True,
"config": {"to": ["admin@example.com"]}
}]
results = push_alert({"alert_level": "red", "alert_message": "测试"}, channels)
assert len(results) == 1
assert results[0]["channel"] == "mail"
def test_disabled_channel_skipped(self):
"""已禁用的渠道跳过"""
channels = [{
"name": "禁用渠道", "channel_type": "wecom", "enabled": False,
"config": {}
}]
results = push_alert({"alert_level": "yellow", "alert_message": "测试"}, channels)
assert len(results) == 0
def test_empty_channels(self):
"""无渠道 → 空结果"""
results = push_alert({"alert_level": "yellow", "alert_message": "测试"}, [])
assert results == []
class TestSendWecomApp:
"""企微应用消息测试"""
@patch("requests.get")
@patch("requests.post")
def test_app_send_success(self, mock_post, mock_get):
"""企微应用消息发送成功"""
mock_get.return_value.json.return_value = {"errcode": 0, "access_token": "fake_token"}
mock_post.return_value.json.return_value = {"errcode": 0}
result = send_wecom_app("corp123", "secret456", "1000001",
"@all", "标题", "内容", "yellow")
assert result["success"] is True
@patch("requests.get")
def test_app_token_fail(self, mock_get):
"""获取token失败"""
mock_get.return_value.json.return_value = {"errcode": 40013, "errmsg": "invalid corpid"}
result = send_wecom_app("bad_corp", "bad_secret", "1000001",
"@all", "标题", "内容", "yellow")
assert result["success"] is False
assert "获取token失败" in result["message"]
class TestSendMail:
"""邮件推送测试(mock SMTP"""
@patch("smtplib.SMTP_SSL")
def test_mail_send_success(self, mock_smtp):
"""邮件发送成功"""
mock_server = MagicMock()
mock_smtp.return_value = mock_server
config = {"host": "smtp.example.com", "port": 465, "user": "user@example.com",
"password": "pass", "use_ssl": True, "from_addr": "user@example.com"}
result = send_mail(config, ["admin@example.com"], "测试标题", "测试内容")
assert result["success"] is True
mock_server.login.assert_called_once()
mock_server.sendmail.assert_called_once()
@patch("smtplib.SMTP_SSL")
def test_mail_send_failure(self, mock_smtp):
"""邮件发送异常"""
mock_smtp.side_effect = Exception("SMTP server error")
config = {"host": "smtp.example.com", "port": 465, "user": "user@example.com",
"password": "pass", "use_ssl": True, "from_addr": "user@example.com"}
result = send_mail(config, ["admin@example.com"], "测试标题", "测试内容")
assert result["success"] is False
assert "邮件发送失败" in result["message"]
class TestPushPendingAlerts:
"""push_pending_alerts 测试"""
def test_no_channels(self, db):
"""没有渠道 → 返回0"""
count = push_pending_alerts(db)
assert count == 0
def test_no_pending_alerts(self, db):
"""没有待处理预警 → 返回0"""
from app.models import NotificationChannel
db.add(NotificationChannel(name="测试群", channel_type="wecom", config={}, enabled=True))
db.commit()
count = push_pending_alerts(db)
assert count == 0
@patch("app.utils.notifier.push_alert")
def test_push_pending_with_data(self, mock_push, db):
"""有渠道+待处理预警 → 尝试推送"""
from app.models import NotificationChannel, KPIAlert, KPIDefinition
kpi = KPIDefinition(kpi_code="TEST_PUSH", kpi_name="推送测试", dimension="finance", status="active")
db.add(kpi)
db.commit()
db.add(NotificationChannel(name="测试群", channel_type="wecom",
config={"webhook_url": "http://test"}, enabled=True))
db.add(KPIAlert(kpi_id=kpi.id, alert_level="red", alert_message="测试预警", status="pending"))
db.commit()
mock_push.return_value = [{"channel": "wecom", "channel_name": "测试群", "success": True, "message": "ok"}]
count = push_pending_alerts(db)
mock_push.assert_called_once()
assert count == 1