From 7dd80d281f4479c188edeb79605f196ab24b546c Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 12 May 2021 23:56:03 +0300 Subject: [PATCH] Added lost files --- docs2/dispatcher/middlewares.rst | 113 +++++++++++++ .../test_create_chat_invite_link.py | 49 ++++++ .../test_edit_chat_invite_link.py | 49 ++++++ .../test_revoke_chat_invite_link.py | 51 ++++++ .../test_types/test_message_entity.py | 15 ++ tests/test_dispatcher/fsm/__init__.py | 0 tests/test_dispatcher/fsm/test_state.py | 152 ++++++++++++++++++ 7 files changed, 429 insertions(+) create mode 100644 docs2/dispatcher/middlewares.rst create mode 100644 tests/test_api/test_methods/test_create_chat_invite_link.py create mode 100644 tests/test_api/test_methods/test_edit_chat_invite_link.py create mode 100644 tests/test_api/test_methods/test_revoke_chat_invite_link.py create mode 100644 tests/test_api/test_types/test_message_entity.py create mode 100644 tests/test_dispatcher/fsm/__init__.py create mode 100644 tests/test_dispatcher/fsm/test_state.py diff --git a/docs2/dispatcher/middlewares.rst b/docs2/dispatcher/middlewares.rst new file mode 100644 index 00000000..af20d01e --- /dev/null +++ b/docs2/dispatcher/middlewares.rst @@ -0,0 +1,113 @@ +=========== +Middlewares +=========== + +**aiogram** provides powerful mechanism for customizing event handlers via middlewares. + +Middlewares in bot framework seems like Middlewares mechanism in web-frameworks +like `aiohttp `_, +`fastapi `_, +`Django `_ or etc.) +with small difference - here is implemented two layers of middlewares (before and after filters). + +.. note:: + + Middleware is function that triggered on every event received from + Telegram Bot API in many points on processing pipeline. + +Base theory +=========== + +As many books and other literature in internet says: + + Middleware is reusable software that leverages patterns and frameworks to bridge + the gap between the functional requirements of applications and the underlying operating systems, + network protocol stacks, and databases. + +Middleware can modify, extend or reject processing event in many places of pipeline. + +Basics +====== + +Middleware instance can be applied for every type of Telegram Event (Update, Message, etc.) in two places + +1. Outer scope - before processing filters (:code:`..outer_middleware(...)`) +2. Inner scope - after processing filters but before handler (:code:`..middleware(...)`) + +.. image:: ../_static/basics_middleware.png + :alt: Middleware basics + +.. attention:: + + Middleware should be subclass of :code:`BaseMiddleware` (:code:`from aiogram import BaseMiddleware`) or any async callable + +Arguments specification +======================= + +.. autoclass:: aiogram.dispatcher.middlewares.base.BaseMiddleware + :members: + :show-inheritance: + :member-order: bysource + :special-members: __init__, __call__ + :undoc-members: True + + +Examples +======== + +.. danger:: + + Middleware should always call :code:`await handler(event, data)` to propagate event for next middleware/handler + + +Class-based +----------- +.. code-block:: python + + from aiogram import BaseMiddleware + from aiogram.api.types import Message + + + class CounterMiddleware(BaseMiddleware[Message]): + def __init__(self) -> None: + self.counter = 0 + + async def __call__( + self, + handler: Callable[[Message, Dict[str, Any]], Awaitable[Any]], + event: Message, + data: Dict[str, Any] + ) -> Any: + self.counter += 1 + data['counter'] = self.counter + return await handler(event, data) + +and then + +.. code-block:: python3 + + router = Router() + router.message.middleware(CounterMiddleware()) + + +Function-based +-------------- + +.. code-block:: python3 + + @dispatcher.update.outer_middleware() + async def database_transaction_middleware( + handler: Callable[[Update, Dict[str, Any]], Awaitable[Any]], + event: Update, + data: Dict[str, Any] + ) -> Any: + async with database.transaction(): + return await handler(event, data) + + +Facts +===== + +1. Middlewares from outer scope will be called on every incoming event +2. Middlewares from inner scope will be called only when filters pass +3. Inner middlewares is always calls for :class:`aiogram.types.update.Update` event type in due to all incoming updates going to specific event type handler through built in update handler diff --git a/tests/test_api/test_methods/test_create_chat_invite_link.py b/tests/test_api/test_methods/test_create_chat_invite_link.py new file mode 100644 index 00000000..d33d25f7 --- /dev/null +++ b/tests/test_api/test_methods/test_create_chat_invite_link.py @@ -0,0 +1,49 @@ +import pytest + +from aiogram.methods import CreateChatInviteLink, Request +from aiogram.types import ChatInviteLink, User +from tests.mocked_bot import MockedBot + + +class TestCreateChatInviteLink: + @pytest.mark.asyncio + async def test_method(self, bot: MockedBot): + prepare_result = bot.add_result_for( + CreateChatInviteLink, + ok=True, + result=ChatInviteLink( + invite_link="https://t.me/username", + creator=User(id=42, is_bot=False, first_name="User"), + is_primary=False, + is_revoked=False, + ), + ) + + response: ChatInviteLink = await CreateChatInviteLink( + chat_id=-42, + ) + request: Request = bot.get_request() + assert request.method == "createChatInviteLink" + # assert request.data == {"chat_id": -42} + assert response == prepare_result.result + + @pytest.mark.asyncio + async def test_bot_method(self, bot: MockedBot): + prepare_result = bot.add_result_for( + CreateChatInviteLink, + ok=True, + result=ChatInviteLink( + invite_link="https://t.me/username", + creator=User(id=42, is_bot=False, first_name="User"), + is_primary=False, + is_revoked=False, + ), + ) + + response: ChatInviteLink = await bot.create_chat_invite_link( + chat_id=-42, + ) + request: Request = bot.get_request() + assert request.method == "createChatInviteLink" + # assert request.data == {"chat_id": -42} + assert response == prepare_result.result diff --git a/tests/test_api/test_methods/test_edit_chat_invite_link.py b/tests/test_api/test_methods/test_edit_chat_invite_link.py new file mode 100644 index 00000000..212d1e51 --- /dev/null +++ b/tests/test_api/test_methods/test_edit_chat_invite_link.py @@ -0,0 +1,49 @@ +import pytest + +from aiogram.methods import EditChatInviteLink, Request +from aiogram.types import ChatInviteLink, User +from tests.mocked_bot import MockedBot + + +class TestEditChatInviteLink: + @pytest.mark.asyncio + async def test_method(self, bot: MockedBot): + prepare_result = bot.add_result_for( + EditChatInviteLink, + ok=True, + result=ChatInviteLink( + invite_link="https://t.me/username2", + creator=User(id=42, is_bot=False, first_name="User"), + is_primary=False, + is_revoked=False, + ), + ) + + response: ChatInviteLink = await EditChatInviteLink( + chat_id=-42, invite_link="https://t.me/username", member_limit=1 + ) + request: Request = bot.get_request() + assert request.method == "editChatInviteLink" + # assert request.data == {} + assert response == prepare_result.result + + @pytest.mark.asyncio + async def test_bot_method(self, bot: MockedBot): + prepare_result = bot.add_result_for( + EditChatInviteLink, + ok=True, + result=ChatInviteLink( + invite_link="https://t.me/username2", + creator=User(id=42, is_bot=False, first_name="User"), + is_primary=False, + is_revoked=False, + ), + ) + + response: ChatInviteLink = await bot.edit_chat_invite_link( + chat_id=-42, invite_link="https://t.me/username", member_limit=1 + ) + request: Request = bot.get_request() + assert request.method == "editChatInviteLink" + # assert request.data == {} + assert response == prepare_result.result diff --git a/tests/test_api/test_methods/test_revoke_chat_invite_link.py b/tests/test_api/test_methods/test_revoke_chat_invite_link.py new file mode 100644 index 00000000..a791d53b --- /dev/null +++ b/tests/test_api/test_methods/test_revoke_chat_invite_link.py @@ -0,0 +1,51 @@ +import pytest + +from aiogram.methods import Request, RevokeChatInviteLink +from aiogram.types import ChatInviteLink, User +from tests.mocked_bot import MockedBot + + +class TestRevokeChatInviteLink: + @pytest.mark.asyncio + async def test_method(self, bot: MockedBot): + prepare_result = bot.add_result_for( + RevokeChatInviteLink, + ok=True, + result=ChatInviteLink( + invite_link="https://t.me/username", + creator=User(id=42, is_bot=False, first_name="User"), + is_primary=False, + is_revoked=True, + ), + ) + + response: ChatInviteLink = await RevokeChatInviteLink( + chat_id=-42, + invite_link="https://t.me/username", + ) + request: Request = bot.get_request() + assert request.method == "revokeChatInviteLink" + # assert request.data == {} + assert response == prepare_result.result + + @pytest.mark.asyncio + async def test_bot_method(self, bot: MockedBot): + prepare_result = bot.add_result_for( + RevokeChatInviteLink, + ok=True, + result=ChatInviteLink( + invite_link="https://t.me/username", + creator=User(id=42, is_bot=False, first_name="User"), + is_primary=False, + is_revoked=True, + ), + ) + + response: ChatInviteLink = await bot.revoke_chat_invite_link( + chat_id=-42, + invite_link="https://t.me/username", + ) + request: Request = bot.get_request() + assert request.method == "revokeChatInviteLink" + # assert request.data == {} + assert response == prepare_result.result diff --git a/tests/test_api/test_types/test_message_entity.py b/tests/test_api/test_types/test_message_entity.py new file mode 100644 index 00000000..d5195d98 --- /dev/null +++ b/tests/test_api/test_types/test_message_entity.py @@ -0,0 +1,15 @@ +import pytest + +from aiogram.types import MessageEntity +from tests.deprecated import check_deprecated + + +class TestMessageEntity: + def test_extract(self): + entity = MessageEntity(type="hashtag", length=4, offset=5) + assert entity.extract("#foo #bar #baz") == "#bar" + + def test_get_text(self): + entity = MessageEntity(type="hashtag", length=4, offset=5) + with check_deprecated("3.2", exception=AttributeError): + assert entity.get_text("#foo #bar #baz") == "#bar" diff --git a/tests/test_dispatcher/fsm/__init__.py b/tests/test_dispatcher/fsm/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_dispatcher/fsm/test_state.py b/tests/test_dispatcher/fsm/test_state.py new file mode 100644 index 00000000..07037e86 --- /dev/null +++ b/tests/test_dispatcher/fsm/test_state.py @@ -0,0 +1,152 @@ +import pytest + +from aiogram.dispatcher.fsm.state import State, StatesGroup, any_state + + +class TestState: + def test_empty(self): + state = State() + assert state._state is None + assert state._group_name is None + assert state._group is None + + with pytest.raises(RuntimeError): + assert state.group + + assert state.state is None + assert str(state) == "" + + def test_star(self): + state = State(state="*") + assert state._state == "*" + assert state._group_name is None + assert state._group is None + + with pytest.raises(RuntimeError): + assert state.group + assert state.state == "*" + assert str(state) == "" + + def test_star_filter(self): + assert any_state(None, "foo") + assert any_state(None, "bar") + assert any_state(None, "baz") + + def test_alone(self): + state = State("test") + assert state._state == "test" + assert state._group_name is None + assert state._group is None + + with pytest.raises(RuntimeError): + assert state.group + + assert state.state == "@:test" + assert str(state) == "" + + def test_alone_with_group(self): + state = State("test", group_name="Test") + assert state._state == "test" + assert state._group_name == "Test" + assert state._group is None + + with pytest.raises(RuntimeError): + assert state.group == "Test" + + assert state.state == "Test:test" + assert str(state) == "" + + @pytest.mark.parametrize( + "state,check,result", + [ + [State("test"), "test", False], + [State("test"), "@:test", True], + [State("test"), "test1", False], + [State("test", group_name="test"), "test:test", True], + [State("test", group_name="test"), "test:test2", False], + [State("test", group_name="test"), "test2:test", False], + [State("test", group_name="test"), "test2:test2", False], + ], + ) + def test_filter(self, state, check, result): + assert state(None, check) is result + + def test_state_in_unknown_class(self): + with pytest.raises(RuntimeError): + + class MyClass: + state1 = State() + + +class TestStatesGroup: + def test_empty(self): + class MyGroup(StatesGroup): + pass + + assert MyGroup.__states__ == () + assert MyGroup.__state_names__ == () + assert MyGroup.__all_childs__ == () + assert MyGroup.__all_states__ == () + assert MyGroup.__all_states_names__ == () + assert MyGroup.__parent__ is None + assert MyGroup.__full_group_name__ == "MyGroup" + + assert str(MyGroup) == "" + + def test_with_state(self): + class MyGroup(StatesGroup): + state1 = State() + + assert MyGroup.__states__ == (MyGroup.state1,) + assert MyGroup.__state_names__ == ("MyGroup:state1",) + assert MyGroup.__all_childs__ == () + assert MyGroup.__all_states__ == (MyGroup.state1,) + assert MyGroup.__parent__ is None + assert MyGroup.__full_group_name__ == "MyGroup" + + assert str(MyGroup) == "" + + assert MyGroup.state1.state == "MyGroup:state1" + assert MyGroup.state1.group == MyGroup + + def test_nested_group(self): + class MyGroup(StatesGroup): + state1 = State() + + class MyNestedGroup(StatesGroup): + state1 = State() + + assert MyGroup.__states__ == (MyGroup.state1,) + assert MyGroup.__state_names__ == ("MyGroup:state1",) + assert MyGroup.__all_childs__ == (MyGroup.MyNestedGroup,) + assert MyGroup.__all_states__ == (MyGroup.state1, MyGroup.MyNestedGroup.state1) + assert MyGroup.__parent__ is None + assert MyGroup.MyNestedGroup.__parent__ is MyGroup + assert MyGroup.__full_group_name__ == "MyGroup" + assert MyGroup.MyNestedGroup.__full_group_name__ == "MyGroup.MyNestedGroup" + + assert str(MyGroup) == "" + assert str(MyGroup.MyNestedGroup) == "" + + assert MyGroup.state1.state == "MyGroup:state1" + assert MyGroup.state1.group == MyGroup + + assert MyGroup.MyNestedGroup.state1.state == "MyGroup.MyNestedGroup:state1" + assert MyGroup.MyNestedGroup.state1.group == MyGroup.MyNestedGroup + + assert MyGroup.MyNestedGroup.state1 in MyGroup.MyNestedGroup + assert MyGroup.MyNestedGroup.state1 in MyGroup + assert MyGroup.state1 not in MyGroup.MyNestedGroup + assert MyGroup.state1 in MyGroup + + # Not working as well + # assert MyGroup.MyNestedGroup in MyGroup + + assert "MyGroup.MyNestedGroup:state1" in MyGroup + assert "MyGroup.MyNestedGroup:state1" in MyGroup.MyNestedGroup + + assert MyGroup.state1 not in MyGroup.MyNestedGroup + assert "test" not in MyGroup + assert 42 not in MyGroup + + assert MyGroup.MyNestedGroup.get_root() is MyGroup