Files
checkbot/tests/test_telegram.py
T
patrickandClaude Sonnet 5.5 cdcf7f673a
test / pytest (3.11) (push) Waiting to run
test / pytest (3.13) (push) Waiting to run
fix: Härtung nach Referenz-Review, Phase-6d-Fallbacks vermerkt
Telegram: Plain-Text-Fallback bei Parse-Fehler, tag-sichere Kürzung auf 4096.
Matrix: bytebewusste Kürzung. Downtime: lesender Abgleich nach dem POST,
generische Fehlermeldung im Chat. Agent: Platzhalter für nicht ausgeführte
Tool-Calls, Text im letzten Schritt erzwingen, Zeitreserve, Kompaktierung,
Limit pro Antwort. CLAUDE.md: Fallbacks für Phase 6d.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 11:58:54 +01:00

188 lines
5.8 KiB
Python

import json
import httpx
import pytest
from app.chat import MessageRef, TelegramAdapter, TelegramError, mask_secret
from app.chat.telegram import MESSAGE_MAX, truncate_html
TOKEN = "123456:SECRET-token_abc"
BASE = "https://api.telegram.org"
def make(handler, **kwargs):
return TelegramAdapter(TOKEN, "42", transport=httpx.MockTransport(handler), **kwargs)
def ok(result):
return httpx.Response(200, json={"ok": True, "result": result})
async def test_send_payload_and_ref():
seen = {}
def handler(request):
seen["url"] = str(request.url)
seen["body"] = json.loads(request.content)
return ok({"message_id": 7})
ref = await make(handler).send("42", "<b>x</b> &lt;")
assert seen["url"] == f"{BASE}/bot{TOKEN}/sendMessage"
assert seen["body"]["parse_mode"] == "HTML"
assert seen["body"]["chat_id"] == "42" and seen["body"]["text"] == "<b>x</b> &lt;"
assert ref == MessageRef("telegram", "42", "7")
async def test_edit_payload():
seen = {}
def handler(request):
seen["url"] = str(request.url)
seen["body"] = json.loads(request.content)
return ok({})
await make(handler).edit(MessageRef("telegram", "42", "7"), "neu")
assert seen["url"].endswith("/editMessageText")
assert seen["body"]["message_id"] == 7 and seen["body"]["parse_mode"] == "HTML"
async def test_429_retries_once_with_retry_after():
calls, slept = [], []
def handler(request):
calls.append(1)
if len(calls) == 1:
return httpx.Response(429, json={"ok": False, "description": "Too Many Requests",
"parameters": {"retry_after": 3}})
return ok({"message_id": 1})
async def fake_sleep(seconds):
slept.append(seconds)
await make(handler, sleep=fake_sleep).send("42", "x")
assert len(calls) == 2 and slept == [3.0]
async def test_429_twice_raises():
async def fake_sleep(seconds):
pass
def handler(request):
return httpx.Response(429, json={"ok": False, "description": "Too Many Requests"})
with pytest.raises(TelegramError):
await make(handler, sleep=fake_sleep).send("42", "x")
async def test_not_modified_is_ignored():
def handler(request):
return httpx.Response(400, json={"ok": False, "description": "Bad Request: message is not modified"})
await make(handler).edit(MessageRef("telegram", "42", "7"), "gleich")
async def test_other_edit_errors_raise():
def handler(request):
return httpx.Response(400, json={"ok": False, "description": "Bad Request: chat not found"})
with pytest.raises(TelegramError):
await make(handler).edit(MessageRef("telegram", "42", "7"), "x")
async def test_get_me_returns_username():
def handler(request):
return ok({"username": "checkbot_bot"})
assert await make(handler).check() == "Bot @checkbot_bot"
async def test_token_never_in_connection_error():
def handler(request):
raise httpx.ConnectError(f"cannot connect to {request.url}")
adapter = make(handler)
for call in (adapter.send("42", "x"), adapter.check(), adapter.edit(MessageRef("telegram", "42", "1"), "x")):
with pytest.raises(TelegramError) as info:
await call
assert TOKEN not in str(info.value)
assert info.value.__cause__ is None and info.value.__suppress_context__
async def test_token_never_in_http_error_description():
def handler(request):
return httpx.Response(401, json={"ok": False, "description": f"Unauthorized {TOKEN}"})
with pytest.raises(TelegramError) as info:
await make(handler).check()
assert TOKEN not in str(info.value)
def test_mask_secret_handles_url_encoded_variant():
assert TOKEN not in mask_secret(f"x {TOKEN} y", TOKEN)
assert mask_secret("a b", "") == "a b"
def test_truncate_html_keeps_short_text_unchanged():
assert truncate_html("<b>hi</b>") == "<b>hi</b>"
def test_truncate_html_closes_open_tags_and_marks_cut():
text = "<b>" + "A" * 4200
truncated = truncate_html(text)
assert len(truncated) <= MESSAGE_MAX
assert truncated.endswith("</b>…")
assert truncated.count("<b>") == 1
def test_truncate_html_does_not_split_entity():
text = "A" * (MESSAGE_MAX - 3) + "&amp;" + "B" * 20
truncated = truncate_html(text)
assert len(truncated) <= MESSAGE_MAX
assert "&am" not in truncated.replace("&amp;", "")
assert not truncated[:-1].endswith("&")
async def test_send_truncates_long_html_message():
seen = {}
def handler(request):
seen["body"] = json.loads(request.content)
return ok({"message_id": 1})
await make(handler).send("42", "<b>" + "x" * 5000 + "</b>")
assert len(seen["body"]["text"]) <= MESSAGE_MAX
assert seen["body"]["text"].endswith("</b>…")
async def test_send_retries_as_plain_text_on_cant_parse_entities():
calls = []
def handler(request):
body = json.loads(request.content)
calls.append(body)
if "parse_mode" in body:
return httpx.Response(400, json={"ok": False, "description": "Bad Request: can't parse entities"})
return ok({"message_id": 3})
ref = await make(handler).send("42", "<b>broken</b>")
assert len(calls) == 2
assert "parse_mode" not in calls[1]
assert calls[1]["text"] == "broken"
assert ref.message_id == "3"
async def test_edit_retries_as_plain_text_on_cant_parse_entities():
calls = []
def handler(request):
body = json.loads(request.content)
calls.append(body)
if "parse_mode" in body:
return httpx.Response(400, json={"ok": False, "description": "Bad Request: can't parse entities"})
return ok({})
await make(handler).edit(MessageRef("telegram", "42", "7"), "<b>broken &amp;</b>")
assert len(calls) == 2
assert "parse_mode" not in calls[1]
assert calls[1]["text"] == "broken &"