test_telegram_outcome_buttons_1898.py 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. """The outcome prompt must arrive even when Telegram refuses its buttons (#1898).
  2. The Good / Reject affordance on Telegram is an inline URL keyboard. Telegram
  3. validates every button URL and rejects the whole ``sendMessage`` when one of
  4. them is not a URL it accepts -- which is what an install without a public
  5. ``external_url`` produces. Losing the buttons is survivable; losing the message
  6. the user is waiting for is not, and "I never got a message" is precisely the
  7. report this feature keeps drawing.
  8. """
  9. import httpx
  10. import pytest
  11. from backend.app.services.notification_service import NotificationService
  12. CONFIG = {"bot_token": "123456:AAbbCC", "chat_id": "-1002520100736"}
  13. BUTTONS = [
  14. {"text": "\U0001f44d Good", "url": "http://bambuddy.local:8000/api/v1/archives/confirm/tok/good"},
  15. {"text": "\U0001f44e Reject", "url": "http://bambuddy.local:8000/api/v1/archives/confirm/tok/reject"},
  16. ]
  17. PNG = b"\x89PNG\r\n\x1a\n"
  18. class _Client:
  19. """Stand-in for httpx.AsyncClient that rejects any send carrying buttons."""
  20. def __init__(self, reject_buttons: bool):
  21. self.is_closed = False
  22. self.reject_buttons = reject_buttons
  23. self.calls: list[dict] = []
  24. async def post(self, url, data=None, files=None, json=None):
  25. body = json if json is not None else (data or {})
  26. self.calls.append(body)
  27. if self.reject_buttons and "reply_markup" in body:
  28. return httpx.Response(
  29. 400, json={"ok": False, "description": "Bad Request: inline keyboard button URL is invalid"}
  30. )
  31. return httpx.Response(200, json={"ok": True, "result": {}})
  32. def _service(reject_buttons: bool) -> tuple[NotificationService, _Client]:
  33. service = NotificationService()
  34. client = _Client(reject_buttons)
  35. service._http_client = client # bypass real HTTP
  36. return service, client
  37. @pytest.mark.asyncio
  38. async def test_a_rejected_keyboard_falls_back_to_a_plain_message():
  39. service, client = _service(reject_buttons=True)
  40. ok, detail = await service._send_telegram(CONFIG, "*How did your print come out?*\nbody", buttons=BUTTONS)
  41. assert ok, detail
  42. assert len(client.calls) == 2, "expected one attempt with buttons and one without"
  43. assert "reply_markup" in client.calls[0]
  44. assert "reply_markup" not in client.calls[1]
  45. assert client.calls[1]["text"].endswith("body")
  46. @pytest.mark.asyncio
  47. async def test_the_photo_variant_falls_back_too():
  48. """The finish photo rides along with the prompt, so this is the shape most
  49. completed prints actually send."""
  50. service, client = _service(reject_buttons=True)
  51. ok, _ = await service._send_telegram(CONFIG, "*T*\nbody", image_data=PNG, buttons=BUTTONS)
  52. assert ok
  53. assert len(client.calls) == 2
  54. assert "reply_markup" not in client.calls[1]
  55. @pytest.mark.asyncio
  56. async def test_buttons_are_kept_when_telegram_accepts_them():
  57. service, client = _service(reject_buttons=False)
  58. ok, _ = await service._send_telegram(CONFIG, "*T*\nbody", buttons=BUTTONS)
  59. assert ok
  60. assert len(client.calls) == 1
  61. assert client.calls[0]["reply_markup"] == {"inline_keyboard": [BUTTONS]}
  62. @pytest.mark.asyncio
  63. async def test_a_buttonless_failure_is_still_reported():
  64. """The retry exists for the keyboard, not to paper over a bad token."""
  65. class _AlwaysFails(_Client):
  66. async def post(self, url, data=None, files=None, json=None):
  67. self.calls.append(json if json is not None else (data or {}))
  68. return httpx.Response(401, text="Unauthorized")
  69. service = NotificationService()
  70. client = _AlwaysFails(reject_buttons=False)
  71. service._http_client = client
  72. ok, detail = await service._send_telegram(CONFIG, "*T*\nbody")
  73. assert ok is False
  74. assert "401" in detail
  75. assert len(client.calls) == 1