mirror of
https://github.com/aiogram/aiogram.git
synced 2025-12-12 02:03:04 +00:00
Webhook integration in 3.0 (#737)
* Added base webhook implementation and example * Added example * Enable on_startup callback * Correctly handle response into webhook (silent call) * Fixed State filter
This commit is contained in:
parent
e0ab7d8bd3
commit
1c2c7fd88c
13 changed files with 865 additions and 246 deletions
|
|
@ -13,6 +13,7 @@ class MockedSession(BaseSession):
|
|||
super(MockedSession, self).__init__()
|
||||
self.responses: Deque[Response[TelegramType]] = deque()
|
||||
self.requests: Deque[Request] = deque()
|
||||
self.closed = True
|
||||
|
||||
def add_result(self, response: Response[TelegramType]) -> Response[TelegramType]:
|
||||
self.responses.append(response)
|
||||
|
|
@ -22,11 +23,12 @@ class MockedSession(BaseSession):
|
|||
return self.requests.pop()
|
||||
|
||||
async def close(self):
|
||||
pass
|
||||
self.closed = True
|
||||
|
||||
async def make_request(
|
||||
self, bot: Bot, method: TelegramMethod[TelegramType], timeout: Optional[int] = UNSET
|
||||
) -> TelegramType:
|
||||
self.closed = False
|
||||
self.requests.append(method.build_request(bot))
|
||||
response: Response[TelegramType] = self.responses.pop()
|
||||
self.check_response(
|
||||
|
|
@ -45,7 +47,9 @@ class MockedBot(Bot):
|
|||
session: MockedSession
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(MockedBot, self).__init__("42:TEST", session=MockedSession(), **kwargs)
|
||||
super(MockedBot, self).__init__(
|
||||
kwargs.pop("token", "42:TEST"), session=MockedSession(), **kwargs
|
||||
)
|
||||
self._me = User(
|
||||
id=self.id,
|
||||
is_bot=True,
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ class TestDispatcher:
|
|||
async def test_silent_call_request(self, bot: MockedBot, caplog):
|
||||
dispatcher = Dispatcher()
|
||||
bot.add_result_for(SendMessage, ok=False, error_code=400, description="Kaboom")
|
||||
await dispatcher._silent_call_request(bot, SendMessage(chat_id=42, text="test"))
|
||||
await dispatcher.silent_call_request(bot, SendMessage(chat_id=42, text="test"))
|
||||
log_records = [rec.message for rec in caplog.records]
|
||||
assert len(log_records) == 1
|
||||
assert "Failed to make answer" in log_records[0]
|
||||
|
|
@ -576,7 +576,7 @@ class TestDispatcher:
|
|||
dispatcher.update.handlers.reverse()
|
||||
|
||||
with patch(
|
||||
"aiogram.dispatcher.dispatcher.Dispatcher._silent_call_request",
|
||||
"aiogram.dispatcher.dispatcher.Dispatcher.silent_call_request",
|
||||
new_callable=CoroutineMock,
|
||||
) as mocked_silent_call_request:
|
||||
result = await dispatcher._process_update(bot=bot, update=Update(update_id=42))
|
||||
|
|
@ -704,7 +704,7 @@ class TestDispatcher:
|
|||
dispatcher.message.register(simple_message_handler)
|
||||
|
||||
with patch(
|
||||
"aiogram.dispatcher.dispatcher.Dispatcher._silent_call_request",
|
||||
"aiogram.dispatcher.dispatcher.Dispatcher.silent_call_request",
|
||||
new_callable=CoroutineMock,
|
||||
) as mocked_silent_call_request:
|
||||
response = await dispatcher.feed_webhook_update(bot, RAW_UPDATE, _timeout=0.1)
|
||||
|
|
|
|||
0
tests/test_dispatcher/test_webhook/__init__.py
Normal file
0
tests/test_dispatcher/test_webhook/__init__.py
Normal file
161
tests/test_dispatcher/test_webhook/test_aiohtt_server.py
Normal file
161
tests/test_dispatcher/test_webhook/test_aiohtt_server.py
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict
|
||||
|
||||
import pytest
|
||||
from aiohttp import web
|
||||
from aiohttp.test_utils import TestClient
|
||||
from aiohttp.web_app import Application
|
||||
|
||||
from aiogram import Dispatcher, F
|
||||
from aiogram.dispatcher.webhook.aiohttp_server import (
|
||||
SimpleRequestHandler,
|
||||
TokenBasedRequestHandler,
|
||||
ip_filter_middleware,
|
||||
setup_application,
|
||||
)
|
||||
from aiogram.dispatcher.webhook.security import IPFilter
|
||||
from aiogram.methods import GetMe, Request
|
||||
from aiogram.types import Message, User
|
||||
from tests.mocked_bot import MockedBot
|
||||
|
||||
|
||||
class TestAiohttpServer:
|
||||
def test_setup_application(self):
|
||||
app = Application()
|
||||
|
||||
dp = Dispatcher()
|
||||
setup_application(app, dp)
|
||||
|
||||
assert len(app.router.routes()) == 0
|
||||
assert len(app.on_startup) == 2
|
||||
assert len(app.on_shutdown) == 1
|
||||
|
||||
async def test_middleware(self, aiohttp_client):
|
||||
app = Application()
|
||||
ip_filter = IPFilter.default()
|
||||
app.middlewares.append(ip_filter_middleware(ip_filter))
|
||||
|
||||
async def handler(request: Request):
|
||||
return web.json_response({"ok": True})
|
||||
|
||||
app.router.add_route("POST", "/webhook", handler)
|
||||
client: TestClient = await aiohttp_client(app)
|
||||
|
||||
resp = await client.post("/webhook")
|
||||
assert resp.status == 401
|
||||
|
||||
resp = await client.post("/webhook", headers={"X-Forwarded-For": "149.154.167.220"})
|
||||
assert resp.status == 200
|
||||
|
||||
resp = await client.post(
|
||||
"/webhook", headers={"X-Forwarded-For": "149.154.167.220,10.111.0.2"}
|
||||
)
|
||||
assert resp.status == 200
|
||||
|
||||
|
||||
class TestSimpleRequestHandler:
|
||||
async def make_reqest(self, client: TestClient, text: str = "test"):
|
||||
return await client.post(
|
||||
"/webhook",
|
||||
json={
|
||||
"update_id": 0,
|
||||
"message": {
|
||||
"message_id": 0,
|
||||
"from": {"id": 42, "first_name": "Test", "is_bot": False},
|
||||
"chat": {"id": 42, "is_bot": False, "type": "private"},
|
||||
"date": int(time.time()),
|
||||
"text": text,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
async def test(self, bot: MockedBot, aiohttp_client):
|
||||
app = Application()
|
||||
dp = Dispatcher()
|
||||
|
||||
@dp.message(F.text == "test")
|
||||
def handle_message(msg: Message):
|
||||
return msg.answer("PASS")
|
||||
|
||||
handler = SimpleRequestHandler(
|
||||
dispatcher=dp,
|
||||
bot=bot,
|
||||
handle_in_background=False,
|
||||
)
|
||||
handler.register(app, path="/webhook")
|
||||
client: TestClient = await aiohttp_client(app)
|
||||
|
||||
resp = await self.make_reqest(client=client)
|
||||
assert resp.status == 200
|
||||
result = await resp.json()
|
||||
assert result["method"] == "sendMessage"
|
||||
|
||||
resp = await self.make_reqest(client=client, text="spam")
|
||||
assert resp.status == 200
|
||||
result = await resp.json()
|
||||
assert not result
|
||||
|
||||
handler.handle_in_background = True
|
||||
resp = await self.make_reqest(client=client)
|
||||
assert resp.status == 200
|
||||
result = await resp.json()
|
||||
assert not result
|
||||
|
||||
|
||||
class TestTokenBasedRequestHandler:
|
||||
async def test_register(self):
|
||||
dispatcher = Dispatcher()
|
||||
app = Application()
|
||||
|
||||
handler = TokenBasedRequestHandler(dispatcher=dispatcher)
|
||||
|
||||
assert len(app.router.routes()) == 0
|
||||
with pytest.raises(ValueError):
|
||||
handler.register(app, path="/webhook")
|
||||
|
||||
assert len(app.router.routes()) == 0
|
||||
handler.register(app, path="/webhook/{bot_token}")
|
||||
assert len(app.router.routes()) == 1
|
||||
|
||||
async def test_close(self):
|
||||
dispatcher = Dispatcher()
|
||||
|
||||
handler = TokenBasedRequestHandler(dispatcher=dispatcher)
|
||||
|
||||
bot1 = handler.bots["42:TEST"] = MockedBot(token="42:TEST")
|
||||
bot1.add_result_for(GetMe, ok=True, result=User(id=42, is_bot=True, first_name="Test"))
|
||||
assert await bot1.get_me()
|
||||
assert not bot1.session.closed
|
||||
bot2 = handler.bots["1337:TEST"] = MockedBot(token="1337:TEST")
|
||||
bot2.add_result_for(GetMe, ok=True, result=User(id=1337, is_bot=True, first_name="Test"))
|
||||
assert await bot2.get_me()
|
||||
assert not bot2.session.closed
|
||||
|
||||
await handler.close()
|
||||
assert bot1.session.closed
|
||||
assert bot2.session.closed
|
||||
|
||||
async def test_resolve_bot(self):
|
||||
dispatcher = Dispatcher()
|
||||
handler = TokenBasedRequestHandler(dispatcher=dispatcher)
|
||||
|
||||
@dataclass
|
||||
class FakeRequest:
|
||||
match_info: Dict[str, Any]
|
||||
|
||||
bot1 = await handler.resolve_bot(request=FakeRequest(match_info={"bot_token": "42:TEST"}))
|
||||
assert bot1.id == 42
|
||||
|
||||
bot2 = await handler.resolve_bot(
|
||||
request=FakeRequest(match_info={"bot_token": "1337:TEST"})
|
||||
)
|
||||
assert bot2.id == 1337
|
||||
|
||||
bot3 = await handler.resolve_bot(
|
||||
request=FakeRequest(match_info={"bot_token": "1337:TEST"})
|
||||
)
|
||||
assert bot3.id == 1337
|
||||
|
||||
assert bot2 == bot3
|
||||
assert len(handler.bots) == 2
|
||||
57
tests/test_dispatcher/test_webhook/test_security.py
Normal file
57
tests/test_dispatcher/test_webhook/test_security.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
from ipaddress import IPv4Address, IPv4Network
|
||||
|
||||
import pytest
|
||||
|
||||
from aiogram.dispatcher.webhook.security import IPFilter
|
||||
|
||||
|
||||
class TestSecurity:
|
||||
def test_empty_init(self):
|
||||
ip_filter = IPFilter()
|
||||
assert not ip_filter._allowed_ips
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"ip,result",
|
||||
[
|
||||
("127.0.0.1", True),
|
||||
("127.0.0.2", False),
|
||||
(IPv4Address("127.0.0.1"), True),
|
||||
(IPv4Address("127.0.0.2"), False),
|
||||
(IPv4Address("192.168.0.32"), True),
|
||||
("192.168.0.33", False),
|
||||
("10.111.0.5", True),
|
||||
("10.111.0.100", True),
|
||||
("10.111.1.100", False),
|
||||
],
|
||||
)
|
||||
def test_check_ip(self, ip, result):
|
||||
ip_filter = IPFilter(
|
||||
ips=["127.0.0.1", IPv4Address("192.168.0.32"), IPv4Network("10.111.0.0/24")]
|
||||
)
|
||||
assert (ip in ip_filter) is result
|
||||
|
||||
def test_default(self):
|
||||
ip_filter = IPFilter.default()
|
||||
assert isinstance(ip_filter, IPFilter)
|
||||
assert len(ip_filter._allowed_ips) == 5116
|
||||
assert "91.108.4.50" in ip_filter
|
||||
assert "149.154.160.20" in ip_filter
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"ip,ip_range",
|
||||
[
|
||||
["127.0.0.1", {IPv4Address("127.0.0.1")}],
|
||||
["91.108.4.0/22", set(IPv4Network("91.108.4.0/22").hosts())],
|
||||
[IPv4Address("91.108.4.5"), {IPv4Address("91.108.4.5")}],
|
||||
[IPv4Network("91.108.4.0/22"), set(IPv4Network("91.108.4.0/22").hosts())],
|
||||
[42, set()],
|
||||
],
|
||||
)
|
||||
def test_allow_ip(self, ip, ip_range):
|
||||
ip_filter = IPFilter()
|
||||
if not ip_range:
|
||||
with pytest.raises(ValueError):
|
||||
ip_filter.allow_ip(ip)
|
||||
else:
|
||||
ip_filter.allow_ip(ip)
|
||||
assert ip_filter._allowed_ips == ip_range
|
||||
Loading…
Add table
Add a link
Reference in a new issue