diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d64bf3..5ed049a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +- The inbound telegram simulation delivers a complete `Update` (`update_id`, + `message_id`, `date`, `from` and a typed `chat`), so clients that parse it + with `Update.de_json` no longer fail on the webhook they just received. + ## [0.2.0] - 2026-08-13 ### Added diff --git a/mockpost/routers/telegram.py b/mockpost/routers/telegram.py index 8aada01..3448ad1 100644 --- a/mockpost/routers/telegram.py +++ b/mockpost/routers/telegram.py @@ -38,6 +38,7 @@ TOKEN_RE = re.compile(r"^\d+:[A-Za-z0-9_-]{35}$") _message_ids = count(1000) +_update_ids = count(1) class ClientMessage(BaseModel): @@ -70,6 +71,29 @@ def chat_of(chat_id: Any) -> dict: return {"id": numeric, "type": "supergroup" if numeric < 0 else "private"} +def inbound_update(chat_id: int | str, text: str, from_user: str | None, + message_id_seed: str) -> dict: + """A complete Update, the shape every Bot API client deserializes. + + Clients do not read the raw dict: python-telegram-bot calls + `Update.de_json`, which needs `update_id` and, inside `message`, + `message_id`, `date`, `chat` and `from`. Delivering a partial body made + those clients raise on the webhook they had just received.""" + numeric_seed = abs(hash(message_id_seed)) % 1_000_000 + return { + "update_id": next(_update_ids), + "message": { + "message_id": numeric_seed, + "date": int(time.time()), + "chat": chat_of(chat_id), + "from": {"id": numeric_seed, "is_bot": False, + "first_name": from_user or "Simulated", + "username": from_user or "simulated"}, + "text": text, + }, + } + + def message_object(token: str, chat_id: Any, **content) -> dict: return {"message_id": next(_message_ids), "from": bot_user(token), "chat": chat_of(chat_id), "date": int(time.time()), **content} @@ -287,7 +311,8 @@ async def client_send_message(payload: ClientMessage, request: Request): raw_payload=payload.model_dump(), status="received", test_id=test_id, app_id=app_id, ) results = await deliver_to_app_webhooks( - "telegram", "message", {"message": {"chat": {"id": payload.chat_id}, "text": payload.text}}, + "telegram", "message", + inbound_update(payload.chat_id, payload.text, payload.from_user, msg_id), test_id=test_id, app_id=app_id, ) return {"ok": True, "message_id": msg_id, "app_id": app_id, "webhooks_delivered": results} diff --git a/tests/test_telegram.py b/tests/test_telegram.py index ec64fa1..1e4cbb6 100644 --- a/tests/test_telegram.py +++ b/tests/test_telegram.py @@ -111,3 +111,23 @@ def test_get_updates_returns_simulated_inbound_messages(client): updates = client.post(f"/telegram/bot{TOKEN}/getUpdates").json()["result"] assert updates[0]["message"]["text"] == "from a user" assert updates[0]["message"]["chat"]["id"] == 555 + + +def test_inbound_simulation_delivers_a_complete_update(client): + """The delivered body must be a full Update: clients deserialize it with + Update.de_json, which needs update_id, message_id, date, chat and from.""" + client.post("/api/apps", json={"name": "bot-app", "creds": {"bot_token": TOKEN}}) + client.post("/api/webhooks", json={"channel": "telegram", "app": "bot-app", + "target_url": "http://127.0.0.1:9/tg"}) + client.post("/telegram/client/sendMessage", + headers={"X-MockPost-App": "bot-app"}, + json={"chat_id": 778899, "text": "/start ABC12345", "from_user": "tester"}) + + delivery = client.get("/api/webhooks/deliveries", params={"channel": "telegram"}).json()[0] + update = json.loads(delivery["payload"]) + assert update["update_id"] >= 1 + message = update["message"] + assert message["message_id"] and message["date"] + assert message["chat"] == {"id": 778899, "type": "private"} + assert message["from"]["username"] == "tester" + assert message["text"] == "/start ABC12345"