feat: P0/P1/P2全部功能 — 四层泳道/视角切换/KPI看板/预警/差异反打/预算/知识面板/回顾会/情景预测/Excel导入/角色权限
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user