Files
cma-management/backend/tests/test_notifications.py
wecom-fullstack 0c2e57a4cf fix(cma): 工作台跨实体污染治本 — 全局键拆账套 + 缺列收口 + 预警归属兜底
任务 dama-workbench-entity-isolation-fullstack-20260922(DAMA 诊断 / 项目Bot batch5 §B / 任总 2026-09-22「都批」)

B1 现金基线拆账套(零 DDL)
- cash_forecast_engine: 键 cash.current_balance → cash.current_balance.<entity_id>;
  读只读本账套键、不做旧键回退;set_* 签名 entity_id 必传(漏传 TypeError)
- cash.py: POST /balance 挂 get_entity_id;网银流水联动写回带账套
B3(a) alignment_config 拆账套(零 DDL)
- alignment.py: GET/POST /config、GET /tree 只读 alignment_config.<entity_id>;未配置=configured:false
B3(b) 预警 KPI 归属兜底
- bot_bridge_v2: 去掉 kpi_id or 1 兜底;本账套无 KPI 定义 → 跳过建预警 + OperationLog 留痕
  (kpi_alerts.kpi_id NOT NULL ⇒ 方案A 置 NULL 不可行,走方案B 跳过+审计)
B2 缺列五表分表处置
- notification_logs: 经 alert_id → kpi_alerts.entity_id join 归属(无父键行不返回)
- notification_channels / okr_templates: 集团共享口径写入 docstring(有意不过滤)
- system_configs: 由 B1/B3(a) 拆键覆盖;kpi_data_quality_logs 沿用 batch5 的 join 收口
P0 语义收敛
- tenant.py: current_tenant 收敛为项目Bot A2A 状态记录,GET 仅 it/ceo;POST 补 operation_logs 审计
证据:新增/改写用例 22 条;全量 1370 passed(基线 1348)
2026-09-22 11:06:02 +08:00

