mirror of
https://github.com/aiogram/aiogram.git
synced 2026-04-08 16:37:47 +00:00
Fixed tests, covered flags and events isolation modules
This commit is contained in:
parent
84be7d8323
commit
4257f7db57
17 changed files with 298 additions and 33 deletions
|
|
@ -3,3 +3,4 @@ exclude_lines =
|
|||
pragma: no cover
|
||||
if TYPE_CHECKING:
|
||||
@abstractmethod
|
||||
@overload
|
||||
|
|
|
|||
2
Makefile
2
Makefile
|
|
@ -84,7 +84,7 @@ reformat:
|
|||
# =================================================================================================
|
||||
.PHONY: test-run-services
|
||||
test-run-services:
|
||||
docker-compose -f tests/docker-compose.yml -p aiogram3-dev up -d
|
||||
@#docker-compose -f tests/docker-compose.yml -p aiogram3-dev up -d
|
||||
|
||||
.PHONY: test
|
||||
test: test-run-services
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from .client import session
|
|||
from .client.bot import Bot
|
||||
from .dispatcher import filters, handler
|
||||
from .dispatcher.dispatcher import Dispatcher
|
||||
from .dispatcher.flags.flags import FlagGenerator
|
||||
from .dispatcher.flags.flag import FlagGenerator
|
||||
from .dispatcher.middlewares.base import BaseMiddleware
|
||||
from .dispatcher.router import Router
|
||||
from .utils.magic_filter import MagicFilter
|
||||
|
|
|
|||
|
|
@ -6,19 +6,23 @@ from magic_filter import AttrDict
|
|||
from aiogram.dispatcher.flags.getter import extract_flags_from_object
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(frozen=True)
|
||||
class Flag:
|
||||
name: str
|
||||
value: Any
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(frozen=True)
|
||||
class FlagDecorator:
|
||||
flag: Flag
|
||||
|
||||
def with_value(self, value: Any) -> "FlagDecorator":
|
||||
@classmethod
|
||||
def _with_flag(cls, flag: Flag) -> "FlagDecorator":
|
||||
return cls(flag)
|
||||
|
||||
def _with_value(self, value: Any) -> "FlagDecorator":
|
||||
new_flag = Flag(self.flag.name, value)
|
||||
return type(self)(new_flag)
|
||||
return self._with_flag(new_flag)
|
||||
|
||||
@overload
|
||||
def __call__(self, value: Callable[..., Any]) -> Callable[..., Any]: # type: ignore
|
||||
|
|
@ -46,7 +50,7 @@ class FlagDecorator:
|
|||
self.flag.name: self.flag.value,
|
||||
}
|
||||
return cast(Callable[..., Any], value)
|
||||
return self.with_value(AttrDict(kwargs) if value is None else value)
|
||||
return self._with_value(AttrDict(kwargs) if value is None else value)
|
||||
|
||||
|
||||
class FlagGenerator:
|
||||
|
|
@ -104,5 +104,6 @@ class BaseEventIsolation(ABC):
|
|||
"""
|
||||
yield None
|
||||
|
||||
@abstractmethod
|
||||
async def close(self) -> None:
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -56,6 +56,9 @@ class DisabledEventIsolation(BaseEventIsolation):
|
|||
async def lock(self, bot: Bot, key: StorageKey) -> AsyncGenerator[None, None]:
|
||||
yield
|
||||
|
||||
async def close(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class SimpleEventIsolation(BaseEventIsolation):
|
||||
def __init__(self) -> None:
|
||||
|
|
@ -67,3 +70,6 @@ class SimpleEventIsolation(BaseEventIsolation):
|
|||
lock = self._locks[key]
|
||||
async with lock:
|
||||
yield
|
||||
|
||||
async def close(self) -> None:
|
||||
self._locks.clear()
|
||||
|
|
|
|||
|
|
@ -194,15 +194,17 @@ class RedisEventIsolation(BaseEventIsolation):
|
|||
def __init__(
|
||||
self,
|
||||
redis: Redis,
|
||||
key_builder: KeyBuilder,
|
||||
key_builder: Optional[KeyBuilder] = None,
|
||||
lock_kwargs: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
if key_builder is None:
|
||||
key_builder = DefaultKeyBuilder()
|
||||
self.redis = redis
|
||||
self.key_builder = key_builder
|
||||
self.lock_kwargs = lock_kwargs or {}
|
||||
|
||||
@classmethod
|
||||
async def from_redis(
|
||||
def from_url(
|
||||
cls,
|
||||
url: str,
|
||||
connection_kwargs: Optional[Dict[str, Any]] = None,
|
||||
|
|
@ -223,3 +225,6 @@ class RedisEventIsolation(BaseEventIsolation):
|
|||
redis_key = self.key_builder.build(key, "lock")
|
||||
async with self.redis.lock(name=redis_key, **self.lock_kwargs):
|
||||
yield None
|
||||
|
||||
async def close(self) -> None:
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -4,9 +4,13 @@ import pytest
|
|||
from _pytest.config import UsageError
|
||||
from aioredis.connection import parse_url as parse_redis_url
|
||||
|
||||
from aiogram import Bot
|
||||
from aiogram.dispatcher.fsm.storage.memory import MemoryStorage
|
||||
from aiogram.dispatcher.fsm.storage.redis import RedisStorage
|
||||
from aiogram import Bot, Dispatcher
|
||||
from aiogram.dispatcher.fsm.storage.memory import (
|
||||
DisabledEventIsolation,
|
||||
MemoryStorage,
|
||||
SimpleEventIsolation,
|
||||
)
|
||||
from aiogram.dispatcher.fsm.storage.redis import RedisEventIsolation, RedisStorage
|
||||
from tests.mocked_bot import MockedBot
|
||||
|
||||
DATA_DIR = Path(__file__).parent / "data"
|
||||
|
|
@ -67,6 +71,42 @@ async def memory_storage():
|
|||
await storage.close()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
@pytest.mark.redis
|
||||
async def redis_isolation(redis_server):
|
||||
if not redis_server:
|
||||
pytest.skip("Redis is not available here")
|
||||
isolation = RedisEventIsolation.from_url(redis_server)
|
||||
try:
|
||||
await isolation.redis.info()
|
||||
except ConnectionError as e:
|
||||
pytest.skip(str(e))
|
||||
try:
|
||||
yield isolation
|
||||
finally:
|
||||
conn = await isolation.redis
|
||||
await conn.flushdb()
|
||||
await isolation.close()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
async def lock_isolation():
|
||||
isolation = SimpleEventIsolation()
|
||||
try:
|
||||
yield isolation
|
||||
finally:
|
||||
await isolation.close()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
async def disabled_isolation():
|
||||
isolation = DisabledEventIsolation()
|
||||
try:
|
||||
yield isolation
|
||||
finally:
|
||||
await isolation.close()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def bot():
|
||||
bot = MockedBot()
|
||||
|
|
@ -75,3 +115,13 @@ def bot():
|
|||
yield bot
|
||||
finally:
|
||||
Bot.reset_current(token)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
async def dispatcher():
|
||||
dp = Dispatcher()
|
||||
await dp.emit_startup()
|
||||
try:
|
||||
yield dp
|
||||
finally:
|
||||
await dp.emit_shutdown()
|
||||
|
|
|
|||
|
|
@ -76,20 +76,15 @@ class TestDispatcher:
|
|||
assert dp.update.handlers[0].callback == dp._listen_update
|
||||
assert dp.update.outer_middlewares
|
||||
|
||||
def test_parent_router(self):
|
||||
dp = Dispatcher()
|
||||
def test_parent_router(self, dispatcher: Dispatcher):
|
||||
with pytest.raises(RuntimeError):
|
||||
dp.parent_router = Router()
|
||||
assert dp.parent_router is None
|
||||
dp._parent_router = Router()
|
||||
assert dp.parent_router is None
|
||||
dispatcher.parent_router = Router()
|
||||
assert dispatcher.parent_router is None
|
||||
dispatcher._parent_router = Router()
|
||||
assert dispatcher.parent_router is None
|
||||
|
||||
@pytest.mark.parametrize("isolate_events", (True, False))
|
||||
async def test_feed_update(self, isolate_events):
|
||||
dp = Dispatcher(isolate_events=isolate_events)
|
||||
bot = Bot("42:TEST")
|
||||
|
||||
@dp.message()
|
||||
async def test_feed_update(self, dispatcher: Dispatcher, bot: MockedBot):
|
||||
@dispatcher.message()
|
||||
async def my_handler(message: Message, **kwargs):
|
||||
assert "bot" in kwargs
|
||||
assert isinstance(kwargs["bot"], Bot)
|
||||
|
|
@ -97,7 +92,7 @@ class TestDispatcher:
|
|||
return message.text
|
||||
|
||||
results_count = 0
|
||||
result = await dp.feed_update(
|
||||
result = await dispatcher.feed_update(
|
||||
bot=bot,
|
||||
update=Update(
|
||||
update_id=42,
|
||||
|
|
|
|||
0
tests/test_dispatcher/test_flags/__init__.py
Normal file
0
tests/test_dispatcher/test_flags/__init__.py
Normal file
66
tests/test_dispatcher/test_flags/test_decorator.py
Normal file
66
tests/test_dispatcher/test_flags/test_decorator.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import pytest
|
||||
|
||||
from aiogram.dispatcher.flags.flag import Flag, FlagDecorator, FlagGenerator
|
||||
|
||||
|
||||
@pytest.fixture(name="flag")
|
||||
def flag_fixture() -> Flag:
|
||||
return Flag("test", True)
|
||||
|
||||
|
||||
@pytest.fixture(name="flag_decorator")
|
||||
def flag_decorator_fixture(flag: Flag) -> FlagDecorator:
|
||||
return FlagDecorator(flag)
|
||||
|
||||
|
||||
@pytest.fixture(name="flag_generator")
|
||||
def flag_flag_generator() -> FlagGenerator:
|
||||
return FlagGenerator()
|
||||
|
||||
|
||||
class TestFlagDecorator:
|
||||
def test_with_value(self, flag_decorator: FlagDecorator):
|
||||
new_decorator = flag_decorator._with_value(True)
|
||||
|
||||
assert new_decorator is not flag_decorator
|
||||
assert new_decorator.flag is not flag_decorator.flag
|
||||
assert new_decorator.flag
|
||||
|
||||
def test_call_invalid(self, flag_decorator: FlagDecorator):
|
||||
with pytest.raises(ValueError):
|
||||
flag_decorator(True, test=True)
|
||||
|
||||
def test_call_with_function(self, flag_decorator: FlagDecorator):
|
||||
def func():
|
||||
pass
|
||||
|
||||
decorated = flag_decorator(func)
|
||||
assert decorated is func
|
||||
assert hasattr(decorated, "aiogram_flag")
|
||||
|
||||
def test_call_with_arg(self, flag_decorator: FlagDecorator):
|
||||
new_decorator = flag_decorator("hello")
|
||||
assert new_decorator is not flag_decorator
|
||||
assert new_decorator.flag.value == "hello"
|
||||
|
||||
def test_call_with_kwargs(self, flag_decorator: FlagDecorator):
|
||||
new_decorator = flag_decorator(test=True)
|
||||
assert new_decorator is not flag_decorator
|
||||
assert isinstance(new_decorator.flag.value, dict)
|
||||
assert "test" in new_decorator.flag.value
|
||||
|
||||
|
||||
class TestFlagGenerator:
|
||||
def test_getattr(self):
|
||||
generator = FlagGenerator()
|
||||
assert isinstance(generator.foo, FlagDecorator)
|
||||
assert isinstance(generator.bar, FlagDecorator)
|
||||
|
||||
assert generator.foo is not generator.foo
|
||||
assert generator.foo is not generator.bar
|
||||
|
||||
def test_failed_getattr(self):
|
||||
generator = FlagGenerator()
|
||||
|
||||
with pytest.raises(AttributeError):
|
||||
generator._something
|
||||
64
tests/test_dispatcher/test_flags/test_getter.py
Normal file
64
tests/test_dispatcher/test_flags/test_getter.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from aiogram import F
|
||||
from aiogram.dispatcher.event.handler import HandlerObject
|
||||
from aiogram.dispatcher.flags.getter import (
|
||||
check_flags,
|
||||
extract_flags,
|
||||
extract_flags_from_object,
|
||||
get_flag,
|
||||
)
|
||||
|
||||
|
||||
class TestGetters:
|
||||
def test_extract_flags_from_object(self):
|
||||
def func():
|
||||
pass
|
||||
|
||||
assert extract_flags_from_object(func) == {}
|
||||
|
||||
func.aiogram_flag = {"test": True}
|
||||
assert extract_flags_from_object(func) == func.aiogram_flag
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"obj,result",
|
||||
[
|
||||
[None, {}],
|
||||
[{}, {}],
|
||||
[{"handler": None}, {}],
|
||||
[{"handler": HandlerObject(lambda: True, flags={"test": True})}, {"test": True}],
|
||||
],
|
||||
)
|
||||
def test_extract_flags(self, obj, result):
|
||||
assert extract_flags(obj) == result
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"obj,name,default,result",
|
||||
[
|
||||
[None, "test", None, None],
|
||||
[None, "test", 42, 42],
|
||||
[{}, "test", None, None],
|
||||
[{}, "test", 42, 42],
|
||||
[{"handler": None}, "test", None, None],
|
||||
[{"handler": None}, "test", 42, 42],
|
||||
[{"handler": HandlerObject(lambda: True, flags={"test": True})}, "test", None, True],
|
||||
[{"handler": HandlerObject(lambda: True, flags={"test": True})}, "test2", None, None],
|
||||
[{"handler": HandlerObject(lambda: True, flags={"test": True})}, "test2", 42, 42],
|
||||
],
|
||||
)
|
||||
def test_get_flag(self, obj, name, default, result):
|
||||
assert get_flag(obj, name, default=default) == result
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"flags,magic,result",
|
||||
[
|
||||
[{}, F.test, None],
|
||||
[{"test": True}, F.test, True],
|
||||
[{"test": True}, F.spam, None],
|
||||
],
|
||||
)
|
||||
def test_check_flag(self, flags, magic, result):
|
||||
with patch("aiogram.dispatcher.flags.getter.extract_flags", return_value=flags):
|
||||
assert check_flags(object(), magic) == result
|
||||
30
tests/test_dispatcher/test_fsm/storage/test_isolation.py
Normal file
30
tests/test_dispatcher/test_fsm/storage/test_isolation.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import pytest
|
||||
|
||||
from aiogram.dispatcher.fsm.storage.base import BaseEventIsolation, StorageKey
|
||||
from tests.mocked_bot import MockedBot
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
||||
@pytest.fixture(name="storage_key")
|
||||
def create_storate_key(bot: MockedBot):
|
||||
return StorageKey(chat_id=-42, user_id=42, bot_id=bot.id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"isolation",
|
||||
[
|
||||
pytest.lazy_fixture("redis_isolation"),
|
||||
pytest.lazy_fixture("lock_isolation"),
|
||||
pytest.lazy_fixture("disabled_isolation"),
|
||||
],
|
||||
)
|
||||
class TestIsolations:
|
||||
async def test_lock(
|
||||
self,
|
||||
bot: MockedBot,
|
||||
isolation: BaseEventIsolation,
|
||||
storage_key: StorageKey,
|
||||
):
|
||||
async with isolation.lock(bot=bot, key=storage_key):
|
||||
assert True, "You are kidding me?"
|
||||
|
|
@ -1,7 +1,11 @@
|
|||
import pytest
|
||||
|
||||
from aiogram.dispatcher.fsm.storage.base import DEFAULT_DESTINY, StorageKey
|
||||
from aiogram.dispatcher.fsm.storage.redis import DefaultKeyBuilder
|
||||
from aiogram.dispatcher.fsm.storage.redis import (
|
||||
DefaultKeyBuilder,
|
||||
RedisEventIsolation,
|
||||
RedisStorage,
|
||||
)
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
|
@ -43,3 +47,11 @@ class TestRedisDefaultKeyBuilder:
|
|||
)
|
||||
with pytest.raises(ValueError):
|
||||
key_builder.build(key, FIELD)
|
||||
|
||||
def test_create_isolation(self):
|
||||
fake_redis = object()
|
||||
storage = RedisStorage(redis=fake_redis)
|
||||
isolation = storage.create_isolation()
|
||||
assert isinstance(isolation, RedisEventIsolation)
|
||||
assert isolation.redis is fake_redis
|
||||
assert isolation.key_builder is storage.key_builder
|
||||
|
|
|
|||
|
|
@ -16,11 +16,6 @@ def create_storate_key(bot: MockedBot):
|
|||
[pytest.lazy_fixture("redis_storage"), pytest.lazy_fixture("memory_storage")],
|
||||
)
|
||||
class TestStorages:
|
||||
async def test_lock(self, bot: MockedBot, storage: BaseStorage, storage_key: StorageKey):
|
||||
# TODO: ?!?
|
||||
async with storage.lock(bot=bot, key=storage_key):
|
||||
assert True, "You are kidding me?"
|
||||
|
||||
async def test_set_state(self, bot: MockedBot, storage: BaseStorage, storage_key: StorageKey):
|
||||
assert await storage.get_state(bot=bot, key=storage_key) is None
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import asyncio
|
||||
import time
|
||||
from asyncio import Event
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict
|
||||
|
||||
|
|
@ -19,6 +21,12 @@ from aiogram.methods import GetMe, Request
|
|||
from aiogram.types import Message, User
|
||||
from tests.mocked_bot import MockedBot
|
||||
|
||||
try:
|
||||
from asynctest import CoroutineMock, patch
|
||||
except ImportError:
|
||||
from unittest.mock import AsyncMock as CoroutineMock # type: ignore
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
class TestAiohttpServer:
|
||||
def test_setup_application(self):
|
||||
|
|
@ -74,8 +82,11 @@ class TestSimpleRequestHandler:
|
|||
app = Application()
|
||||
dp = Dispatcher()
|
||||
|
||||
handler_event = Event()
|
||||
|
||||
@dp.message(F.text == "test")
|
||||
def handle_message(msg: Message):
|
||||
handler_event.set()
|
||||
return msg.answer("PASS")
|
||||
|
||||
handler = SimpleRequestHandler(
|
||||
|
|
@ -97,8 +108,15 @@ class TestSimpleRequestHandler:
|
|||
assert not result
|
||||
|
||||
handler.handle_in_background = True
|
||||
resp = await self.make_reqest(client=client)
|
||||
assert resp.status == 200
|
||||
with patch(
|
||||
"aiogram.dispatcher.dispatcher.Dispatcher.silent_call_request",
|
||||
new_callable=CoroutineMock,
|
||||
) as mocked_silent_call_request:
|
||||
handler_event.clear()
|
||||
resp = await self.make_reqest(client=client)
|
||||
assert resp.status == 200
|
||||
await asyncio.wait_for(handler_event.wait(), timeout=1)
|
||||
mocked_silent_call_request.assert_awaited()
|
||||
result = await resp.json()
|
||||
assert not result
|
||||
|
||||
|
|
|
|||
|
|
@ -114,6 +114,24 @@ class TestSimpleI18nMiddleware:
|
|||
assert middleware not in dp.update.outer_middlewares
|
||||
assert middleware in dp.message.outer_middlewares
|
||||
|
||||
async def test_get_unknown_locale(self, i18n: I18n):
|
||||
dp = Dispatcher()
|
||||
middleware = SimpleI18nMiddleware(i18n=i18n)
|
||||
middleware.setup(router=dp)
|
||||
|
||||
locale = await middleware.get_locale(
|
||||
None,
|
||||
{
|
||||
"event_from_user": User(
|
||||
id=42,
|
||||
is_bot=False,
|
||||
first_name="Test",
|
||||
language_code="unknown",
|
||||
)
|
||||
},
|
||||
)
|
||||
assert locale == i18n.default_locale
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestConstI18nMiddleware:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue