Fixed tests, covered flags and events isolation modules

This commit is contained in:
Alex Root Junior 2022-02-16 00:38:29 +02:00
parent 84be7d8323
commit 4257f7db57
No known key found for this signature in database
GPG key ID: 074C1D455EBEA4AC
17 changed files with 298 additions and 33 deletions

View file

@ -3,3 +3,4 @@ exclude_lines =
pragma: no cover
if TYPE_CHECKING:
@abstractmethod
@overload

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -104,5 +104,6 @@ class BaseEventIsolation(ABC):
"""
yield None
@abstractmethod
async def close(self) -> None:
pass

View file

@ -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()

View file

@ -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

View file

@ -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()

View file

@ -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,

View 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

View 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

View 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?"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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: