Repository: aiogram/aiogram
Pull request #776 (2021-12-12 13:15:17)

Check destiny in case of no `with_destiny`
# Description

Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context.

Fixes # (issue)

## Type of change

Please delete options that are not relevant.

- [ ] Documentation (typos, code examples or any documentation update)
- [ ] Bug fix (non-breaking change which fixes an issue)
- [X] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update

# How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration

- [ ] test_destiny_check tests if ValueError is raised correctly

**Test Configuration**:
* Operating System: Ubuntu 20.04.3 LTS
* Python version: 3.8.10

# Checklist:

- [X] My code follows the style guidelines of this project
- [X] I have performed a self-review of my own code
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [X] I have added tests that prove my fix is effective or that my feature works
- [X] New and existing unit tests pass locally with my changes



Patch:
diff --git a/.apiversion b/.apiversion
index 37c2d996..9ad974f6 100644
--- a/.apiversion
+++ b/.apiversion
@@ -1,1 +1,1 @@
-5.4
+5.5
diff --git a/CHANGES/751.bugfix b/CHANGES/751.bugfix
index ddf1e524..7040b812 100644
--- a/CHANGES/751.bugfix
+++ b/CHANGES/751.bugfix
@@ -1,1 +1,1 @@
-Fixed: Missing ChatMemberHandler import in aiogram/dispatcher/handler
+Fixed: Missing :code:`ChatMemberHandler` import in :code:`aiogram/dispatcher/handler`
diff --git a/CHANGES/777.misc b/CHANGES/777.misc
new file mode 100644
index 00000000..59c5cbc8
--- /dev/null
+++ b/CHANGES/777.misc
@@ -0,0 +1,1 @@
+Added full support of `Bot API 5.5 <https://core.telegram.org/bots/api-changelog#december-7-2021>`_
diff --git a/Makefile b/Makefile
index 09033e7c..63b117c0 100644
--- a/Makefile
+++ b/Makefile
@@ -82,13 +82,16 @@ reformat:
 # =================================================================================================
 # Tests
 # =================================================================================================
+.PHONY: test-run-services
+test-run-services:
+	docker-compose -f tests/docker-compose.yml -p aiogram3-dev up -d
 
 .PHONY: test
-test:
+test: test-run-services
 	$(py) pytest --cov=aiogram --cov-config .coveragerc tests/ --redis $(redis_connection)
 
 .PHONY: test-coverage
-test-coverage:
+test-coverage: test-run-services
 	mkdir -p $(reports_dir)/tests/
 	$(py) pytest --cov=aiogram --cov-config .coveragerc --html=$(reports_dir)/tests/index.html tests/ --redis $(redis_connection)
 
diff --git a/README.rst b/README.rst
index a23a7b99..4633118c 100644
--- a/README.rst
+++ b/README.rst
@@ -13,7 +13,7 @@ aiogram
     :target: https://pypi.python.org/pypi/aiogram
     :alt: Supported python versions
 
-.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.4-blue.svg?logo=telegram
+.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.5-blue.svg?logo=telegram
     :target: https://core.telegram.org/bots/api
     :alt: Telegram Bot API
 
diff --git a/aiogram/__init__.py b/aiogram/__init__.py
index 32e6a5e5..320acd7e 100644
--- a/aiogram/__init__.py
+++ b/aiogram/__init__.py
@@ -37,4 +37,4 @@ __all__ = (
 )
 
 __version__ = "3.0.0a19"
