Backport RedisStorage, deep-linking

This commit is contained in:
Alex Root Junior 2021-06-13 02:45:16 +03:00
parent bc96bdd3b6
commit 988d55ff65
30 changed files with 852 additions and 183 deletions

View file

@ -1,9 +1,62 @@
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 tests.mocked_bot import MockedBot
def pytest_addoption(parser):
parser.addoption("--redis", default=None, help="run tests which require redis connection")
def pytest_configure(config):
config.addinivalue_line("markers", "redis: marked tests require redis connection to run")
def pytest_collection_modifyitems(config, items):
redis_uri = config.getoption("--redis")
if redis_uri is None:
skip_redis = pytest.mark.skip(reason="need --redis option with redis URI to run")
for item in items:
if "redis" in item.keywords:
item.add_marker(skip_redis)
return
try:
parse_redis_url(redis_uri)
except ValueError as e:
raise UsageError(f"Invalid redis URI {redis_uri!r}: {e}")
@pytest.fixture(scope="session")
def redis_server(request):
redis_uri = request.config.getoption("--redis")
return redis_uri
@pytest.fixture()
@pytest.mark.redis
async def redis_storage(redis_server):
storage = RedisStorage.from_url(redis_server)
try:
yield storage
finally:
conn = await storage.redis
await conn.flushdb()
await storage.close()
@pytest.fixture()
async def memory_storage():
storage = MemoryStorage()
try:
yield storage
finally:
await storage.close()
@pytest.fixture()
def bot():
bot = MockedBot()

7
tests/docker-compose.yml Normal file
View file

@ -0,0 +1,7 @@
version: "3.9"
services:
redis:
image: redis:6-alpine
ports:
- "${REDIS_PORT-6379}:6379"

View file

@ -2,8 +2,8 @@ import datetime
from typing import Optional
import pytest
from aiogram.types import Chat, Message
from aiogram.types import Chat, Message
from tests.mocked_bot import MockedBot

View file

@ -423,7 +423,7 @@ class TestDispatcher:
assert User.get_current(False)
return kwargs
result = await router.update.trigger(update, test="PASS")
result = await router.update.trigger(update, test="PASS", bot=None)
assert isinstance(result, dict)
assert result["event_update"] == update
assert result["event_router"] == router
@ -526,8 +526,9 @@ class TestDispatcher:
assert len(log_records) == 1
assert "Cause exception while process update" in log_records[0]
@pytest.mark.parametrize("as_task", [True, False])
@pytest.mark.asyncio
async def test_polling(self, bot: MockedBot):
async def test_polling(self, bot: MockedBot, as_task: bool):
dispatcher = Dispatcher()
async def _mock_updates(*_):
@ -539,8 +540,11 @@ class TestDispatcher:
"aiogram.dispatcher.dispatcher.Dispatcher._listen_updates"
) as patched_listen_updates:
patched_listen_updates.return_value = _mock_updates()
await dispatcher._polling(bot=bot)
mocked_process_update.assert_awaited()
await dispatcher._polling(bot=bot, handle_as_tasks=as_task)
if as_task:
pass
else:
mocked_process_update.assert_awaited()
@pytest.mark.asyncio
async def test_exception_handler_catch_exceptions(self):
@ -548,9 +552,12 @@ class TestDispatcher:
router = Router()
dp.include_router(router)
class CustomException(Exception):
pass
@router.message()
async def message_handler(message: Message):
raise Exception("KABOOM")
raise CustomException("KABOOM")
update = Update(
update_id=42,
@ -562,23 +569,23 @@ class TestDispatcher:
from_user=User(id=42, is_bot=False, first_name="Test"),
),
)
with pytest.raises(Exception, match="KABOOM"):
await dp.update.trigger(update)
with pytest.raises(CustomException, match="KABOOM"):
await dp.update.trigger(update, bot=None)
@router.errors()
async def error_handler(event: Update, exception: Exception):
return "KABOOM"
response = await dp.update.trigger(update)
response = await dp.update.trigger(update, bot=None)
assert response == "KABOOM"
@dp.errors()
async def root_error_handler(event: Update, exception: Exception):
return exception
response = await dp.update.trigger(update)
response = await dp.update.trigger(update, bot=None)
assert isinstance(response, Exception)
assert isinstance(response, CustomException)
assert str(response) == "KABOOM"
@pytest.mark.asyncio
@ -654,20 +661,3 @@ class TestDispatcher:
log_records = [rec.message for rec in caplog.records]
assert "Cause exception while process update" in log_records[0]
@pytest.mark.parametrize(
"strategy,case,expected",
[
[FSMStrategy.USER_IN_CHAT, (-42, 42), (-42, 42)],
[FSMStrategy.CHAT, (-42, 42), (-42, -42)],
[FSMStrategy.GLOBAL_USER, (-42, 42), (42, 42)],
[FSMStrategy.USER_IN_CHAT, (42, 42), (42, 42)],
[FSMStrategy.CHAT, (42, 42), (42, 42)],
[FSMStrategy.GLOBAL_USER, (42, 42), (42, 42)],
],
)
def test_get_current_state_context(self, strategy, case, expected):
dp = Dispatcher(fsm_strategy=strategy)
chat_id, user_id = case
state = dp.current_state(chat_id=chat_id, user_id=user_id)
assert (state.chat_id, state.user_id) == expected

