924e40a84e
- feishu_notifier: 新增 send_feishu_card(interactive msg_type,复用 _post_feishu) - compare_alert_format: 新增 format_alert_card(schema 2.0, header red, markdown摘要+table 6列) - 列序: 时间/手机号/用时/失败原因/版本/trace(lark_md);无 width 属性 - cost 列 helper _cost_cell: total_ms→Ns / step_count→M步 / 两者用" / "连 / 都无给"-" - 截断: 超 max_total 只出摘要; 空 hits 返回「本期无异常」卡片 - 保留 format_alert_message / format_alert_post(有测试依赖) - compare_alert_worker: _send(post) → _send_card(card); _scan_and_alert 调 format_alert_card - SEND_EMPTY 分支: 传空 hits 给 format_alert_card 得「本期无异常」卡片 - webhook 空降级保留; build_hits/水位逻辑不动 - tests: format/feishu/worker 测试全适配新接口,86 passed 零回归 - 删除临时脚本 scripts/_test_alert_card.py Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
170 lines
6.6 KiB
Python
170 lines
6.6 KiB
Python
"""飞书群机器人发送:text/post 消息体格式 / 关键词失败 / 网络失败,均 monkeypatch httpx 不真发。"""
|
|
from __future__ import annotations
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from app.integrations import feishu_notifier
|
|
|
|
# ---- send_feishu_text ----
|
|
|
|
def test_send_posts_text_payload(monkeypatch):
|
|
captured = {}
|
|
|
|
def fake_post(url, json, timeout):
|
|
captured["url"] = url
|
|
captured["json"] = json
|
|
return httpx.Response(200, json={"code": 0, "msg": "success"})
|
|
|
|
monkeypatch.setattr(feishu_notifier.httpx, "post", fake_post)
|
|
feishu_notifier.send_feishu_text("https://open.feishu.cn/hook/xxx", "比价失败报警 · test", timeout=5.0)
|
|
assert captured["url"] == "https://open.feishu.cn/hook/xxx"
|
|
assert captured["json"] == {"msg_type": "text", "content": {"text": "比价失败报警 · test"}}
|
|
|
|
|
|
def test_send_raises_on_keyword_rejection(monkeypatch):
|
|
def fake_post(url, json, timeout):
|
|
return httpx.Response(200, json={"code": 19024, "msg": "Key Words Not Found"})
|
|
|
|
monkeypatch.setattr(feishu_notifier.httpx, "post", fake_post)
|
|
with pytest.raises(feishu_notifier.FeishuNotifyError):
|
|
feishu_notifier.send_feishu_text("https://hook", "无关键词文本", timeout=5.0)
|
|
|
|
|
|
def test_send_raises_on_http_error(monkeypatch):
|
|
def fake_post(url, json, timeout):
|
|
return httpx.Response(500, text="boom")
|
|
|
|
monkeypatch.setattr(feishu_notifier.httpx, "post", fake_post)
|
|
with pytest.raises(feishu_notifier.FeishuNotifyError):
|
|
feishu_notifier.send_feishu_text("https://hook", "比价失败报警", timeout=5.0)
|
|
|
|
|
|
# ---- send_feishu_post ----
|
|
|
|
def test_send_post_payload_structure(monkeypatch):
|
|
"""send_feishu_post 发出 msg_type=post 的 payload,结构符合飞书 post 格式。"""
|
|
captured = {}
|
|
|
|
def fake_post(url, json, timeout):
|
|
captured["url"] = url
|
|
captured["json"] = json
|
|
return httpx.Response(200, json={"code": 0, "msg": "success"})
|
|
|
|
monkeypatch.setattr(feishu_notifier.httpx, "post", fake_post)
|
|
content = [[{"tag": "text", "text": "合计 3 条"}]]
|
|
feishu_notifier.send_feishu_post(
|
|
"https://open.feishu.cn/hook/yyy",
|
|
"🚨 比价失败报警 · 2026-08-04",
|
|
content,
|
|
timeout=5.0,
|
|
)
|
|
assert captured["url"] == "https://open.feishu.cn/hook/yyy"
|
|
payload = captured["json"]
|
|
assert payload["msg_type"] == "post"
|
|
zh_cn = payload["content"]["post"]["zh_cn"]
|
|
assert zh_cn["title"] == "🚨 比价失败报警 · 2026-08-04"
|
|
assert zh_cn["content"] == content
|
|
|
|
|
|
def test_send_post_raises_on_code_nonzero(monkeypatch):
|
|
"""send_feishu_post code!=0 时抛 FeishuNotifyError。"""
|
|
def fake_post(url, json, timeout):
|
|
return httpx.Response(200, json={"code": 19024, "msg": "Key Words Not Found"})
|
|
|
|
monkeypatch.setattr(feishu_notifier.httpx, "post", fake_post)
|
|
with pytest.raises(feishu_notifier.FeishuNotifyError):
|
|
feishu_notifier.send_feishu_post(
|
|
"https://hook", "🚨 比价失败报警", [[{"tag": "text", "text": "test"}]], timeout=5.0
|
|
)
|
|
|
|
|
|
def test_send_post_raises_on_http_error(monkeypatch):
|
|
"""send_feishu_post 非 2xx 抛 FeishuNotifyError。"""
|
|
def fake_post(url, json, timeout):
|
|
return httpx.Response(500, text="internal server error")
|
|
|
|
monkeypatch.setattr(feishu_notifier.httpx, "post", fake_post)
|
|
with pytest.raises(feishu_notifier.FeishuNotifyError):
|
|
feishu_notifier.send_feishu_post(
|
|
"https://hook", "🚨 比价失败报警", [[{"tag": "text", "text": "test"}]], timeout=5.0
|
|
)
|
|
|
|
|
|
def test_send_post_raises_on_network_error(monkeypatch):
|
|
"""send_feishu_post 网络异常抛 FeishuNotifyError。"""
|
|
def fake_post(url, json, timeout):
|
|
raise httpx.ConnectError("connection refused")
|
|
|
|
monkeypatch.setattr(feishu_notifier.httpx, "post", fake_post)
|
|
with pytest.raises(feishu_notifier.FeishuNotifyError):
|
|
feishu_notifier.send_feishu_post(
|
|
"https://hook", "🚨 比价失败报警", [[{"tag": "text", "text": "test"}]], timeout=5.0
|
|
)
|
|
|
|
|
|
# ---- send_feishu_card ----
|
|
|
|
def test_send_card_payload_structure(monkeypatch):
|
|
"""send_feishu_card 发出 msg_type=interactive + card 字段的 payload。"""
|
|
captured = {}
|
|
|
|
def fake_post(url, json, timeout):
|
|
captured["url"] = url
|
|
captured["json"] = json
|
|
return httpx.Response(200, json={"code": 0, "msg": "success"})
|
|
|
|
monkeypatch.setattr(feishu_notifier.httpx, "post", fake_post)
|
|
card = {
|
|
"schema": "2.0",
|
|
"header": {"title": {"tag": "plain_text", "content": "🚨 比价失败报警 · test"}, "template": "red"},
|
|
"body": {"elements": [{"tag": "markdown", "content": "近 15 分钟"}]},
|
|
}
|
|
feishu_notifier.send_feishu_card("https://open.feishu.cn/hook/zzz", card, timeout=5.0)
|
|
assert captured["url"] == "https://open.feishu.cn/hook/zzz"
|
|
assert captured["json"]["msg_type"] == "interactive"
|
|
assert captured["json"]["card"] is card
|
|
|
|
|
|
def test_send_card_uses_default_timeout(monkeypatch):
|
|
"""send_feishu_card 默认 timeout=10.0。"""
|
|
captured = {}
|
|
|
|
def fake_post(url, json, timeout):
|
|
captured["timeout"] = timeout
|
|
return httpx.Response(200, json={"code": 0, "msg": "success"})
|
|
|
|
monkeypatch.setattr(feishu_notifier.httpx, "post", fake_post)
|
|
feishu_notifier.send_feishu_card("https://hook", {"schema": "2.0"})
|
|
assert captured["timeout"] == 10.0
|
|
|
|
|
|
def test_send_card_raises_on_code_nonzero(monkeypatch):
|
|
"""send_feishu_card code!=0 时抛 FeishuNotifyError。"""
|
|
def fake_post(url, json, timeout):
|
|
return httpx.Response(200, json={"code": 19024, "msg": "Key Words Not Found"})
|
|
|
|
monkeypatch.setattr(feishu_notifier.httpx, "post", fake_post)
|
|
with pytest.raises(feishu_notifier.FeishuNotifyError):
|
|
feishu_notifier.send_feishu_card("https://hook", {"schema": "2.0"})
|
|
|
|
|
|
def test_send_card_raises_on_http_error(monkeypatch):
|
|
"""send_feishu_card 非 2xx 抛 FeishuNotifyError。"""
|
|
def fake_post(url, json, timeout):
|
|
return httpx.Response(500, text="boom")
|
|
|
|
monkeypatch.setattr(feishu_notifier.httpx, "post", fake_post)
|
|
with pytest.raises(feishu_notifier.FeishuNotifyError):
|
|
feishu_notifier.send_feishu_card("https://hook", {"schema": "2.0"})
|
|
|
|
|
|
def test_send_card_raises_on_network_error(monkeypatch):
|
|
"""send_feishu_card 网络异常抛 FeishuNotifyError。"""
|
|
def fake_post(url, json, timeout):
|
|
raise httpx.ConnectError("connection refused")
|
|
|
|
monkeypatch.setattr(feishu_notifier.httpx, "post", fake_post)
|
|
with pytest.raises(feishu_notifier.FeishuNotifyError):
|
|
feishu_notifier.send_feishu_card("https://hook", {"schema": "2.0"})
|