diff --git a/aiogram/api/client/base.py b/aiogram/api/client/base.py index bfc71d44..cb0ea192 100644 --- a/aiogram/api/client/base.py +++ b/aiogram/api/client/base.py @@ -1,5 +1,6 @@ from __future__ import annotations +from contextlib import asynccontextmanager from typing import Any, Optional, TypeVar from ...utils.mixins import ContextInstanceMixin, DataMixin @@ -32,11 +33,15 @@ class BaseBot(ContextInstanceMixin, DataMixin): async def close(self): await self.session.close() - async def __aenter__(self): - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb): - await self.session.close() + @asynccontextmanager + async def context(self, auto_close: bool = True): + token = self.set_current(self) + try: + yield self + finally: + if auto_close: + await self.close() + self.reset_current(token) def __hash__(self): return hash(self.__token) diff --git a/aiogram/api/client/session/base.py b/aiogram/api/client/session/base.py index 4f960b23..f2f40727 100644 --- a/aiogram/api/client/session/base.py +++ b/aiogram/api/client/session/base.py @@ -5,6 +5,8 @@ import datetime import json from typing import Any, Callable, Optional, TypeVar, Union +from aiogram.utils.exceptions import TelegramAPIError + from ...methods import Response, TelegramMethod from ..telegram import PRODUCTION, TelegramAPIServer @@ -32,7 +34,7 @@ class BaseSession(abc.ABC): def raise_for_status(self, response: Response[T]) -> None: if response.ok: return - raise Exception(response.description) + raise TelegramAPIError(response.description) @abc.abstractmethod async def close(self): # pragma: no cover diff --git a/aiogram/api/types/user.py b/aiogram/api/types/user.py index 1c2a9df0..70175f40 100644 --- a/aiogram/api/types/user.py +++ b/aiogram/api/types/user.py @@ -24,3 +24,9 @@ class User(TelegramObject): """User‘s or bot’s username""" language_code: Optional[str] = None """IETF language tag of the user's language""" + + @property + def full_name(self): + if self.last_name: + return f"{self.first_name} {self.last_name}" + return self.first_name diff --git a/tests/test_api/test_client/test_base_bot.py b/tests/test_api/test_client/test_base_bot.py index 22a5a767..04c46668 100644 --- a/tests/test_api/test_client/test_base_bot.py +++ b/tests/test_api/test_client/test_base_bot.py @@ -48,10 +48,16 @@ class TestBaseBot: mocked_close.assert_awaited() @pytest.mark.asyncio - async def test_context_manager(self): + @pytest.mark.parametrize("close", [True, False]) + async def test_context_manager(self, close: bool): with patch( "aiogram.api.client.session.aiohttp.AiohttpSession.close", new_callable=CoroutineMock ) as mocked_close: - async with BaseBot("42:TEST", session=AiohttpSession()) as bot: + async with BaseBot("42:TEST", session=AiohttpSession()).context( + auto_close=close + ) as bot: assert isinstance(bot, BaseBot) - mocked_close.assert_awaited() + if close: + mocked_close.assert_awaited() + else: + mocked_close.assert_not_awaited() diff --git a/tests/test_api/test_types/test_user.py b/tests/test_api/test_types/test_user.py new file mode 100644 index 00000000..ed09b97c --- /dev/null +++ b/tests/test_api/test_types/test_user.py @@ -0,0 +1,20 @@ +import pytest + +from aiogram.api.types import User + + +class TestUser: + @pytest.mark.parametrize( + "first,last,result", + [ + ["User", None, "User"], + ["", None, ""], + [" ", None, " "], + ["User", "Name", "User Name"], + ["User", " ", "User "], + [" ", " ", " "], + ], + ) + def test_full_name(self, first: str, last: str, result: bool): + user = User(id=42, is_bot=False, first_name=first, last_name=last) + assert user.full_name == result diff --git a/tests/test_dispatcher/test_dispatcher.py b/tests/test_dispatcher/test_dispatcher.py index c7a15fb5..ab69931b 100644 --- a/tests/test_dispatcher/test_dispatcher.py +++ b/tests/test_dispatcher/test_dispatcher.py @@ -4,9 +4,11 @@ import time import pytest from aiogram import Bot +from aiogram.api.methods import GetUpdates, SendMessage from aiogram.api.types import Chat, Message, Update, User from aiogram.dispatcher.dispatcher import Dispatcher from aiogram.dispatcher.router import Router +from tests.mocked_bot import MockedBot class TestDispatcher: @@ -77,12 +79,43 @@ class TestDispatcher: assert result == "test" assert handled - @pytest.mark.skip @pytest.mark.asyncio - async def test_listen_updates(self): - pass + async def test_listen_updates(self, bot: MockedBot): + dispatcher = Dispatcher() + bot.add_result_for( + GetUpdates, ok=True, result=[Update(update_id=update_id) for update_id in range(42)] + ) + index = 0 + async for update in dispatcher._listen_updates(bot=bot): + assert update.update_id == index + index += 1 + if index == 42: + break + + @pytest.mark.asyncio + 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(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] @pytest.mark.skip @pytest.mark.asyncio async def test_polling(self): pass + + @pytest.mark.skip + @pytest.mark.asyncio + async def test_process_update(self): + pass + + @pytest.mark.skip + @pytest.mark.asyncio + async def test_run_polling(self): + pass + + @pytest.mark.skip + def test_run(self): + pass