mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-20 11:50:27 +00:00
339 lines
10 KiB
Python
339 lines
10 KiB
Python
import json
|
|
from typing import Any
|
|
from unittest.mock import AsyncMock
|
|
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
from starlette.requests import Request
|
|
|
|
from app.config import settings
|
|
from app.webserver.telegram import (
|
|
TelegramWebhookProcessor,
|
|
create_telegram_router,
|
|
)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def reset_webhook_settings(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(settings, "WEBHOOK_PATH", "/telegram-webhook", raising=False)
|
|
monkeypatch.setattr(settings, "WEBHOOK_SECRET_TOKEN", "", raising=False)
|
|
monkeypatch.setattr(settings, "WEBHOOK_URL", None, raising=False)
|
|
monkeypatch.setattr(settings, "BOT_RUN_MODE", "webhook", raising=False)
|
|
monkeypatch.setattr(settings, "WEBHOOK_MAX_QUEUE_SIZE", 8, raising=False)
|
|
monkeypatch.setattr(settings, "WEBHOOK_WORKERS", 1, raising=False)
|
|
monkeypatch.setattr(settings, "WEBHOOK_ENQUEUE_TIMEOUT", 0.0, raising=False)
|
|
monkeypatch.setattr(settings, "WEBHOOK_WORKER_SHUTDOWN_TIMEOUT", 1.0, raising=False)
|
|
|
|
|
|
def _get_route(router, path: str, method: str = "POST"):
|
|
for route in router.routes:
|
|
if getattr(route, "path", "") == path and method in getattr(route, "methods", set()):
|
|
return route
|
|
raise AssertionError(f"Route {path} with method {method} not found")
|
|
|
|
|
|
def _build_request(path: str, body: bytes, headers: dict[str, str] | None = None) -> Request:
|
|
scope = {
|
|
"type": "http",
|
|
"asgi": {"version": "3.0"},
|
|
"method": "POST",
|
|
"path": path,
|
|
"headers": [
|
|
(k.lower().encode("latin-1"), v.encode("latin-1")) for k, v in (headers or {}).items()
|
|
],
|
|
}
|
|
|
|
async def receive() -> dict[str, Any]:
|
|
return {"type": "http.request", "body": body, "more_body": False}
|
|
|
|
return Request(scope, receive)
|
|
|
|
|
|
def _webhook_path() -> str:
|
|
return settings.get_telegram_webhook_path()
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_webhook_without_secret() -> None:
|
|
bot = AsyncMock()
|
|
dispatcher = AsyncMock()
|
|
dispatcher.feed_update = AsyncMock()
|
|
|
|
sample_update = {
|
|
"update_id": 123,
|
|
"message": {
|
|
"message_id": 10,
|
|
"date": 1715700000,
|
|
"chat": {"id": 456, "type": "private"},
|
|
"text": "ping",
|
|
},
|
|
}
|
|
|
|
router = create_telegram_router(bot, dispatcher)
|
|
path = _webhook_path()
|
|
route = _get_route(router, path)
|
|
request = _build_request(path, json.dumps(sample_update).encode("utf-8"))
|
|
|
|
response = await route.endpoint(request)
|
|
|
|
assert response.status_code == 200
|
|
dispatcher.feed_update.assert_awaited_once()
|
|
args, _kwargs = dispatcher.feed_update.await_args
|
|
assert args[0] is bot
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_webhook_with_secret(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
bot = AsyncMock()
|
|
dispatcher = AsyncMock()
|
|
dispatcher.feed_update = AsyncMock()
|
|
|
|
monkeypatch.setattr(settings, "WEBHOOK_SECRET_TOKEN", "super-secret", raising=False)
|
|
|
|
sample_update = {
|
|
"update_id": 321,
|
|
"message": {
|
|
"message_id": 20,
|
|
"date": 1715700000,
|
|
"chat": {"id": 789, "type": "private"},
|
|
"text": "pong",
|
|
},
|
|
}
|
|
|
|
router = create_telegram_router(bot, dispatcher)
|
|
path = _webhook_path()
|
|
route = _get_route(router, path)
|
|
request = _build_request(
|
|
path,
|
|
json.dumps(sample_update).encode("utf-8"),
|
|
headers={"X-Telegram-Bot-Api-Secret-Token": "super-secret"},
|
|
)
|
|
|
|
response = await route.endpoint(request)
|
|
|
|
assert response.status_code == 200
|
|
dispatcher.feed_update.assert_awaited_once()
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_webhook_secret_mismatch(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
bot = AsyncMock()
|
|
dispatcher = AsyncMock()
|
|
dispatcher.feed_update = AsyncMock()
|
|
|
|
monkeypatch.setattr(settings, "WEBHOOK_SECRET_TOKEN", "expected", raising=False)
|
|
|
|
router = create_telegram_router(bot, dispatcher)
|
|
path = _webhook_path()
|
|
route = _get_route(router, path)
|
|
request = _build_request(
|
|
path,
|
|
json.dumps({"update_id": 1}).encode("utf-8"),
|
|
headers={"X-Telegram-Bot-Api-Secret-Token": "wrong"},
|
|
)
|
|
|
|
with pytest.raises(HTTPException) as exc:
|
|
await route.endpoint(request)
|
|
|
|
assert exc.value.status_code == 401
|
|
dispatcher.feed_update.assert_not_called()
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_webhook_invalid_payload() -> None:
|
|
bot = AsyncMock()
|
|
dispatcher = AsyncMock()
|
|
dispatcher.feed_update = AsyncMock()
|
|
|
|
router = create_telegram_router(bot, dispatcher)
|
|
path = _webhook_path()
|
|
route = _get_route(router, path)
|
|
request = _build_request(path, b"not-json")
|
|
|
|
with pytest.raises(HTTPException) as exc:
|
|
await route.endpoint(request)
|
|
|
|
assert exc.value.status_code == 400
|
|
dispatcher.feed_update.assert_not_called()
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_webhook_invalid_content_type() -> None:
|
|
bot = AsyncMock()
|
|
dispatcher = AsyncMock()
|
|
dispatcher.feed_update = AsyncMock()
|
|
|
|
sample_update = {
|
|
"update_id": 123,
|
|
"message": {
|
|
"message_id": 10,
|
|
"date": 1715700000,
|
|
"chat": {"id": 456, "type": "private"},
|
|
"text": "ping",
|
|
},
|
|
}
|
|
|
|
router = create_telegram_router(bot, dispatcher)
|
|
path = _webhook_path()
|
|
route = _get_route(router, path)
|
|
request = _build_request(
|
|
path,
|
|
json.dumps(sample_update).encode("utf-8"),
|
|
headers={"Content-Type": "text/plain"},
|
|
)
|
|
|
|
with pytest.raises(HTTPException) as exc:
|
|
await route.endpoint(request)
|
|
|
|
assert exc.value.status_code == 415
|
|
dispatcher.feed_update.assert_not_called()
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_webhook_uses_processor() -> None:
|
|
bot = AsyncMock()
|
|
dispatcher = AsyncMock()
|
|
dispatcher.feed_update = AsyncMock()
|
|
|
|
processor = TelegramWebhookProcessor(
|
|
bot=bot,
|
|
dispatcher=dispatcher,
|
|
queue_maxsize=1,
|
|
worker_count=0,
|
|
enqueue_timeout=0.0,
|
|
shutdown_timeout=1.0,
|
|
)
|
|
await processor.start()
|
|
|
|
sample_update = {
|
|
"update_id": 999,
|
|
"message": {
|
|
"message_id": 77,
|
|
"date": 1715700000,
|
|
"chat": {"id": 111, "type": "private"},
|
|
"text": "processor",
|
|
},
|
|
}
|
|
|
|
router = create_telegram_router(bot, dispatcher, processor=processor)
|
|
path = _webhook_path()
|
|
route = _get_route(router, path)
|
|
request = _build_request(path, json.dumps(sample_update).encode("utf-8"))
|
|
|
|
response = await route.endpoint(request)
|
|
|
|
assert response.status_code == 200
|
|
dispatcher.feed_update.assert_not_awaited()
|
|
assert processor.is_running
|
|
|
|
await processor.stop()
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_webhook_processor_overloaded() -> None:
|
|
bot = AsyncMock()
|
|
dispatcher = AsyncMock()
|
|
dispatcher.feed_update = AsyncMock()
|
|
|
|
processor = TelegramWebhookProcessor(
|
|
bot=bot,
|
|
dispatcher=dispatcher,
|
|
queue_maxsize=1,
|
|
worker_count=0,
|
|
enqueue_timeout=0.0,
|
|
shutdown_timeout=1.0,
|
|
)
|
|
await processor.start()
|
|
|
|
router = create_telegram_router(bot, dispatcher, processor=processor)
|
|
path = _webhook_path()
|
|
route = _get_route(router, path)
|
|
|
|
request_payload = json.dumps({"update_id": 1}).encode("utf-8")
|
|
request = _build_request(path, request_payload)
|
|
await route.endpoint(request)
|
|
|
|
with pytest.raises(HTTPException) as exc:
|
|
await route.endpoint(request)
|
|
|
|
assert exc.value.status_code == 503
|
|
assert exc.value.detail == "webhook_queue_full"
|
|
dispatcher.feed_update.assert_not_called()
|
|
|
|
await processor.stop()
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_webhook_processor_not_running() -> None:
|
|
bot = AsyncMock()
|
|
dispatcher = AsyncMock()
|
|
dispatcher.feed_update = AsyncMock()
|
|
|
|
processor = TelegramWebhookProcessor(
|
|
bot=bot,
|
|
dispatcher=dispatcher,
|
|
queue_maxsize=1,
|
|
worker_count=1,
|
|
enqueue_timeout=0.0,
|
|
shutdown_timeout=1.0,
|
|
)
|
|
|
|
router = create_telegram_router(bot, dispatcher, processor=processor)
|
|
path = _webhook_path()
|
|
route = _get_route(router, path)
|
|
request = _build_request(path, json.dumps({"update_id": 5}).encode("utf-8"))
|
|
|
|
with pytest.raises(HTTPException) as exc:
|
|
await route.endpoint(request)
|
|
|
|
assert exc.value.status_code == 503
|
|
assert exc.value.detail == "webhook_processor_unavailable"
|
|
dispatcher.feed_update.assert_not_called()
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_webhook_path_normalization(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
bot = AsyncMock()
|
|
dispatcher = AsyncMock()
|
|
dispatcher.feed_update = AsyncMock()
|
|
|
|
monkeypatch.setattr(settings, "WEBHOOK_PATH", " telegram/webhook ", raising=False)
|
|
|
|
router = create_telegram_router(bot, dispatcher)
|
|
normalized_path = settings.get_telegram_webhook_path()
|
|
|
|
assert normalized_path == "/telegram/webhook"
|
|
route = _get_route(router, normalized_path)
|
|
|
|
request = _build_request(normalized_path, json.dumps({"update_id": 7}).encode("utf-8"))
|
|
response = await route.endpoint(request)
|
|
|
|
assert response.status_code == 200
|
|
dispatcher.feed_update.assert_awaited_once()
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_health_endpoint(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
bot = AsyncMock()
|
|
dispatcher = AsyncMock()
|
|
dispatcher.feed_update = AsyncMock()
|
|
|
|
monkeypatch.setattr(settings, "WEBHOOK_URL", "https://example.com", raising=False)
|
|
monkeypatch.setattr(settings, "WEBHOOK_PATH", "/custom", raising=False)
|
|
monkeypatch.setattr(settings, "WEBHOOK_MAX_QUEUE_SIZE", 42, raising=False)
|
|
monkeypatch.setattr(settings, "WEBHOOK_WORKERS", 2, raising=False)
|
|
|
|
router = create_telegram_router(bot, dispatcher)
|
|
route = _get_route(router, "/health/telegram-webhook", method="GET")
|
|
|
|
response = await route.endpoint()
|
|
|
|
assert response.status_code == 200
|
|
payload = json.loads(response.body.decode("utf-8"))
|
|
assert payload["status"] == "ok"
|
|
assert payload["mode"] == settings.get_bot_run_mode()
|
|
assert payload["path"] == "/custom"
|
|
assert payload["webhook_configured"] is True
|
|
assert payload["queue_maxsize"] == 42
|
|
assert payload["workers"] == 2
|