mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-23 12:53:41 +00:00
- Add pyproject.toml with uv and ruff configuration - Pin Python version to 3.13 via .python-version - Add Makefile commands: lint, format, fix - Apply ruff formatting to entire codebase - Remove unused imports (base64 in yookassa/simple_subscription) - Update .gitignore for new config files
337 lines
10 KiB
Python
337 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
|