Files
ai-mail-bot/tests/test_unsub_keyboard.py
T
earthjasonlin 8a471cb489 fix(tg_bot): 放宽退订链接 URL 长度上限,长 tracking 链接不被误杀
实测 D:\DOWNLOADS\mail_bot.db 中 ChatGPT 推广邮件的退订链接
(https://r.openai.com/asm/unsubscribe/?...) 长度 700+ 字符,
而 _link_buttons 的 len(url) < 200 校验会把它过滤掉,导致
即使 AI 提取出退订链接、显示层也修好了,按钮依然不显示。
上限 200 -> 2048(Telegram inline URL 按钮安全范围)。

新增回归测试 test_long_unsub_url_rendered / test_oversized_url_filtered,
共 9 个用例全部通过。
2026-09-03 23:52:31 +08:00

105 lines
4.2 KiB
Python

"""测试摘要键盘渲染:推广邮件的退订链接按钮必须显示且排最前。"""
from src.tg_bot import _is_unsub_link, _link_buttons, _summary_keyboard
def _rows_of(kb: dict) -> list[list[dict]]:
return kb["inline_keyboard"]
def test_promotion_with_unsub_link_shows_button_first():
"""推广邮件:AI 提取到退订链接时必须显示,且排在第一行"""
links = [
{"text": "Manage Preferences", "url": "https://e.example.com/pref"},
{"text": "推广活动详情", "url": "https://e.example.com/deal"},
]
kb = _summary_keyboard(can_reply=False, is_promotion=True, links=links)
rows = _rows_of(kb)
first = rows[0][0]
assert first["url"] == "https://e.example.com/pref"
assert "🔕" in first["text"]
# 其余按钮仍然存在
texts = [b["text"] for r in rows for b in r]
assert "告诉我更多" in texts
assert "查看原文" in texts
def test_promotion_without_links_keeps_default_buttons():
"""推广邮件:没有链接时保持原有按钮"""
kb = _summary_keyboard(can_reply=False, is_promotion=True, links=None)
rows = _rows_of(kb)
assert len(rows) == 2
assert rows[0][0]["text"] == "告诉我更多"
assert rows[1][0]["text"] == "查看原文"
def test_promotion_with_only_non_unsub_link():
"""推广邮件:只有非退订链接时也正常渲染(🔗),不冒充退订按钮"""
links = [{"text": "查看活动", "url": "https://e.example.com/event"}]
kb = _summary_keyboard(can_reply=False, is_promotion=True, links=links)
rows = _rows_of(kb)
assert rows[0][0]["text"] == "🔗 查看活动"
assert rows[1][0]["text"] == "告诉我更多"
assert rows[2][0]["text"] == "查看原文"
def test_normal_email_links_still_rendered():
"""普通邮件:链接按钮行为不变"""
links = [{"text": "查看账单", "url": "https://e.example.com/bill"}]
kb = _summary_keyboard(can_reply=True, is_promotion=False, links=links)
rows = _rows_of(kb)
texts = [b["text"] for r in rows for b in r]
assert "🔗 查看账单" in texts
def test_unsub_detection_variants():
"""退订链接识别:文字与 URL 关键词变体"""
assert _is_unsub_link({"text": "Unsubscribe", "url": "https://x.com"})
assert _is_unsub_link({"text": "退订", "url": "https://x.com"})
assert _is_unsub_link({"text": "点击这里取消订阅", "url": "https://x.com"})
assert _is_unsub_link({"text": "管理偏好设置", "url": "https://x.com"})
assert _is_unsub_link({"text": "更多", "url": "https://x.com/unsubscribe"})
assert _is_unsub_link({"text": "Preferences", "url": "https://x.com/email-pref"})
assert not _is_unsub_link({"text": "查看详情", "url": "https://x.com/deal"})
def test_unsub_link_sorted_before_other_links():
"""退订链接即使不在 links 第一位也会被排到最前"""
links = [
{"text": "查看详情", "url": "https://x.com/a"},
{"text": "unsubscribe", "url": "https://x.com/b"},
]
rows = _link_buttons(links)
assert rows[0][0]["url"] == "https://x.com/b"
assert rows[1][0]["url"] == "https://x.com/a"
def test_invalid_url_filtered():
"""非法 URL(非 http、多重协议头)被过滤"""
links = [
{"text": "退订", "url": "javascript:alert(1)"},
{"text": "退订", "url": "https://x.com/https://evil.com"},
{"text": "退订", "url": "https://ok.example.com/unsub"},
]
rows = _link_buttons(links)
assert len(rows) == 1
assert rows[0][0]["url"] == "https://ok.example.com/unsub"
def test_long_unsub_url_rendered():
"""长 tracking 退订链接(如 OpenAI 的 >700 字符)必须渲染,不能被长度过滤误杀"""
url = "https://r.openai.com/asm/unsubscribe/?user_id=108370056&data=" + "x" * 700
assert len(url) > 200
links = [{"text": "取消订阅", "url": url}]
rows = _link_buttons(links)
assert len(rows) == 1
assert rows[0][0]["url"] == url
assert "🔕" in rows[0][0]["text"]
def test_oversized_url_filtered():
"""超过 2048 的 URL 仍被过滤"""
url = "https://example.com/" + "x" * 2100
links = [{"text": "退订", "url": url}]
rows = _link_buttons(links)
assert rows == []