| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103 |
- """The outcome prompt must arrive even when Telegram refuses its buttons (#1898).
- The Good / Reject affordance on Telegram is an inline URL keyboard. Telegram
- validates every button URL and rejects the whole ``sendMessage`` when one of
- them is not a URL it accepts -- which is what an install without a public
- ``external_url`` produces. Losing the buttons is survivable; losing the message
- the user is waiting for is not, and "I never got a message" is precisely the
- report this feature keeps drawing.
- """
- import httpx
- import pytest
- from backend.app.services.notification_service import NotificationService
- CONFIG = {"bot_token": "123456:AAbbCC", "chat_id": "-1002520100736"}
- BUTTONS = [
- {"text": "\U0001f44d Good", "url": "http://bambuddy.local:8000/api/v1/archives/confirm/tok/good"},
- {"text": "\U0001f44e Reject", "url": "http://bambuddy.local:8000/api/v1/archives/confirm/tok/reject"},
- ]
- PNG = b"\x89PNG\r\n\x1a\n"
- class _Client:
- """Stand-in for httpx.AsyncClient that rejects any send carrying buttons."""
- def __init__(self, reject_buttons: bool):
- self.is_closed = False
- self.reject_buttons = reject_buttons
- self.calls: list[dict] = []
- async def post(self, url, data=None, files=None, json=None):
- body = json if json is not None else (data or {})
- self.calls.append(body)
- if self.reject_buttons and "reply_markup" in body:
- return httpx.Response(
- 400, json={"ok": False, "description": "Bad Request: inline keyboard button URL is invalid"}
- )
- return httpx.Response(200, json={"ok": True, "result": {}})
- def _service(reject_buttons: bool) -> tuple[NotificationService, _Client]:
- service = NotificationService()
- client = _Client(reject_buttons)
- service._http_client = client # bypass real HTTP
- return service, client
- @pytest.mark.asyncio
- async def test_a_rejected_keyboard_falls_back_to_a_plain_message():
- service, client = _service(reject_buttons=True)
- ok, detail = await service._send_telegram(CONFIG, "*How did your print come out?*\nbody", buttons=BUTTONS)
- assert ok, detail
- assert len(client.calls) == 2, "expected one attempt with buttons and one without"
- assert "reply_markup" in client.calls[0]
- assert "reply_markup" not in client.calls[1]
- assert client.calls[1]["text"].endswith("body")
- @pytest.mark.asyncio
- async def test_the_photo_variant_falls_back_too():
- """The finish photo rides along with the prompt, so this is the shape most
- completed prints actually send."""
- service, client = _service(reject_buttons=True)
- ok, _ = await service._send_telegram(CONFIG, "*T*\nbody", image_data=PNG, buttons=BUTTONS)
- assert ok
- assert len(client.calls) == 2
- assert "reply_markup" not in client.calls[1]
- @pytest.mark.asyncio
- async def test_buttons_are_kept_when_telegram_accepts_them():
- service, client = _service(reject_buttons=False)
- ok, _ = await service._send_telegram(CONFIG, "*T*\nbody", buttons=BUTTONS)
- assert ok
- assert len(client.calls) == 1
- assert client.calls[0]["reply_markup"] == {"inline_keyboard": [BUTTONS]}
- @pytest.mark.asyncio
- async def test_a_buttonless_failure_is_still_reported():
- """The retry exists for the keyboard, not to paper over a bad token."""
- class _AlwaysFails(_Client):
- async def post(self, url, data=None, files=None, json=None):
- self.calls.append(json if json is not None else (data or {}))
- return httpx.Response(401, text="Unauthorized")
- service = NotificationService()
- client = _AlwaysFails(reject_buttons=False)
- service._http_client = client
- ok, detail = await service._send_telegram(CONFIG, "*T*\nbody")
- assert ok is False
- assert "401" in detail
- assert len(client.calls) == 1
|