2026-07-15 05:45:57 +02:00
|
|
|
"""TelegramClient coverage: NullTelegramClient no-op, LiveTelegramClient calls."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
|
|
|
|
|
import httpx
|
|
|
|
|
import pytest
|
|
|
|
|
from roboco.services.telegram_client import (
|
|
|
|
|
LiveTelegramClient,
|
|
|
|
|
NullTelegramClient,
|
|
|
|
|
build_telegram_client,
|
|
|
|
|
)
|
|
|
|
|
from roboco.services.telegram_credentials import TelegramCredentialsData
|
|
|
|
|
|
|
|
|
|
_CREDS = TelegramCredentialsData(bot_token="123456:ABC", chat_id="987654321")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_null_client_is_unconfigured() -> None:
|
|
|
|
|
client = build_telegram_client(None, timeout=5.0)
|
|
|
|
|
assert isinstance(client, NullTelegramClient)
|
|
|
|
|
assert client.configured is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_null_client_send_message_is_a_noop() -> None:
|
|
|
|
|
client: NullTelegramClient = NullTelegramClient()
|
|
|
|
|
result = await client.send_message("hello")
|
|
|
|
|
assert result.sent is False
|
|
|
|
|
assert result.detail # non-empty reason
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_telegram_client_with_creds_returns_live_client() -> None:
|
|
|
|
|
client = build_telegram_client(_CREDS, timeout=5.0)
|
|
|
|
|
assert isinstance(client, LiveTelegramClient)
|
|
|
|
|
assert client.configured is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_live_client_send_message_success() -> None:
|
|
|
|
|
def handler(request: httpx.Request) -> httpx.Response:
|
|
|
|
|
assert request.url.path == "/bot123456:ABC/sendMessage"
|
|
|
|
|
body = json.loads(request.content.decode())
|
|
|
|
|
assert body == {"chat_id": _CREDS.chat_id, "text": "hi"}
|
|
|
|
|
return httpx.Response(200, json={"ok": True, "result": {"message_id": 1}})
|
|
|
|
|
|
|
|
|
|
transport = httpx.MockTransport(handler)
|
|
|
|
|
http_client = httpx.AsyncClient(transport=transport)
|
|
|
|
|
client = LiveTelegramClient(_CREDS, timeout=5.0, client=http_client)
|
|
|
|
|
result = await client.send_message("hi")
|
|
|
|
|
assert result.sent is True
|
|
|
|
|
await client.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_live_client_send_message_http_error_is_graceful() -> None:
|
|
|
|
|
def handler(_request: httpx.Request) -> httpx.Response:
|
|
|
|
|
return httpx.Response(401, text="unauthorized")
|
|
|
|
|
|
|
|
|
|
transport = httpx.MockTransport(handler)
|
|
|
|
|
http_client = httpx.AsyncClient(transport=transport)
|
|
|
|
|
client = LiveTelegramClient(_CREDS, timeout=5.0, client=http_client)
|
|
|
|
|
result = await client.send_message("hi")
|
|
|
|
|
assert result.sent is False
|
|
|
|
|
assert "401" in result.detail
|
|
|
|
|
await client.close()
|
2026-07-18 00:47:09 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
_REPLY_TO_MESSAGE_ID = 42
|
|
|
|
|
_SENT_MESSAGE_ID = 7
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_live_client_send_message_with_reply_markup_and_message_id() -> None:
|
|
|
|
|
def handler(request: httpx.Request) -> httpx.Response:
|
|
|
|
|
body = json.loads(request.content.decode())
|
|
|
|
|
assert body["reply_markup"] == {"force_reply": True}
|
|
|
|
|
assert body["reply_to_message_id"] == _REPLY_TO_MESSAGE_ID
|
|
|
|
|
return httpx.Response(
|
|
|
|
|
200, json={"ok": True, "result": {"message_id": _SENT_MESSAGE_ID}}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
transport = httpx.MockTransport(handler)
|
|
|
|
|
http_client = httpx.AsyncClient(transport=transport)
|
|
|
|
|
client = LiveTelegramClient(_CREDS, timeout=5.0, client=http_client)
|
|
|
|
|
result = await client.send_message(
|
|
|
|
|
"hi",
|
|
|
|
|
reply_markup={"force_reply": True},
|
|
|
|
|
reply_to_message_id=_REPLY_TO_MESSAGE_ID,
|
|
|
|
|
)
|
|
|
|
|
assert result.sent is True
|
|
|
|
|
assert result.message_id == _SENT_MESSAGE_ID
|
|
|
|
|
await client.close()
|
|
|
|
|
|
|
|
|
|
|
2026-07-18 15:54:49 +02:00
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_live_client_send_message_with_formatting_fields() -> None:
|
|
|
|
|
def handler(request: httpx.Request) -> httpx.Response:
|
|
|
|
|
body = json.loads(request.content.decode())
|
|
|
|
|
assert body["parse_mode"] == "HTML"
|
|
|
|
|
assert body["link_preview_options"] == {"is_disabled": True}
|
|
|
|
|
return httpx.Response(200, json={"ok": True, "result": {"message_id": 1}})
|
|
|
|
|
|
|
|
|
|
transport = httpx.MockTransport(handler)
|
|
|
|
|
http_client = httpx.AsyncClient(transport=transport)
|
|
|
|
|
client = LiveTelegramClient(_CREDS, timeout=5.0, client=http_client)
|
|
|
|
|
result = await client.send_message(
|
|
|
|
|
"<b>hi</b>", parse_mode="HTML", disable_link_preview=True
|
|
|
|
|
)
|
|
|
|
|
assert result.sent is True
|
|
|
|
|
await client.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_live_client_send_message_omits_formatting_fields_when_unset() -> None:
|
|
|
|
|
def handler(request: httpx.Request) -> httpx.Response:
|
|
|
|
|
body = json.loads(request.content.decode())
|
|
|
|
|
assert "parse_mode" not in body
|
|
|
|
|
assert "link_preview_options" not in body
|
|
|
|
|
return httpx.Response(200, json={"ok": True, "result": {"message_id": 1}})
|
|
|
|
|
|
|
|
|
|
transport = httpx.MockTransport(handler)
|
|
|
|
|
http_client = httpx.AsyncClient(transport=transport)
|
|
|
|
|
client = LiveTelegramClient(_CREDS, timeout=5.0, client=http_client)
|
|
|
|
|
result = await client.send_message("hi")
|
|
|
|
|
assert result.sent is True
|
|
|
|
|
await client.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_live_client_edit_message_text_with_parse_mode() -> None:
|
|
|
|
|
def handler(request: httpx.Request) -> httpx.Response:
|
|
|
|
|
assert request.url.path == "/bot123456:ABC/editMessageText"
|
|
|
|
|
body = json.loads(request.content.decode())
|
|
|
|
|
assert body["text"] == "<b>done</b>"
|
|
|
|
|
assert body["parse_mode"] == "HTML"
|
|
|
|
|
assert body["link_preview_options"] == {"is_disabled": True}
|
|
|
|
|
return httpx.Response(200, json={"ok": True, "result": True})
|
|
|
|
|
|
|
|
|
|
transport = httpx.MockTransport(handler)
|
|
|
|
|
http_client = httpx.AsyncClient(transport=transport)
|
|
|
|
|
client = LiveTelegramClient(_CREDS, timeout=5.0, client=http_client)
|
|
|
|
|
await client.edit_message_text(
|
|
|
|
|
7, "<b>done</b>", parse_mode="HTML", disable_link_preview=True
|
|
|
|
|
)
|
|
|
|
|
await client.close()
|
|
|
|
|
|
|
|
|
|
|
2026-07-18 00:47:09 +02:00
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_live_client_get_updates_success() -> None:
|
|
|
|
|
def handler(request: httpx.Request) -> httpx.Response:
|
|
|
|
|
assert request.url.path == "/bot123456:ABC/getUpdates"
|
|
|
|
|
assert request.url.params["timeout"] == "25"
|
|
|
|
|
assert request.url.params["offset"] == "10"
|
|
|
|
|
return httpx.Response(
|
|
|
|
|
200, json={"ok": True, "result": [{"update_id": 10}, {"update_id": 11}]}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
transport = httpx.MockTransport(handler)
|
|
|
|
|
http_client = httpx.AsyncClient(transport=transport)
|
|
|
|
|
client = LiveTelegramClient(_CREDS, timeout=5.0, client=http_client)
|
|
|
|
|
updates = await client.get_updates(offset=10, timeout=25, limit=50)
|
|
|
|
|
assert [u["update_id"] for u in updates] == [10, 11]
|
|
|
|
|
await client.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_live_client_get_updates_network_error_returns_empty() -> None:
|
|
|
|
|
def handler(_request: httpx.Request) -> httpx.Response:
|
|
|
|
|
raise httpx.ConnectError("boom")
|
|
|
|
|
|
|
|
|
|
transport = httpx.MockTransport(handler)
|
|
|
|
|
http_client = httpx.AsyncClient(transport=transport)
|
|
|
|
|
client = LiveTelegramClient(_CREDS, timeout=5.0, client=http_client)
|
|
|
|
|
updates = await client.get_updates(offset=None, timeout=25, limit=50)
|
|
|
|
|
assert updates == []
|
|
|
|
|
await client.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_live_client_answer_callback_query_posts_id() -> None:
|
|
|
|
|
def handler(request: httpx.Request) -> httpx.Response:
|
|
|
|
|
assert request.url.path == "/bot123456:ABC/answerCallbackQuery"
|
|
|
|
|
body = json.loads(request.content.decode())
|
|
|
|
|
assert body == {"callback_query_id": "cq1", "text": "ok"}
|
|
|
|
|
return httpx.Response(200, json={"ok": True, "result": True})
|
|
|
|
|
|
|
|
|
|
transport = httpx.MockTransport(handler)
|
|
|
|
|
http_client = httpx.AsyncClient(transport=transport)
|
|
|
|
|
client = LiveTelegramClient(_CREDS, timeout=5.0, client=http_client)
|
|
|
|
|
await client.answer_callback_query("cq1", "ok")
|
|
|
|
|
await client.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_live_client_edit_message_reply_markup_clears_keyboard() -> None:
|
|
|
|
|
def handler(request: httpx.Request) -> httpx.Response:
|
|
|
|
|
assert request.url.path == "/bot123456:ABC/editMessageReplyMarkup"
|
|
|
|
|
body = json.loads(request.content.decode())
|
|
|
|
|
assert body["reply_markup"] == {}
|
|
|
|
|
return httpx.Response(200, json={"ok": True, "result": True})
|
|
|
|
|
|
|
|
|
|
transport = httpx.MockTransport(handler)
|
|
|
|
|
http_client = httpx.AsyncClient(transport=transport)
|
|
|
|
|
client = LiveTelegramClient(_CREDS, timeout=5.0, client=http_client)
|
|
|
|
|
await client.edit_message_reply_markup(7, None)
|
|
|
|
|
await client.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_null_client_v2_methods_are_all_noops() -> None:
|
|
|
|
|
client: NullTelegramClient = NullTelegramClient()
|
|
|
|
|
assert await client.get_updates(offset=None, timeout=25, limit=50) == []
|
|
|
|
|
# None of these raise — that's the whole contract.
|
|
|
|
|
await client.answer_callback_query("cq1", "text")
|
|
|
|
|
await client.edit_message_reply_markup(1, None)
|
2026-07-18 15:54:49 +02:00
|
|
|
await client.edit_message_text(1, "done", parse_mode="HTML")
|
|
|
|
|
await client.send_message("hi", parse_mode="HTML", disable_link_preview=True)
|