test_telegram_forum_topic.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. """Tests for optional Telegram forum-topic delivery via message_thread_id (#1518).
  2. Telegram forum groups route messages to a topic by ``message_thread_id``. The
  3. field is optional: when it is absent, Telegram posts to the group's General
  4. topic, which is the behaviour every existing install already relies on.
  5. The subtlety worth pinning is the type. ``sendMessage`` is posted as JSON, and
  6. Telegram rejects a *string* thread id there, while the multipart ``sendPhoto``
  7. call would accept one. A string passed straight through would therefore work
  8. for notifications carrying a thumbnail and 400 for plain-text ones — so these
  9. tests assert an ``int`` reaches both call sites.
  10. """
  11. import httpx
  12. import pytest
  13. from backend.app.services.notification_service import NotificationService
  14. class _CaptureClient:
  15. """Stand-in for httpx.AsyncClient recording the JSON body and form data."""
  16. def __init__(self):
  17. self.is_closed = False
  18. self.calls: list[dict] = []
  19. async def post(self, url, data=None, files=None, json=None):
  20. self.calls.append({"url": url, "data": data, "files": files, "json": json})
  21. return httpx.Response(200, json={"ok": True, "result": {}})
  22. @pytest.fixture
  23. def service_with_capture():
  24. service = NotificationService()
  25. client = _CaptureClient()
  26. service._http_client = client # bypass real HTTP
  27. return service, client
  28. BASE_CONFIG = {"bot_token": "123456:AAbbCC", "chat_id": "-1002520100736"}
  29. PNG = b"\x89PNG\r\n\x1a\n"
  30. @pytest.mark.asyncio
  31. async def test_thread_id_omitted_when_unset(service_with_capture):
  32. """Default config must produce exactly the pre-#1518 payload."""
  33. service, client = service_with_capture
  34. ok, _ = await service._send_telegram(BASE_CONFIG, "*T*\nbody")
  35. assert ok
  36. assert "message_thread_id" not in client.calls[0]["json"]
  37. @pytest.mark.asyncio
  38. async def test_blank_thread_id_is_treated_as_unset(service_with_capture):
  39. """An emptied-out form field must not turn into a bogus topic."""
  40. service, client = service_with_capture
  41. ok, _ = await service._send_telegram({**BASE_CONFIG, "message_thread_id": " "}, "*T*\nbody")
  42. assert ok
  43. assert "message_thread_id" not in client.calls[0]["json"]
  44. @pytest.mark.asyncio
  45. async def test_sendmessage_carries_thread_id_as_int(service_with_capture):
  46. service, client = service_with_capture
  47. ok, _ = await service._send_telegram({**BASE_CONFIG, "message_thread_id": "25"}, "*T*\nbody")
  48. assert ok
  49. body = client.calls[0]["json"]
  50. assert body["message_thread_id"] == 25
  51. assert isinstance(body["message_thread_id"], int), "Telegram 400s on a string thread id in JSON"
  52. @pytest.mark.asyncio
  53. async def test_sendphoto_carries_thread_id(service_with_capture):
  54. """Thumbnail notifications take the multipart path and must route too."""
  55. service, client = service_with_capture
  56. ok, _ = await service._send_telegram({**BASE_CONFIG, "message_thread_id": "25"}, "*T*\nbody", image_data=PNG)
  57. assert ok
  58. call = client.calls[0]
  59. assert call["url"].endswith("/sendPhoto")
  60. assert call["data"]["message_thread_id"] == 25
  61. @pytest.mark.asyncio
  62. async def test_thread_id_accepts_native_int(service_with_capture):
  63. """config is a JSON blob — the value may already deserialise as an int."""
  64. service, client = service_with_capture
  65. ok, _ = await service._send_telegram({**BASE_CONFIG, "message_thread_id": 25}, "*T*\nbody")
  66. assert ok
  67. assert client.calls[0]["json"]["message_thread_id"] == 25
  68. @pytest.mark.asyncio
  69. async def test_non_numeric_thread_id_fails_without_sending(service_with_capture):
  70. """Reject locally rather than let Telegram answer with an opaque 400."""
  71. service, client = service_with_capture
  72. ok, error = await service._send_telegram({**BASE_CONFIG, "message_thread_id": "General"}, "*T*\nbody")
  73. assert not ok
  74. assert "not a number" in error
  75. assert client.calls == []
  76. @pytest.mark.asyncio
  77. async def test_error_message_does_not_leak_bot_token(service_with_capture):
  78. service, _ = service_with_capture
  79. ok, error = await service._send_telegram({**BASE_CONFIG, "message_thread_id": "oops"}, "*T*\nbody")
  80. assert not ok
  81. assert "AAbbCC" not in error