200 lines
7.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""通知配置模块测试 — 渠道CRUD + 推送历史"""
import pytest
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
from tests.conftest import create_test_user, create_test_kpi, get_token_for_user, auth_header
class TestNotificationChannels:
"""通知渠道CRUD测试"""
BASE = "/api/cma/notifications"
def test_list_channels_empty(self, client: TestClient, db: Session):
"""空列表"""
create_test_user(db)
token = get_token_for_user(client)
resp = client.get(f"{self.BASE}/channels", headers=auth_header(token))
assert resp.status_code == 200
assert resp.json()["data"] == []
def test_create_wecom_channel(self, client: TestClient, db: Session):
"""创建企微机器人渠道"""
create_test_user(db)
token = get_token_for_user(client)
resp = client.post(
f"{self.BASE}/channels",
headers=auth_header(token),
json={
"name": "告警群",
"channel_type": "wecom",
"config": {"webhook_url": "https://qyapi.weixin.qq.com/webhook/abc"},
"enabled": True,
},
)
assert resp.status_code == 200
data = resp.json()
assert data["name"] == "告警群"
assert data["channel_type"] == "wecom"
assert data["enabled"] is True
def test_create_mail_channel(self, client: TestClient, db: Session):
"""创建邮件渠道"""
create_test_user(db)
token = get_token_for_user(client)
resp = client.post(
f"{self.BASE}/channels",
headers=auth_header(token),
json={
"name": "管理邮箱",
"channel_type": "mail",
"config": {
"host": "smtp.qq.com",
"port": 465,
"user": "admin@example.com",
"password": "pass",
"to": ["admin@example.com"],
},
},
)
assert resp.status_code == 200
assert resp.json()["channel_type"] == "mail"
def test_update_channel(self, client: TestClient, db: Session):
"""更新渠道"""
create_test_user(db)
token = get_token_for_user(client)
create_resp = client.post(
f"{self.BASE}/channels",
headers=auth_header(token),
json={"name": "旧名称", "channel_type": "wecom", "config": {}},
)
ch_id = create_resp.json()["id"]
resp = client.put(
f"{self.BASE}/channels/{ch_id}",
headers=auth_header(token),
json={"name": "新名称", "enabled": False},
)
assert resp.status_code == 200
assert resp.json()["name"] == "新名称"
assert resp.json()["enabled"] is False
def test_delete_channel(self, client: TestClient, db: Session):
"""删除渠道"""
create_test_user(db)
token = get_token_for_user(client)
create_resp = client.post(
f"{self.BASE}/channels",
headers=auth_header(token),
json={"name": "待删除", "channel_type": "wecom", "config": {}},
)
ch_id = create_resp.json()["id"]
resp = client.delete(
f"{self.BASE}/channels/{ch_id}",
headers=auth_header(token),
)
assert resp.status_code == 200
assert resp.json()["message"] == "已删除"
# 验证已删除
get_resp = client.get(f"{self.BASE}/channels", headers=auth_header(token))
assert len(get_resp.json()["data"]) == 0
def test_delete_channel_not_found(self, client: TestClient, db: Session):
"""删除不存在的渠道"""
create_test_user(db)
token = get_token_for_user(client)
resp = client.delete(f"{self.BASE}/channels/99999", headers=auth_header(token))
assert resp.status_code == 404
def test_test_channel_network(self, client: TestClient, db: Session):
"""测试推送(网络可能不通,但不应500)"""
create_test_user(db)
token = get_token_for_user(client)
create_resp = client.post(
f"{self.BASE}/channels",
headers=auth_header(token),
json={"name": "测试", "channel_type": "wecom", "config": {"webhook_url": "http://invalid"}, "enabled": True},
)
ch_id = create_resp.json()["id"]
# 网络不通,但应该返回结果而不是500
resp = client.post(
f"{self.BASE}/channels/{ch_id}/test",
headers=auth_header(token),
)
assert resp.status_code == 200
assert "results" in resp.json()
class TestNotificationLogs:
"""通知历史测试"""
def test_logs_empty(self, client: TestClient, db: Session):
"""空日志"""
create_test_user(db)
token = get_token_for_user(client)
resp = client.get("/api/cma/notifications/logs", headers=auth_header(token))
assert resp.status_code == 200
assert resp.json()["total"] == 0
def test_logs_with_data(self, client: TestClient, db: Session):
"""有了日志记录后可查询
2026-09-22(dama-workbench-entity-isolation-fullstack-20260922 §四 B2)行为变更:
`notification_logs` 无 entity_id 列 → 改为**经父键 join 归属**
(`alert_id → kpi_alerts.id → kpi_alerts.entity_id`)。
故日志必须挂到**本账套真实存在**的 KPIAlert 上才可见;
`alert_id IS NULL` 或指向不存在父键的孤儿行**不返回**(宁少勿错)。
"""
from app.models import NotificationLog, KPIAlert
from datetime import datetime
kpi = create_test_kpi(db, kpi_code="NOTIF_LOG_KPI", entity_id=1)
alert = KPIAlert(entity_id=1, kpi_id=kpi.id, alert_level="yellow",
alert_message="通知日志用例预警", status="pending")
db.add(alert)
db.commit()
db.refresh(alert)
db.add_all([
NotificationLog(
alert_id=alert.id, channel="wecom", recipient="测试群",
title="测试通知", content="测试内容", status="sent", sent_at=datetime.now(),
),
# 孤儿行:无父键可归属 → 一律不返回
NotificationLog(alert_id=None, channel="wecom", title="无归属通知", status="sent"),
])
db.commit()
create_test_user(db)
token = get_token_for_user(client)
resp = client.get("/api/cma/notifications/logs", headers=auth_header(token))
assert resp.status_code == 200
assert resp.json()["total"] == 1, resp.json()
assert len(resp.json()["data"]) == 1
assert resp.json()["data"][0]["title"] == "测试通知"
class TestAlertPush:
"""手动推送测试"""
def test_push_alerts_no_channels(self, client: TestClient, db: Session):
"""推送端点已移除,预期404"""
create_test_user(db)
token = get_token_for_user(client)
resp = client.post("/api/cma/notifications/alerts/push", headers=auth_header(token))
assert resp.status_code == 404, "push端点已移除"