"""提醒送达与到期文案测试 覆盖两处回归: 1. Telegram 不能带 parse_mode=HTML:消息里嵌的是资产名/平台名等用户数据, 含 < > & 时整条消息会被 Telegram 拒收(400),提醒静默发不出去; 失败时还必须把响应正文带回,否则只看到状态码根本无从排查。 2. days 可以为负(已过期),文案统一走 renewal_service.day_text, 不能直出「-37 天后到期」;前端对应 Fmt.countdownText,两边一致。 运行:.venv/bin/pytest tests/test_notification.py -v """ import httpx import pytest from app.core.config import settings from app.services import notification_service, renewal_service class _OkResp: status_code = 200 text = "ok" def raise_for_status(self): return None def test_send_telegram_omits_parse_mode(monkeypatch): """用户数据里的 < > & 不能让整条提醒被拒收""" captured = {} def fake_post(url, json=None, timeout=None): captured["url"] = url captured["payload"] = json return _OkResp() monkeypatch.setattr(notification_service.httpx, "post", fake_post) monkeypatch.setattr(settings, "TELEGRAM_BOT_TOKEN", "tok") monkeypatch.setattr(settings, "TELEGRAM_CHAT_ID", "123") text = "⏰ 资产续费提醒\n• <测试&资产> [a&b] (已过期 3 天) 到期 2026-08-15" result = notification_service.send_telegram(text) assert result["ok"] is True # 关键:不再声明 HTML 解析模式,纯文本原样送达 assert "parse_mode" not in captured["payload"] assert captured["payload"]["text"] == text assert captured["payload"]["chat_id"] == "123" assert captured["payload"]["disable_web_page_preview"] is True def test_send_telegram_failure_returns_response_body(monkeypatch): """拒收原因在响应体里,只留状态码无法排查""" request = httpx.Request("POST", "https://api.telegram.org/bottok/sendMessage") response = httpx.Response(400, request=request, text='{"description":"chat not found"}') def fake_post(url, json=None, timeout=None): raise httpx.HTTPStatusError("400 Bad Request", request=request, response=response) monkeypatch.setattr(notification_service.httpx, "post", fake_post) monkeypatch.setattr(settings, "TELEGRAM_BOT_TOKEN", "tok") monkeypatch.setattr(settings, "TELEGRAM_CHAT_ID", "123") result = notification_service.send_telegram("x") assert result["ok"] is False assert result["channel"] == "telegram" assert "400" in result["message"] assert "chat not found" in result["message"] def test_send_telegram_skips_when_unconfigured(monkeypatch): monkeypatch.setattr(settings, "TELEGRAM_BOT_TOKEN", "") result = notification_service.send_telegram("x") assert result["ok"] is False assert "未配置" in result["message"] @pytest.mark.parametrize( "days,expected", [(-37, "已过期 37 天"), (-1, "已过期 1 天"), (0, "今天到期"), (1, "1 天后"), (30, "30 天后")], ) def test_day_text_never_prints_negative(days, expected): assert renewal_service.day_text(days) == expected class _FakeAsset: """build_renewal_message 只用到 name / provider / expiry_date""" def __init__(self, name, provider, expiry_date): self.name = name self.provider = provider self.expiry_date = expiry_date def test_renewal_message_separates_overdue(): """已过期与即将到期分开计数,且不出现「-37 天后」这类文案""" msg = renewal_service.build_renewal_message( [ (_FakeAsset("ChatGPT Plus", "openai", "2026-08-15"), -37), (_FakeAsset("vps-1", "vultr", "2026-10-01"), 11), ], threshold_days=30, ) assert "已过期 1 项" in msg assert "30 天内到期 1 项" in msg assert "(已过期 37 天)" in msg assert "(11 天后)" in msg assert "-37" not in msg def test_renewal_message_empty_when_nothing_expiring(): assert renewal_service.build_renewal_message([], threshold_days=30) == ""