View file

@ -1,45 +0,0 @@
import pytest
from aiogram.dispatcher.fsm.storage.memory import MemoryStorage, MemoryStorageRecord
@pytest.fixture()
def storage():
return MemoryStorage()
class TestMemoryStorage:
@pytest.mark.asyncio
async def test_set_state(self, storage: MemoryStorage):
assert await storage.get_state(chat_id=-42, user_id=42) is None
await storage.set_state(chat_id=-42, user_id=42, state="state")
assert await storage.get_state(chat_id=-42, user_id=42) == "state"
assert -42 in storage.storage
assert 42 in storage.storage[-42]
assert isinstance(storage.storage[-42][42], MemoryStorageRecord)
assert storage.storage[-42][42].state == "state"
@pytest.mark.asyncio
async def test_set_data(self, storage: MemoryStorage):
assert await storage.get_data(chat_id=-42, user_id=42) == {}
await storage.set_data(chat_id=-42, user_id=42, data={"foo": "bar"})
assert await storage.get_data(chat_id=-42, user_id=42) == {"foo": "bar"}
assert -42 in storage.storage
assert 42 in storage.storage[-42]
assert isinstance(storage.storage[-42][42], MemoryStorageRecord)
assert storage.storage[-42][42].data == {"foo": "bar"}
@pytest.mark.asyncio
async def test_update_data(self, storage: MemoryStorage):
assert await storage.get_data(chat_id=-42, user_id=42) == {}
assert await storage.update_data(chat_id=-42, user_id=42, data={"foo": "bar"}) == {
"foo": "bar"
}
assert await storage.update_data(chat_id=-42, user_id=42, data={"baz": "spam"}) == {
"foo": "bar",
"baz": "spam",
}

View file

@ -0,0 +1,21 @@
import pytest
from aiogram.dispatcher.fsm.storage.redis import RedisStorage
from tests.mocked_bot import MockedBot
@pytest.mark.redis
class TestRedisStorage:
@pytest.mark.parametrize(
"prefix_bot,result",
[
[False, "fsm:-1:2"],
[True, "fsm:42:-1:2"],
[{42: "kaboom"}, "fsm:kaboom:-1:2"],
[lambda bot: "kaboom", "fsm:kaboom:-1:2"],
],
)
@pytest.mark.asyncio
async def test_generate_key(self, bot: MockedBot, redis_server, prefix_bot, result):
storage = RedisStorage.from_url(redis_server, prefix_bot=prefix_bot)
assert storage.generate_key(bot, -1, 2) == result

View file

@ -0,0 +1,44 @@
import pytest
from aiogram.dispatcher.fsm.storage.base import BaseStorage
from tests.mocked_bot import MockedBot
@pytest.mark.parametrize(
"storage",
[pytest.lazy_fixture("redis_storage"), pytest.lazy_fixture("memory_storage")],
)
class TestStorages:
@pytest.mark.asyncio
async def test_lock(self, bot: MockedBot, storage: BaseStorage):
# TODO: ?!?
async with storage.lock(bot=bot, chat_id=-42, user_id=42):
assert True, "You are kidding me?"
@pytest.mark.asyncio
async def test_set_state(self, bot: MockedBot, storage: BaseStorage):
assert await storage.get_state(bot=bot, chat_id=-42, user_id=42) is None
await storage.set_state(bot=bot, chat_id=-42, user_id=42, state="state")
assert await storage.get_state(bot=bot, chat_id=-42, user_id=42) == "state"
await storage.set_state(bot=bot, chat_id=-42, user_id=42, state=None)
assert await storage.get_state(bot=bot, chat_id=-42, user_id=42) is None
@pytest.mark.asyncio
async def test_set_data(self, bot: MockedBot, storage: BaseStorage):
assert await storage.get_data(bot=bot, chat_id=-42, user_id=42) == {}
await storage.set_data(bot=bot, chat_id=-42, user_id=42, data={"foo": "bar"})
assert await storage.get_data(bot=bot, chat_id=-42, user_id=42) == {"foo": "bar"}
await storage.set_data(bot=bot, chat_id=-42, user_id=42, data={})
assert await storage.get_data(bot=bot, chat_id=-42, user_id=42) == {}
@pytest.mark.asyncio
async def test_update_data(self, bot: MockedBot, storage: BaseStorage):
assert await storage.get_data(bot=bot, chat_id=-42, user_id=42) == {}
assert await storage.update_data(
bot=bot, chat_id=-42, user_id=42, data={"foo": "bar"}
) == {"foo": "bar"}
assert await storage.update_data(
bot=bot, chat_id=-42, user_id=42, data={"baz": "spam"}
) == {"foo": "bar", "baz": "spam"}

View file

@ -2,27 +2,28 @@ import pytest
from aiogram.dispatcher.fsm.context import FSMContext
from aiogram.dispatcher.fsm.storage.memory import MemoryStorage
from tests.mocked_bot import MockedBot
@pytest.fixture()
def state():
def state(bot: MockedBot):
storage = MemoryStorage()
ctx = storage.storage[-42][42]
ctx = storage.storage[bot][-42][42]
ctx.state = "test"
ctx.data = {"foo": "bar"}
return FSMContext(storage=storage, user_id=-42, chat_id=42)
return FSMContext(bot=bot, storage=storage, user_id=-42, chat_id=42)
class TestFSMContext:
@pytest.mark.asyncio
async def test_address_mapping(self):
async def test_address_mapping(self, bot: MockedBot):
storage = MemoryStorage()
ctx = storage.storage[-42][42]
ctx = storage.storage[bot][-42][42]
ctx.state = "test"
ctx.data = {"foo": "bar"}
state = FSMContext(storage=storage, chat_id=-42, user_id=42)
state2 = FSMContext(storage=storage, chat_id=42, user_id=42)
state3 = FSMContext(storage=storage, chat_id=69, user_id=69)
state = FSMContext(bot=bot, storage=storage, chat_id=-42, user_id=42)
state2 = FSMContext(bot=bot, storage=storage, chat_id=42, user_id=42)
state3 = FSMContext(bot=bot, storage=storage, chat_id=69, user_id=69)
assert await state.get_state() == "test"
assert await state2.get_state() is None

View file

@ -0,0 +1,27 @@
import pytest
from aiogram.utils.auth_widget import check_integrity
TOKEN = "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
@pytest.fixture
def data():
return {
"id": "42",
"first_name": "John",
"last_name": "Smith",
"username": "username",
"photo_url": "https://t.me/i/userpic/320/picname.jpg",
"auth_date": "1565810688",
"hash": "c303db2b5a06fe41d23a9b14f7c545cfc11dcc7473c07c9c5034ae60062461ce",
}
class TestCheckIntegrity:
def test_ok(self, data):
assert check_integrity(TOKEN, data) is True
def test_fail(self, data):
data.pop("username")
assert check_integrity(TOKEN, data) is False

