"""测试摘要键盘渲染:推广邮件的退订链接按钮必须显示且排最前。""" from src.tg_bot import ( _ensure_unsub_link, _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 == [] def test_mailto_unsub_link_rendered(): """mailto: 退订链接(如 InferX 的 mailto:team@inferx.net?subject=Unsubscribe)必须渲染""" links = [{"text": "退订", "url": "mailto:team@inferx.net?subject=Unsubscribe"}] kb = _summary_keyboard(can_reply=False, is_promotion=True, links=links) rows = kb["inline_keyboard"] assert rows[0][0]["url"] == "mailto:team@inferx.net?subject=Unsubscribe" assert "🔕" in rows[0][0]["text"] assert _is_unsub_link(links[0]) def test_ensure_unsub_link_http_header_fallback(): """AI 没提取到退订链接时,用 List-Unsubscribe 头的 http 链接兜底""" header = "https://links.iterable.com/s/uh/tKjpy4SHCKvbzE2votNyZEJDvlMdeodVyWFExNs310ugJ_RvZtoZq4gGLQ0BXMSL2lf3kEZsmr1XJmrODIFHpEwcypo3LR096pZ7jsfBynZBgGWz171Wc1E6ZIIC5Lu0e-Stq6p5-_Ad3CnNlUWrW1tnrLSeiGsOiQG_zMYKVbtfQZgvfm3QYfD9lQ/Cey29KU8yOzIUltaEJA8EMRFf-FKMZQy/24" links = _ensure_unsub_link([], header) assert links == [{"text": "退订", "url": header}] kb = _summary_keyboard(can_reply=False, is_promotion=True, links=links) assert kb["inline_keyboard"][0][0]["url"] == header def test_ensure_unsub_link_mailto_header_fallback(): """List-Unsubscribe 头只有 mailto 时也兜底""" header = "mailto:unsubscribe+19693436+25822730@unsubscribe.iterable.com" links = _ensure_unsub_link([], header) assert links == [{"text": "退订", "url": header}] def test_ensure_unsub_link_no_duplicate_when_ai_extracted(): """AI 已提取退订链接时,不重复添加头部链接""" ai_links = [{"text": "取消订阅", "url": "https://r.openai.com/asm/unsubscribe/?x=1"}] links = _ensure_unsub_link(ai_links, "https://links.example.com/unsub") assert links == ai_links def test_ensure_unsub_link_ignores_invalid_header(): """头部无有效链接时不兜底""" assert _ensure_unsub_link([], "") == [] assert _ensure_unsub_link([], "javascript:alert(1)") == []