-__api_version__ = "5.4"
+__api_version__ = "5.5"
diff --git a/aiogram/client/bot.py b/aiogram/client/bot.py
index 318b9664..74bc7d4a 100644
--- a/aiogram/client/bot.py
+++ b/aiogram/client/bot.py
@@ -29,6 +29,7 @@ from ..methods import (
     AnswerShippingQuery,
     ApproveChatJoinRequest,
     BanChatMember,
+    BanChatSenderChat,
     Close,
     CopyMessage,
     CreateChatInviteLink,
@@ -102,6 +103,7 @@ from ..methods import (
     StopPoll,
     TelegramMethod,
     UnbanChatMember,
+    UnbanChatSenderChat,
     UnpinAllChatMessages,
     UnpinChatMessage,
     UploadStickerFile,
@@ -1637,6 +1639,50 @@ class Bot(ContextInstanceMixin["Bot"]):
         )
         return await self(call, request_timeout=request_timeout)
 
+    async def ban_chat_sender_chat(
+        self,
+        chat_id: Union[int, str],
+        sender_chat_id: int,
+        request_timeout: Optional[int] = None,
+    ) -> bool:
+        """
+        Use this method to ban a channel chat in a supergroup or a channel. Until the chat is `unbanned <https://core.telegram.org/bots/api#unbanchatsenderchat>`_, the owner of the banned chat won't be able to send messages on behalf of **any of their channels**. The bot must be an administrator in the supergroup or channel for this to work and must have the appropriate administrator rights. Returns :code:`True` on success.
+
+        Source: https://core.telegram.org/bots/api#banchatsenderchat
+
+        :param chat_id: Unique identifier for the target chat or username of the target channel (in the format :code:`@channelusername`)
+        :param sender_chat_id: Unique identifier of the target sender chat
+        :param request_timeout: Request timeout
+        :return: Returns True on success.
+        """
+        call = BanChatSenderChat(
+            chat_id=chat_id,
+            sender_chat_id=sender_chat_id,
+        )
+        return await self(call, request_timeout=request_timeout)
+
+    async def unban_chat_sender_chat(
+        self,
+        chat_id: Union[int, str],
+        sender_chat_id: int,
+        request_timeout: Optional[int] = None,
+    ) -> bool:
+        """
+        Use this method to unban a previously banned channel chat in a supergroup or channel. The bot must be an administrator for this to work and must have the appropriate administrator rights. Returns :code:`True` on success.
+
+        Source: https://core.telegram.org/bots/api#unbanchatsenderchat
+
+        :param chat_id: Unique identifier for the target chat or username of the target channel (in the format :code:`@channelusername`)
+        :param sender_chat_id: Unique identifier of the target sender chat
+        :param request_timeout: Request timeout
+        :return: Returns True on success.
+        """
+        call = UnbanChatSenderChat(
+            chat_id=chat_id,
+            sender_chat_id=sender_chat_id,
+        )
+        return await self(call, request_timeout=request_timeout)
+
     async def set_chat_permissions(
         self,
         chat_id: Union[int, str],
diff --git a/aiogram/dispatcher/fsm/storage/redis.py b/aiogram/dispatcher/fsm/storage/redis.py
index b722984a..8828691f 100644
--- a/aiogram/dispatcher/fsm/storage/redis.py
+++ b/aiogram/dispatcher/fsm/storage/redis.py
@@ -6,7 +6,7 @@ from aioredis import ConnectionPool, Redis
 
 from aiogram import Bot
 from aiogram.dispatcher.fsm.state import State
-from aiogram.dispatcher.fsm.storage.base import BaseStorage, StateType, StorageKey
+from aiogram.dispatcher.fsm.storage.base import DEFAULT_DESTINY, BaseStorage, StateType, StorageKey
 
 DEFAULT_REDIS_LOCK_KWARGS = {"timeout": 60}
 
@@ -62,6 +62,13 @@ class DefaultKeyBuilder(KeyBuilder):
         parts.extend([str(key.chat_id), str(key.user_id)])
         if self.with_destiny:
             parts.append(key.destiny)
+        elif key.destiny != DEFAULT_DESTINY:
+            raise ValueError(
+                "Redis key builder is not configured to use key destiny other the default.\n"
+                "\n"
+                "Probably, you should set `with_destiny=True` in for DefaultKeyBuilder.\n"
+                "E.g: `RedisStorage(redis, key_builder=DefaultKeyBuilder(with_destiny=True))`"
+            )
         parts.append(part)
         return self.separator.join(parts)
 
diff --git a/aiogram/dispatcher/router.py b/aiogram/dispatcher/router.py
index d8526ddd..c9e7dea9 100644
--- a/aiogram/dispatcher/router.py
+++ b/aiogram/dispatcher/router.py
@@ -16,8 +16,8 @@ INTERNAL_UPDATE_TYPES = frozenset({"update", "error"})
 
 class Router:
     """
-    Router can route update and it nested update types like messages, callback query, polls and all other event types.
-    Here is used event-observer pattern.
+    Router can route update, and it nested update types like messages, callback query,
+    polls and all other event types.
 
     Event handlers can be registered in observer by two ways:
 
diff --git a/aiogram/methods/__init__.py b/aiogram/methods/__init__.py
index 2954881f..085044ae 100644
--- a/aiogram/methods/__init__.py
+++ b/aiogram/methods/__init__.py
@@ -5,6 +5,7 @@ from .answer_pre_checkout_query import AnswerPreCheckoutQuery
 from .answer_shipping_query import AnswerShippingQuery
 from .approve_chat_join_request import ApproveChatJoinRequest
 from .ban_chat_member import BanChatMember
+from .ban_chat_sender_chat import BanChatSenderChat
 from .base import Request, Response, TelegramMethod
 from .close import Close
 from .copy_message import CopyMessage
@@ -78,6 +79,7 @@ from .set_webhook import SetWebhook
 from .stop_message_live_location import StopMessageLiveLocation
 from .stop_poll import StopPoll
 from .unban_chat_member import UnbanChatMember
+from .unban_chat_sender_chat import UnbanChatSenderChat
 from .unpin_all_chat_messages import UnpinAllChatMessages
 from .unpin_chat_message import UnpinChatMessage
 from .upload_sticker_file import UploadStickerFile
@@ -120,6 +122,8 @@ __all__ = (
     "RestrictChatMember",
     "PromoteChatMember",
     "SetChatAdministratorCustomTitle",
+    "BanChatSenderChat",
+    "UnbanChatSenderChat",
     "SetChatPermissions",
     "ExportChatInviteLink",
     "CreateChatInviteLink",
diff --git a/aiogram/methods/ban_chat_sender_chat.py b/aiogram/methods/ban_chat_sender_chat.py
new file mode 100644
index 00000000..c68d8f29
--- /dev/null
+++ b/aiogram/methods/ban_chat_sender_chat.py
@@ -0,0 +1,28 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Any, Dict, Union
+
+from .base import Request, TelegramMethod
+
+if TYPE_CHECKING:
+    from ..client.bot import Bot
+
+
+class BanChatSenderChat(TelegramMethod[bool]):
+    """
+    Use this method to ban a channel chat in a supergroup or a channel. Until the chat is `unbanned <https://core.telegram.org/bots/api#unbanchatsenderchat>`_, the owner of the banned chat won't be able to send messages on behalf of **any of their channels**. The bot must be an administrator in the supergroup or channel for this to work and must have the appropriate administrator rights. Returns :code:`True` on success.
+
+    Source: https://core.telegram.org/bots/api#banchatsenderchat
+    """
+
+    __returning__ = bool
+
+    chat_id: Union[int, str]
+    """Unique identifier for the target chat or username of the target channel (in the format :code:`@channelusername`)"""
+    sender_chat_id: int
+    """Unique identifier of the target sender chat"""
+
+    def build_request(self, bot: Bot) -> Request:
+        data: Dict[str, Any] = self.dict()
+
+        return Request(method="banChatSenderChat", data=data)
diff --git a/aiogram/methods/unban_chat_sender_chat.py b/aiogram/methods/unban_chat_sender_chat.py
new file mode 100644
index 00000000..9da79e1f
--- /dev/null
+++ b/aiogram/methods/unban_chat_sender_chat.py
@@ -0,0 +1,28 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Any, Dict, Union
+
+from .base import Request, TelegramMethod
+
+if TYPE_CHECKING:
+    from ..client.bot import Bot
+
+
+class UnbanChatSenderChat(TelegramMethod[bool]):
+    """
+    Use this method to unban a previously banned channel chat in a supergroup or channel. The bot must be an administrator for this to work and must have the appropriate administrator rights. Returns :code:`True` on success.
+
+    Source: https://core.telegram.org/bots/api#unbanchatsenderchat
+    """
+
+    __returning__ = bool
+
+    chat_id: Union[int, str]
+    """Unique identifier for the target chat or username of the target channel (in the format :code:`@channelusername`)"""
+    sender_chat_id: int
+    """Unique identifier of the target sender chat"""
+
+    def build_request(self, bot: Bot) -> Request:
+        data: Dict[str, Any] = self.dict()
+
+        return Request(method="unbanChatSenderChat", data=data)
diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py
index b12fbd28..c2df6f6b 100644
--- a/aiogram/types/chat.py
+++ b/aiogram/types/chat.py
@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, Optional
 from .base import TelegramObject
 
 if TYPE_CHECKING:
+    from ..methods import BanChatSenderChat, UnbanChatSenderChat
     from .chat_location import ChatLocation
     from .chat_permissions import ChatPermissions
     from .chat_photo import ChatPhoto
@@ -34,6 +35,8 @@ class Chat(TelegramObject):
     """*Optional*. Chat photo. Returned only in :class:`aiogram.methods.get_chat.GetChat`."""
     bio: Optional[str] = None
     """*Optional*. Bio of the other party in a private chat. Returned only in :class:`aiogram.methods.get_chat.GetChat`."""
+    has_private_forwards: Optional[bool] = None
+    """*Optional*. True, if privacy settings of the other party in the private chat allows to use :code:`tg://user?id=<user_id>` links only in chats with the user. Returned only in :class:`aiogram.methods.get_chat.GetChat`."""
     description: Optional[str] = None
     """*Optional*. Description, for groups, supergroups and channel chats. Returned only in :class:`aiogram.methods.get_chat.GetChat`."""
     invite_link: Optional[str] = None
@@ -46,6 +49,8 @@ class Chat(TelegramObject):
     """*Optional*. For supergroups, the minimum allowed delay between consecutive messages sent by each unpriviledged user; in seconds. Returned only in :class:`aiogram.methods.get_chat.GetChat`."""
     message_auto_delete_time: Optional[int] = None
     """*Optional*. The time after which all messages sent to the chat will be automatically deleted; in seconds. Returned only in :class:`aiogram.methods.get_chat.GetChat`."""
+    has_protected_content: Optional[bool] = None
+    """*Optional*. True, if messages from the chat can't be forwarded to other chats. Returned only in :class:`aiogram.methods.get_chat.GetChat`."""
     sticker_set_name: Optional[str] = None
     """*Optional*. For supergroups, name of group sticker set. Returned only in :class:`aiogram.methods.get_chat.GetChat`."""
     can_set_sticker_set: Optional[bool] = None
@@ -70,3 +75,19 @@ class Chat(TelegramObject):
         short_id = str(self.id).replace("-100", "")
         shift = int(-1 * pow(10, len(short_id) + 2))
         return shift - self.id
+
+    def ban_sender_chat(self, sender_chat_id: int) -> BanChatSenderChat:
+        from ..methods import BanChatSenderChat
+
+        return BanChatSenderChat(
+            chat_id=self.id,
+            sender_chat_id=sender_chat_id,
+        )
+
+    def unban_sender_chat(self, sender_chat_id: int) -> UnbanChatSenderChat:
+        from ..methods import UnbanChatSenderChat
+
+        return UnbanChatSenderChat(
+            chat_id=self.id,
+            sender_chat_id=sender_chat_id,
+        )
diff --git a/aiogram/types/inline_keyboard_button.py b/aiogram/types/inline_keyboard_button.py
index 2e4142ca..b661339a 100644
--- a/aiogram/types/inline_keyboard_button.py
+++ b/aiogram/types/inline_keyboard_button.py
@@ -19,7 +19,7 @@ class InlineKeyboardButton(MutableTelegramObject):
     text: str
     """Label text on the button"""
     url: Optional[str] = None
-    """*Optional*. HTTP or tg:// url to be opened when button is pressed"""
+    """*Optional*. HTTP or tg:// url to be opened when the button is pressed. Links :code:`tg://user?id=<user_id>` can be used to mention a user by their ID without using a username, if this is allowed by their privacy settings."""
     login_url: Optional[LoginUrl] = None
     """*Optional*. An HTTP URL used to automatically authorize the user. Can be used as a replacement for the `Telegram Login Widget <https://core.telegram.org/widgets/login>`_."""
     callback_data: Optional[str] = None
diff --git a/aiogram/types/input_file.py b/aiogram/types/input_file.py
index 9efa455d..fb204e6d 100644
--- a/aiogram/types/input_file.py
+++ b/aiogram/types/input_file.py
@@ -79,10 +79,8 @@ class BufferedInputFile(InputFile):
 
     async def read(self, chunk_size: int) -> AsyncGenerator[bytes, None]:
         buffer = io.BytesIO(self.data)
-        chunk = buffer.read(chunk_size)
-        while chunk:
+        while chunk := buffer.read(chunk_size):
             yield chunk
-            chunk = buffer.read(chunk_size)
 
 
 class FSInputFile(InputFile):
diff --git a/aiogram/types/message.py b/aiogram/types/message.py
index c33e9991..81ea168a 100644
--- a/aiogram/types/message.py
+++ b/aiogram/types/message.py
@@ -85,9 +85,9 @@ class Message(TelegramObject):
     chat: Chat
     """Conversation the message belongs to"""
     from_user: Optional[User] = Field(None, alias="from")
-    """*Optional*. Sender, empty for messages sent to channels"""
+    """*Optional*. Sender of the message; empty for messages sent to channels. For backward compatibility, the field contains a fake sender user in non-channel chats, if the message was sent on behalf of a chat."""
     sender_chat: Optional[Chat] = None
-    """*Optional*. Sender of the message, sent on behalf of a chat. The channel itself for channel messages. The supergroup itself for messages from anonymous group administrators. The linked channel for messages automatically forwarded to the discussion group"""
+    """*Optional*. Sender of the message, sent on behalf of a chat. For example, the channel itself for channel posts, the supergroup itself for messages from anonymous group administrators, the linked channel for messages automatically forwarded to the discussion group.  For backward compatibility, the field *from* contains a fake sender user in non-channel chats, if the message was sent on behalf of a chat."""
     forward_from: Optional[User] = None
     """*Optional*. For forwarded messages, sender of the original message"""
     forward_from_chat: Optional[Chat] = None
@@ -100,12 +100,16 @@ class Message(TelegramObject):
     """*Optional*. Sender's name for messages forwarded from users who disallow adding a link to their account in forwarded messages"""
     forward_date: Optional[int] = None
     """*Optional*. For forwarded messages, date the original message was sent in Unix time"""
+    is_automatic_forward: Optional[bool] = None
+    """*Optional*. True, if the message is a channel post that was automatically forwarded to the connected discussion group"""
     reply_to_message: Optional[Message] = None
     """*Optional*. For replies, the original message. Note that the Message object in this field will not contain further *reply_to_message* fields even if it itself is a reply."""
     via_bot: Optional[User] = None
     """*Optional*. Bot through which the message was sent"""
     edit_date: Optional[int] = None
     """*Optional*. Date the message was last edited in Unix time"""
+    has_protected_content: Optional[bool] = None
+    """*Optional*. True, if the message can't be forwarded"""
     media_group_id: Optional[str] = None
     """*Optional*. The unique identifier of a media message group this message belongs to"""
     author_signature: Optional[str] = None
diff --git a/docs/api/methods/ban_chat_sender_chat.rst b/docs/api/methods/ban_chat_sender_chat.rst
new file mode 100644
index 00000000..5c6182eb
--- /dev/null
+++ b/docs/api/methods/ban_chat_sender_chat.rst
@@ -0,0 +1,51 @@
+#################
+banChatSenderChat
+#################
+
+Returns: :obj:`bool`
+
+.. automodule:: aiogram.methods.ban_chat_sender_chat
+    :members:
+    :member-order: bysource
+    :undoc-members: True
+
+
+Usage
+=====
+
+As bot method
+-------------
+
+.. code-block::
+
+    result: bool = await bot.ban_chat_sender_chat(...)
+
+
+Method as object
+----------------
+
+Imports:
+
+- :code:`from aiogram.methods.ban_chat_sender_chat import BanChatSenderChat`
+- alias: :code:`from aiogram.methods import BanChatSenderChat`
+
+In handlers with current bot
+----------------------------
+
+.. code-block:: python
+
+    result: bool = await BanChatSenderChat(...)
+
+With specific bot
+~~~~~~~~~~~~~~~~~
+
+.. code-block:: python
+
+    result: bool = await bot(BanChatSenderChat(...))
+
+As reply into Webhook in handler
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: python
+
+    return BanChatSenderChat(...)
diff --git a/docs/api/methods/index.rst b/docs/api/methods/index.rst
index 4d9735a5..1a4143d0 100644
--- a/docs/api/methods/index.rst
+++ b/docs/api/methods/index.rst
@@ -54,6 +54,8 @@ Available methods
     restrict_chat_member
     promote_chat_member
     set_chat_administrator_custom_title
+    ban_chat_sender_chat
+    unban_chat_sender_chat
     set_chat_permissions
     export_chat_invite_link
     create_chat_invite_link
diff --git a/docs/api/methods/unban_chat_sender_chat.rst b/docs/api/methods/unban_chat_sender_chat.rst
new file mode 100644
index 00000000..f0f929da
--- /dev/null
+++ b/docs/api/methods/unban_chat_sender_chat.rst
@@ -0,0 +1,51 @@
+###################
+unbanChatSenderChat
+###################
+
+Returns: :obj:`bool`
+
+.. automodule:: aiogram.methods.unban_chat_sender_chat
+    :members:
+    :member-order: bysource
+    :undoc-members: True
+
+
+Usage
+=====
+
+As bot method
+-------------
+
+.. code-block::
+
+    result: bool = await bot.unban_chat_sender_chat(...)
+
+
+Method as object
+----------------
+
+Imports:
+
+- :code:`from aiogram.methods.unban_chat_sender_chat import UnbanChatSenderChat`
+- alias: :code:`from aiogram.methods import UnbanChatSenderChat`
+
+In handlers with current bot
+----------------------------
+
+.. code-block:: python
+
+    result: bool = await UnbanChatSenderChat(...)
+
+With specific bot
+~~~~~~~~~~~~~~~~~
+
+.. code-block:: python
+
+    result: bool = await bot(UnbanChatSenderChat(...))
+
+As reply into Webhook in handler
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: python
+
+    return UnbanChatSenderChat(...)
diff --git a/pyproject.toml b/pyproject.toml
index 1f7b162c..a36de2e6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "aiogram"
-version = "3.0.0-alpha.18"
+version = "3.0.0-alpha.19"
 description = "Modern and fully asynchronous framework for Telegram Bot API"
 authors = [
     "Alex Root Junior <jroot.junior@gmail.com>",


Test patch:
diff --git a/tests/test_api/test_methods/test_ban_chat_sender_chat.py b/tests/test_api/test_methods/test_ban_chat_sender_chat.py
new file mode 100644
index 00000000..1d832e87
--- /dev/null
+++ b/tests/test_api/test_methods/test_ban_chat_sender_chat.py
@@ -0,0 +1,32 @@
+import pytest
+
+from aiogram.methods import BanChatSenderChat, Request
+from tests.mocked_bot import MockedBot
+
+
+class TestBanChatSenderChat:
+    @pytest.mark.asyncio
+    async def test_method(self, bot: MockedBot):
+        prepare_result = bot.add_result_for(BanChatSenderChat, ok=True, result=True)
+
+        response: bool = await BanChatSenderChat(
+            chat_id=-42,
+            sender_chat_id=-1337,
+        )
+        request: Request = bot.get_request()
+        assert request.method == "banChatSenderChat"
+        # 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(BanChatSenderChat, ok=True, result=True)
+
+        response: bool = await bot.ban_chat_sender_chat(
+            chat_id=-42,
+            sender_chat_id=-1337,
+        )
+        request: Request = bot.get_request()
+        assert request.method == "banChatSenderChat"
+        # assert request.data == {}
+        assert response == prepare_result.result
diff --git a/tests/test_api/test_methods/test_unban_chat_sender_chat.py b/tests/test_api/test_methods/test_unban_chat_sender_chat.py
new file mode 100644
index 00000000..4f80a5e1
--- /dev/null
+++ b/tests/test_api/test_methods/test_unban_chat_sender_chat.py
@@ -0,0 +1,32 @@
+import pytest
+
+from aiogram.methods import Request, UnbanChatSenderChat
+from tests.mocked_bot import MockedBot
+
+
+class TestUnbanChatSenderChat:
+    @pytest.mark.asyncio
+    async def test_method(self, bot: MockedBot):
+        prepare_result = bot.add_result_for(UnbanChatSenderChat, ok=True, result=True)
+
+        response: bool = await UnbanChatSenderChat(
+            chat_id=-42,
+            sender_chat_id=-1337,
+        )
+        request: Request = bot.get_request()
+        assert request.method == "unbanChatSenderChat"
+        # 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(UnbanChatSenderChat, ok=True, result=True)
+
+        response: bool = await bot.unban_chat_sender_chat(
+            chat_id=-42,
+            sender_chat_id=-1337,
+        )
+        request: Request = bot.get_request()
+        assert request.method == "unbanChatSenderChat"
+        # assert request.data == {}
+        assert response == prepare_result.result
diff --git a/tests/test_api/test_types/test_chat.py b/tests/test_api/test_types/test_chat.py
new file mode 100644
index 00000000..5fc9e081
--- /dev/null
+++ b/tests/test_api/test_types/test_chat.py
@@ -0,0 +1,17 @@
+from aiogram.types import Chat
+
+
+class TestChat:
+    def test_ban_sender_chat(self):
+        chat = Chat(id=-42, type="supergroup")
+
+        method = chat.ban_sender_chat(sender_chat_id=-1337)
+        assert method.chat_id == chat.id
+        assert method.sender_chat_id == -1337
+
+    def test_unban_sender_chat(self):
+        chat = Chat(id=-42, type="supergroup")
+
+        method = chat.unban_sender_chat(sender_chat_id=-1337)
+        assert method.chat_id == chat.id
+        assert method.sender_chat_id == -1337
diff --git a/tests/test_dispatcher/test_fsm/storage/test_redis.py b/tests/test_dispatcher/test_fsm/storage/test_redis.py
index 1a2280d6..dcb71c3d 100644
--- a/tests/test_dispatcher/test_fsm/storage/test_redis.py
+++ b/tests/test_dispatcher/test_fsm/storage/test_redis.py
@@ -1,6 +1,8 @@
+from typing import Literal
+
 import pytest
 
-from aiogram.dispatcher.fsm.storage.base import StorageKey
+from aiogram.dispatcher.fsm.storage.base import DEFAULT_DESTINY, StorageKey
 from aiogram.dispatcher.fsm.storage.redis import DefaultKeyBuilder
 
 pytestmark = pytest.mark.asyncio
@@ -10,7 +12,6 @@ BOT_ID = 42
 CHAT_ID = -1
 USER_ID = 2
 FIELD = "data"
-DESTINY = "testing"
 
 
 class TestRedisDefaultKeyBuilder:
@@ -19,8 +20,8 @@ class TestRedisDefaultKeyBuilder:
         [
             [False, False, f"{PREFIX}:{CHAT_ID}:{USER_ID}:{FIELD}"],
             [True, False, f"{PREFIX}:{BOT_ID}:{CHAT_ID}:{USER_ID}:{FIELD}"],
-            [True, True, f"{PREFIX}:{BOT_ID}:{CHAT_ID}:{USER_ID}:{DESTINY}:{FIELD}"],
-            [False, True, f"{PREFIX}:{CHAT_ID}:{USER_ID}:{DESTINY}:{FIELD}"],
+            [True, True, f"{PREFIX}:{BOT_ID}:{CHAT_ID}:{USER_ID}:{DEFAULT_DESTINY}:{FIELD}"],
+            [False, True, f"{PREFIX}:{CHAT_ID}:{USER_ID}:{DEFAULT_DESTINY}:{FIELD}"],
         ],
     )
     async def test_generate_key(self, with_bot_id: bool, with_destiny: bool, result: str):
@@ -29,5 +30,18 @@ class TestRedisDefaultKeyBuilder:
             with_bot_id=with_bot_id,
             with_destiny=with_destiny,
         )
-        key = StorageKey(chat_id=CHAT_ID, user_id=USER_ID, bot_id=BOT_ID, destiny=DESTINY)
+        key = StorageKey(chat_id=CHAT_ID, user_id=USER_ID, bot_id=BOT_ID, destiny=DEFAULT_DESTINY)
         assert key_builder.build(key, FIELD) == result
+
+    async def test_destiny_check(self):
+        key_builder = DefaultKeyBuilder(
+            with_destiny=False,
+        )
+        key = StorageKey(chat_id=CHAT_ID, user_id=USER_ID, bot_id=BOT_ID)
+        assert key_builder.build(key, FIELD)
+
+        key = StorageKey(
+            chat_id=CHAT_ID, user_id=USER_ID, bot_id=BOT_ID, destiny="CUSTOM_TEST_DESTINY"
+        )
+        with pytest.raises(ValueError):
+            key_builder.build(key, FIELD)