View file

@ -0,0 +1,97 @@
import pytest
from async_lru import alru_cache
from aiogram.utils.deep_linking import (
create_start_link,
create_startgroup_link,
decode_payload,
encode_payload,
)
# enable asyncio mode
from tests.mocked_bot import MockedBot
pytestmark = pytest.mark.asyncio
PAYLOADS = [
"foo",
"AAbbCCddEEff1122334455",
"aaBBccDDeeFF5544332211",
-12345678901234567890,
12345678901234567890,
]
WRONG_PAYLOADS = [
"@BotFather",
"Some:special$characters#=",
"spaces spaces spaces",
1234567890123456789.0,
]
@pytest.fixture(params=PAYLOADS, name="payload")
def payload_fixture(request):
return request.param
@pytest.fixture(params=WRONG_PAYLOADS, name="wrong_payload")
def wrong_payload_fixture(request):
return request.param
@pytest.fixture(autouse=True)
def get_bot_user_fixture(monkeypatch):
"""Monkey patching of bot.me calling."""
@alru_cache()
async def get_bot_user_mock(self):
from aiogram.types import User
return User(
id=12345678,
is_bot=True,
first_name="FirstName",
last_name="LastName",
username="username",
language_code="uk-UA",
)
monkeypatch.setattr(MockedBot, "me", get_bot_user_mock)
class TestDeepLinking:
async def test_get_start_link(self, bot, payload):
link = await create_start_link(bot=bot, payload=payload)
assert link == f"https://t.me/username?start={payload}"
async def test_wrong_symbols(self, bot, wrong_payload):
with pytest.raises(ValueError):
await create_start_link(bot, wrong_payload)
async def test_get_startgroup_link(self, bot, payload):
link = await create_startgroup_link(bot, payload)
assert link == f"https://t.me/username?startgroup={payload}"
async def test_filter_encode_and_decode(self, payload):
encoded = encode_payload(payload)
decoded = decode_payload(encoded)
assert decoded == str(payload)
async def test_get_start_link_with_encoding(self, bot, wrong_payload):
# define link
link = await create_start_link(bot, wrong_payload, encode=True)
# define reference link
encoded_payload = encode_payload(wrong_payload)
assert link == f"https://t.me/username?start={encoded_payload}"
async def test_64_len_payload(self, bot):
payload = "p" * 64
link = await create_start_link(bot, payload)
assert link
async def test_too_long_payload(self, bot):
payload = "p" * 65
print(payload, len(payload))
with pytest.raises(ValueError):
await create_start_link(bot, payload)

View file

@ -35,7 +35,7 @@ class TestMarkdown:
[hitalic, ("test", "test"), " ", "<i>test test</i>"],
[code, ("test", "test"), " ", "`test test`"],
[hcode, ("test", "test"), " ", "<code>test test</code>"],
[pre, ("test", "test"), " ", "```test test```"],
[pre, ("test", "test"), " ", "```\ntest test\n```"],
[hpre, ("test", "test"), " ", "<pre>test test</pre>"],
[underline, ("test", "test"), " ", "__\rtest test__\r"],
[hunderline, ("test", "test"), " ", "<u>test test</u>"],

View file

@ -55,7 +55,7 @@ class TestTextDecoration:
[markdown_decoration, MessageEntity(type="bold", offset=0, length=5), "*test*"],
[markdown_decoration, MessageEntity(type="italic", offset=0, length=5), "_\rtest_\r"],
[markdown_decoration, MessageEntity(type="code", offset=0, length=5), "`test`"],
[markdown_decoration, MessageEntity(type="pre", offset=0, length=5), "```test```"],
[markdown_decoration, MessageEntity(type="pre", offset=0, length=5), "```\ntest\n```"],
[
markdown_decoration,
MessageEntity(type="pre", offset=0, length=5, language="python"),