aiogram-testing 1.2.0__tar.gz → 1.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {aiogram_testing-1.2.0 → aiogram_testing-1.3.0}/PKG-INFO +23 -2
- {aiogram_testing-1.2.0 → aiogram_testing-1.3.0}/README.md +22 -1
- aiogram_testing-1.3.0/aiogram_tests/__init__.py +8 -0
- aiogram_testing-1.2.0/aiogram_tests/requester.py → aiogram_testing-1.3.0/aiogram_tests/calls.py +5 -48
- {aiogram_testing-1.2.0 → aiogram_testing-1.3.0}/aiogram_tests/handler/handler.py +1 -1
- {aiogram_testing-1.2.0 → aiogram_testing-1.3.0}/aiogram_tests/mocked_bot.py +45 -4
- aiogram_testing-1.3.0/aiogram_tests/py.typed +0 -0
- aiogram_testing-1.3.0/aiogram_tests/requester.py +53 -0
- {aiogram_testing-1.2.0 → aiogram_testing-1.3.0}/aiogram_tests/tester.py +3 -3
- {aiogram_testing-1.2.0 → aiogram_testing-1.3.0}/pyproject.toml +1 -1
- aiogram_testing-1.2.0/aiogram_tests/__init__.py +0 -6
- {aiogram_testing-1.2.0 → aiogram_testing-1.3.0}/aiogram_tests/exceptions.py +0 -0
- {aiogram_testing-1.2.0 → aiogram_testing-1.3.0}/aiogram_tests/handler/__init__.py +0 -0
- {aiogram_testing-1.2.0 → aiogram_testing-1.3.0}/aiogram_tests/handler/base.py +0 -0
- {aiogram_testing-1.2.0 → aiogram_testing-1.3.0}/aiogram_tests/pytest_plugin.py +0 -0
- {aiogram_testing-1.2.0 → aiogram_testing-1.3.0}/aiogram_tests/types/__init__.py +0 -0
- {aiogram_testing-1.2.0 → aiogram_testing-1.3.0}/aiogram_tests/types/dataset/__init__.py +0 -0
- {aiogram_testing-1.2.0 → aiogram_testing-1.3.0}/aiogram_tests/types/dataset/base.py +0 -0
- {aiogram_testing-1.2.0 → aiogram_testing-1.3.0}/aiogram_tests/types/dataset/chats.py +0 -0
- {aiogram_testing-1.2.0 → aiogram_testing-1.3.0}/aiogram_tests/types/dataset/entities.py +0 -0
- {aiogram_testing-1.2.0 → aiogram_testing-1.3.0}/aiogram_tests/types/dataset/events.py +0 -0
- {aiogram_testing-1.2.0 → aiogram_testing-1.3.0}/aiogram_tests/types/dataset/media.py +0 -0
- {aiogram_testing-1.2.0 → aiogram_testing-1.3.0}/aiogram_tests/types/dataset/messages.py +0 -0
- {aiogram_testing-1.2.0 → aiogram_testing-1.3.0}/aiogram_tests/types/dataset/misc.py +0 -0
- {aiogram_testing-1.2.0 → aiogram_testing-1.3.0}/aiogram_tests/types/dataset/payments.py +0 -0
- {aiogram_testing-1.2.0 → aiogram_testing-1.3.0}/aiogram_tests/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aiogram-testing
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: A library for testing your bots on aiogram
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: k0te1ch
|
|
@@ -101,7 +101,28 @@ calls.assert_not_called(AnswerCallbackQuery)
|
|
|
101
101
|
```
|
|
102
102
|
|
|
103
103
|
Accessing a method that was not called (`calls.edit_message_text`) raises `MethodIsNotCalledError`, an
|
|
104
|
-
`AttributeError`, so `hasattr(calls, "edit_message_text")` works.
|
|
104
|
+
`AttributeError`, so `hasattr(calls, "edit_message_text")` works. `calls.result` is what the dispatcher returned for
|
|
105
|
+
the update (a handler's return value, or `UNHANDLED` when nothing matched).
|
|
106
|
+
|
|
107
|
+
### Bot API answers and failures
|
|
108
|
+
|
|
109
|
+
With `auto_mock_success` on (the default) the bot answers like Telegram would: `send_message` returns a `Message`
|
|
110
|
+
with a fresh `message_id`, the target chat and the text; edits return the edited message (or `True` for inline
|
|
111
|
+
messages); boolean methods return `True`; `get_me` returns the bot. Queue a specific answer or a failure with
|
|
112
|
+
`add_result_for`; queued answers are used first, in order:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from aiogram.exceptions import TelegramBadRequest, TelegramRetryAfter
|
|
116
|
+
|
|
117
|
+
bot.add_result_for(SendMessage, ok=False, error_code=400, description="Bad Request: chat not found")
|
|
118
|
+
bot.add_result_for(SendMessage, ok=False, error_code=429, description="Too Many Requests", retry_after=3)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
aiogram raises `TelegramRetryAfter` only for a non-zero `retry_after`; with `0` the failure is a plain
|
|
122
|
+
`TelegramAPIError`. Patch `asyncio.sleep` in the code under test if a retry should not actually wait.
|
|
123
|
+
|
|
124
|
+
Code that talks to the bot outside the dispatcher (background jobs, notifiers) can use a `MockedBot` directly;
|
|
125
|
+
`bot.calls` holds every call it made.
|
|
105
126
|
|
|
106
127
|
### Testing a whole router: `BotTester`
|
|
107
128
|
|
|
@@ -79,7 +79,28 @@ calls.assert_not_called(AnswerCallbackQuery)
|
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
Accessing a method that was not called (`calls.edit_message_text`) raises `MethodIsNotCalledError`, an
|
|
82
|
-
`AttributeError`, so `hasattr(calls, "edit_message_text")` works.
|
|
82
|
+
`AttributeError`, so `hasattr(calls, "edit_message_text")` works. `calls.result` is what the dispatcher returned for
|
|
83
|
+
the update (a handler's return value, or `UNHANDLED` when nothing matched).
|
|
84
|
+
|
|
85
|
+
### Bot API answers and failures
|
|
86
|
+
|
|
87
|
+
With `auto_mock_success` on (the default) the bot answers like Telegram would: `send_message` returns a `Message`
|
|
88
|
+
with a fresh `message_id`, the target chat and the text; edits return the edited message (or `True` for inline
|
|
89
|
+
messages); boolean methods return `True`; `get_me` returns the bot. Queue a specific answer or a failure with
|
|
90
|
+
`add_result_for`; queued answers are used first, in order:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from aiogram.exceptions import TelegramBadRequest, TelegramRetryAfter
|
|
94
|
+
|
|
95
|
+
bot.add_result_for(SendMessage, ok=False, error_code=400, description="Bad Request: chat not found")
|
|
96
|
+
bot.add_result_for(SendMessage, ok=False, error_code=429, description="Too Many Requests", retry_after=3)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
aiogram raises `TelegramRetryAfter` only for a non-zero `retry_after`; with `0` the failure is a plain
|
|
100
|
+
`TelegramAPIError`. Patch `asyncio.sleep` in the code under test if a retry should not actually wait.
|
|
101
|
+
|
|
102
|
+
Code that talks to the bot outside the dispatcher (background jobs, notifiers) can use a `MockedBot` directly;
|
|
103
|
+
`bot.calls` holds every call it made.
|
|
83
104
|
|
|
84
105
|
### Testing a whole router: `BotTester`
|
|
85
106
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from .calls import Calls
|
|
2
|
+
from .calls import CallsList
|
|
3
|
+
from .mocked_bot import MockedBot
|
|
4
|
+
from .requester import MockedRequester
|
|
5
|
+
from .tester import BotTester
|
|
6
|
+
|
|
7
|
+
__all__ = ["BotTester", "Calls", "CallsList", "MockedBot", "MockedRequester"]
|
|
8
|
+
__version__ = "1.3.0" # x-release-please-version
|
aiogram_testing-1.2.0/aiogram_tests/requester.py → aiogram_testing-1.3.0/aiogram_tests/calls.py
RENAMED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
1
2
|
from collections.abc import Iterator
|
|
2
3
|
from typing import Any
|
|
3
4
|
from typing import TypeVar
|
|
4
5
|
|
|
5
6
|
from aiogram.methods import TelegramMethod
|
|
6
|
-
from aiogram.methods.base import Response
|
|
7
|
-
from aiogram.methods.base import TelegramType
|
|
8
7
|
|
|
9
8
|
from .exceptions import MethodIsNotCalledError
|
|
10
|
-
from .handler.base import RequestHandler
|
|
11
9
|
from .utils import camel_case2snake_case
|
|
12
10
|
|
|
13
11
|
M = TypeVar("M", bound=TelegramMethod)
|
|
@@ -32,8 +30,11 @@ class Calls:
|
|
|
32
30
|
the IDE and type checkers understand.
|
|
33
31
|
"""
|
|
34
32
|
|
|
35
|
-
def __init__(self, methods:
|
|
33
|
+
def __init__(self, methods: Iterable[TelegramMethod] = (), result: Any = None):
|
|
36
34
|
self._methods = list(methods)
|
|
35
|
+
# What the dispatcher returned for the update: a handler's return value, UNHANDLED, True after an error
|
|
36
|
+
# handler...
|
|
37
|
+
self.result = result
|
|
37
38
|
# Same list on every access: tests pop() from calls.send_message
|
|
38
39
|
self._groups: dict[str, CallsList] = {}
|
|
39
40
|
for m in self._methods:
|
|
@@ -84,47 +85,3 @@ class Calls:
|
|
|
84
85
|
if calls:
|
|
85
86
|
made = "\n".join(f" {call!r}" for call in calls)
|
|
86
87
|
raise AssertionError(f"{method.__name__} was called {len(calls)} time(s):\n{made}")
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
class MockedRequester:
|
|
90
|
-
def __init__(self, request_handler: RequestHandler):
|
|
91
|
-
self._handler: RequestHandler = request_handler
|
|
92
|
-
|
|
93
|
-
async def query(self, *args, **kwargs) -> Calls:
|
|
94
|
-
self._check_arguments(*args, **kwargs)
|
|
95
|
-
|
|
96
|
-
methods = self._handler.bot.session.methods
|
|
97
|
-
already_made = len(methods)
|
|
98
|
-
await self._handler(*args, **kwargs)
|
|
99
|
-
|
|
100
|
-
return Calls(list(methods)[already_made:])
|
|
101
|
-
|
|
102
|
-
def add_result_for(
|
|
103
|
-
self,
|
|
104
|
-
method: type[TelegramMethod[TelegramType]],
|
|
105
|
-
ok: bool,
|
|
106
|
-
result: TelegramType = None,
|
|
107
|
-
description: str | None = None,
|
|
108
|
-
error_code: int = 200,
|
|
109
|
-
migrate_to_chat_id: int | None = None,
|
|
110
|
-
retry_after: int | None = None,
|
|
111
|
-
) -> Response[TelegramType]:
|
|
112
|
-
response = self._handler.add_result_for(
|
|
113
|
-
method=method,
|
|
114
|
-
ok=ok,
|
|
115
|
-
result=result,
|
|
116
|
-
description=description,
|
|
117
|
-
error_code=error_code,
|
|
118
|
-
migrate_to_chat_id=migrate_to_chat_id,
|
|
119
|
-
retry_after=retry_after,
|
|
120
|
-
)
|
|
121
|
-
return response
|
|
122
|
-
|
|
123
|
-
def _check_arguments(self, *args, **kwargs) -> None:
|
|
124
|
-
build_update = getattr(self._handler, "build_update", None)
|
|
125
|
-
if build_update is None:
|
|
126
|
-
return
|
|
127
|
-
try:
|
|
128
|
-
build_update(*args, **kwargs)
|
|
129
|
-
except TypeError as e:
|
|
130
|
-
raise AttributeError(f"incorrect argument name. {e}") from e
|
|
@@ -65,7 +65,7 @@ class TelegramEventObserverHandler(RequestHandler):
|
|
|
65
65
|
await state.set_state(self._state)
|
|
66
66
|
await state.update_data(**self._state_data)
|
|
67
67
|
|
|
68
|
-
await self.dp.feed_update(self.bot, update)
|
|
68
|
+
return await self.dp.feed_update(self.bot, update)
|
|
69
69
|
|
|
70
70
|
def _get_state_context(self, update: types.Update) -> FSMContext:
|
|
71
71
|
"""
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
import typing
|
|
1
3
|
from collections import deque
|
|
2
4
|
from collections.abc import AsyncGenerator
|
|
5
|
+
from datetime import UTC
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Any
|
|
3
8
|
|
|
4
9
|
from aiogram import Bot
|
|
5
10
|
from aiogram.client.session.base import BaseSession
|
|
@@ -7,10 +12,13 @@ from aiogram.methods import TelegramMethod
|
|
|
7
12
|
from aiogram.methods.base import Request
|
|
8
13
|
from aiogram.methods.base import Response
|
|
9
14
|
from aiogram.methods.base import TelegramType
|
|
15
|
+
from aiogram.types import Chat
|
|
16
|
+
from aiogram.types import Message
|
|
10
17
|
from aiogram.types import ResponseParameters
|
|
11
18
|
from aiogram.types import User
|
|
12
19
|
from aiogram.types.base import UNSET_TYPE
|
|
13
20
|
|
|
21
|
+
from .calls import Calls
|
|
14
22
|
from .exceptions import MockedResponseMissingError
|
|
15
23
|
|
|
16
24
|
DEFAULT_AUTO_MOCK_SUCCESS = True
|
|
@@ -47,6 +55,9 @@ class MockedSession(BaseSession):
|
|
|
47
55
|
request = Request(method=method.__api_method__, data=method.__dict__, files=None)
|
|
48
56
|
self.requests.append(request)
|
|
49
57
|
self.methods.append(method)
|
|
58
|
+
if not self.responses and getattr(bot, "auto_mock_success", False):
|
|
59
|
+
# Answer at request time, so a failure queued earlier is never followed by a stale success
|
|
60
|
+
bot.add_result_for(type(method), ok=True, result=bot.default_result(method))
|
|
50
61
|
if not self.responses:
|
|
51
62
|
raise MockedResponseMissingError(
|
|
52
63
|
f"no mocked response for {method.__api_method__}: call "
|
|
@@ -79,6 +90,7 @@ class MockedBot(Bot):
|
|
|
79
90
|
language_code="ru",
|
|
80
91
|
)
|
|
81
92
|
self.auto_mock_success = auto_mock_success
|
|
93
|
+
self._message_ids = itertools.count(1)
|
|
82
94
|
|
|
83
95
|
def add_result_for(
|
|
84
96
|
self,
|
|
@@ -103,10 +115,39 @@ class MockedBot(Bot):
|
|
|
103
115
|
self.session.add_result(response)
|
|
104
116
|
return response
|
|
105
117
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
118
|
+
@property
|
|
119
|
+
def calls(self) -> Calls:
|
|
120
|
+
"""
|
|
121
|
+
Every Bot API call this bot made, including ones outside the dispatcher (background jobs, notifiers)
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
return Calls(self.session.methods)
|
|
125
|
+
|
|
126
|
+
def default_result(self, method: TelegramMethod) -> Any:
|
|
127
|
+
"""
|
|
128
|
+
What Telegram would plausibly answer to ``method`` when no result was queued: the sent or edited message
|
|
129
|
+
for message methods, ``True`` for boolean ones, the bot itself for ``getMe``
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
returning = method.__returning__
|
|
133
|
+
options = typing.get_args(returning) or (returning,)
|
|
134
|
+
if User in options:
|
|
135
|
+
return self._me
|
|
136
|
+
if Message in options and getattr(method, "inline_message_id", None) is None:
|
|
137
|
+
chat_id = getattr(method, "chat_id", None)
|
|
138
|
+
return Message(
|
|
139
|
+
message_id=getattr(method, "message_id", None) or next(self._message_ids),
|
|
140
|
+
date=datetime.now(UTC),
|
|
141
|
+
chat=Chat(id=chat_id if isinstance(chat_id, int) else -1, type="private"),
|
|
142
|
+
from_user=self._me,
|
|
143
|
+
text=getattr(method, "text", None),
|
|
144
|
+
caption=getattr(method, "caption", None),
|
|
145
|
+
)
|
|
146
|
+
if bool in options:
|
|
147
|
+
return True
|
|
148
|
+
if typing.get_origin(returning) is list:
|
|
149
|
+
return []
|
|
150
|
+
return None
|
|
110
151
|
|
|
111
152
|
def get_request(self) -> Request:
|
|
112
153
|
return self.session.get_request()
|
|
File without changes
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from aiogram.methods import TelegramMethod
|
|
2
|
+
from aiogram.methods.base import Response
|
|
3
|
+
from aiogram.methods.base import TelegramType
|
|
4
|
+
|
|
5
|
+
from .calls import Calls
|
|
6
|
+
from .calls import CallsList
|
|
7
|
+
from .handler.base import RequestHandler
|
|
8
|
+
|
|
9
|
+
__all__ = ["Calls", "CallsList", "MockedRequester"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MockedRequester:
|
|
13
|
+
def __init__(self, request_handler: RequestHandler):
|
|
14
|
+
self._handler: RequestHandler = request_handler
|
|
15
|
+
|
|
16
|
+
async def query(self, *args, **kwargs) -> Calls:
|
|
17
|
+
self._check_arguments(*args, **kwargs)
|
|
18
|
+
|
|
19
|
+
methods = self._handler.bot.session.methods
|
|
20
|
+
already_made = len(methods)
|
|
21
|
+
result = await self._handler(*args, **kwargs)
|
|
22
|
+
|
|
23
|
+
return Calls(list(methods)[already_made:], result=result)
|
|
24
|
+
|
|
25
|
+
def add_result_for(
|
|
26
|
+
self,
|
|
27
|
+
method: type[TelegramMethod[TelegramType]],
|
|
28
|
+
ok: bool,
|
|
29
|
+
result: TelegramType = None,
|
|
30
|
+
description: str | None = None,
|
|
31
|
+
error_code: int = 200,
|
|
32
|
+
migrate_to_chat_id: int | None = None,
|
|
33
|
+
retry_after: int | None = None,
|
|
34
|
+
) -> Response[TelegramType]:
|
|
35
|
+
response = self._handler.add_result_for(
|
|
36
|
+
method=method,
|
|
37
|
+
ok=ok,
|
|
38
|
+
result=result,
|
|
39
|
+
description=description,
|
|
40
|
+
error_code=error_code,
|
|
41
|
+
migrate_to_chat_id=migrate_to_chat_id,
|
|
42
|
+
retry_after=retry_after,
|
|
43
|
+
)
|
|
44
|
+
return response
|
|
45
|
+
|
|
46
|
+
def _check_arguments(self, *args, **kwargs) -> None:
|
|
47
|
+
build_update = getattr(self._handler, "build_update", None)
|
|
48
|
+
if build_update is None:
|
|
49
|
+
return
|
|
50
|
+
try:
|
|
51
|
+
build_update(*args, **kwargs)
|
|
52
|
+
except TypeError as e:
|
|
53
|
+
raise AttributeError(f"incorrect argument name. {e}") from e
|
|
@@ -12,9 +12,9 @@ from aiogram.methods import TelegramMethod
|
|
|
12
12
|
from aiogram.methods.base import Response
|
|
13
13
|
from aiogram.methods.base import TelegramType
|
|
14
14
|
|
|
15
|
+
from .calls import Calls
|
|
15
16
|
from .mocked_bot import DEFAULT_AUTO_MOCK_SUCCESS
|
|
16
17
|
from .mocked_bot import MockedBot
|
|
17
|
-
from .requester import Calls
|
|
18
18
|
from .types.dataset import CALLBACK_QUERY
|
|
19
19
|
from .types.dataset import CHAT
|
|
20
20
|
from .types.dataset import MESSAGE
|
|
@@ -81,8 +81,8 @@ class BotTester:
|
|
|
81
81
|
|
|
82
82
|
methods = self.bot.session.methods
|
|
83
83
|
already_made = len(methods)
|
|
84
|
-
await self.dp.feed_update(self.bot, update, **self._workflow_data)
|
|
85
|
-
self._last_calls = Calls(list(methods)[already_made:])
|
|
84
|
+
result = await self.dp.feed_update(self.bot, update, **self._workflow_data)
|
|
85
|
+
self._last_calls = Calls(list(methods)[already_made:], result=result)
|
|
86
86
|
return self._last_calls
|
|
87
87
|
|
|
88
88
|
async def send_message(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|