add more tests (#19)

This commit is contained in:
Dmitry Afanasyev
2023-09-29 13:54:57 +03:00
committed by GitHub
parent 7cfda281f7
commit 90ec8ccec1
15 changed files with 263 additions and 76 deletions

View File

@@ -4,23 +4,25 @@ pytest framework. A common change is to allow monkeypatching of the class member
enforcing slots in the subclasses."""
import asyncio
from asyncio import AbstractEventLoop
from contextlib import contextmanager
from datetime import tzinfo
from typing import Any, AsyncGenerator
from typing import Any, AsyncGenerator, Iterator
import pytest
import pytest_asyncio
from fastapi import FastAPI
from httpx import AsyncClient
import respx
from httpx import AsyncClient, Response
from pytest_asyncio.plugin import SubRequest
from telegram import Bot, User
from telegram.ext import Application, ApplicationBuilder, Defaults, ExtBot
from constants import CHAT_GPT_BASE_URI
from core.bot import BotApplication
from core.handlers import bot_event_handlers
from main import Application as AppApplication
from settings.config import AppSettings, get_settings
from tests.integration.bot.networking import NonchalantHttpxRequest
from tests.integration.factories.bot import BotInfoFactory
from tests.integration.factories.bot import BotInfoFactory, BotUserFactory
@pytest.fixture(scope="session")
@@ -123,6 +125,7 @@ def bot_info() -> dict[str, Any]:
async def bot_application(bot_info: dict[str, Any]) -> AsyncGenerator[Any, None]:
# We build a new bot each time so that we use `app` in a context manager without problems
application = ApplicationBuilder().bot(make_bot(bot_info)).application_class(PytestApplication).build()
await application.initialize()
yield application
if application.running:
await application.stop()
@@ -226,27 +229,41 @@ def provider_token(bot_info: dict[str, Any]) -> str:
@pytest_asyncio.fixture(scope="session")
async def main_application(
bot_application: PytestApplication, test_settings: AppSettings
) -> AsyncGenerator[FastAPI, None]:
) -> AsyncGenerator[AppApplication, None]:
bot_app = BotApplication(
application=bot_application,
settings=test_settings,
handlers=bot_event_handlers.handlers,
)
fast_api_app = AppApplication(settings=test_settings, bot_app=bot_app).fastapi_app
bot_app.application._initialized = True
bot_app.application.bot = make_bot(BotInfoFactory())
bot_app.application.bot._bot_user = BotUserFactory()
fast_api_app = AppApplication(settings=test_settings, bot_app=bot_app)
yield fast_api_app
@pytest_asyncio.fixture()
async def rest_client(
main_application: FastAPI,
main_application: AppApplication,
) -> AsyncGenerator[AsyncClient, None]:
"""
Default http client. Use to test unauthorized requests, public endpoints
or special authorization methods.
"""
async with AsyncClient(
app=main_application,
app=main_application.fastapi_app,
base_url="http://test",
headers={"Content-Type": "application/json"},
) as client:
yield client
@contextmanager
def mocked_ask_question_api(host: str) -> Iterator[respx.MockRouter]:
with respx.mock(
assert_all_mocked=True,
assert_all_called=True,
base_url=host,
) as respx_mock:
ask_question_route = respx_mock.post(url=CHAT_GPT_BASE_URI, name="ask_question")
ask_question_route.return_value = Response(status_code=200, text="Привет! Как я могу помочь вам сегодня?")
yield respx_mock

View File

@@ -1,24 +1,30 @@
import asyncio
import time
from asyncio import AbstractEventLoop
from typing import Any
from unittest import mock
import pytest
import telegram
from assertpy import assert_that
from faker import Faker
from httpx import AsyncClient
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from constants import BotStagesEnum
from core.bot import BotApplication, BotQueue
from main import Application
from settings.config import AppSettings
from tests.integration.bot.conftest import mocked_ask_question_api
from tests.integration.bot.networking import MockedRequest
from tests.integration.factories.bot import (
BotChatFactory,
BotEntitleFactory,
BotUserFactory,
BotCallBackQueryFactory,
BotMessageFactory,
BotUpdateFactory,
CallBackFactory,
)
pytestmark = [
pytest.mark.asyncio,
pytest.mark.enable_socket,
]
@@ -34,10 +40,10 @@ async def test_bot_webhook_endpoint(
rest_client: AsyncClient,
main_application: Application,
) -> None:
bot_update = create_bot_update()
bot_update = BotUpdateFactory(message=BotMessageFactory.create_instance(text="/help"))
response = await rest_client.post(url="/api/123456789:AABBCCDDEEFFaabbccddeeff-1234567890", json=bot_update)
assert response.status_code == 202
update = await main_application.state._state["queue"].queue.get() # type: ignore[attr-defined]
update = await main_application.fastapi_app.state._state["queue"].queue.get()
update = update.to_dict()
assert update["update_id"] == bot_update["update_id"]
assert_that(update["message"]).is_equal_to(
@@ -51,22 +57,124 @@ async def test_bot_queue(
) -> None:
bot_queue = BotQueue(bot_app=bot)
event_loop.create_task(bot_queue.get_updates_from_queue())
bot_update = create_bot_update()
bot_update = BotUpdateFactory(message=BotMessageFactory.create_instance(text="/help"))
mocked_request = MockedRequest(bot_update)
await bot_queue.put_updates_on_queue(mocked_request) # type: ignore
await asyncio.sleep(1)
assert bot_queue.queue.empty()
def create_bot_update() -> dict[str, Any]:
bot_update: dict[str, Any] = {}
bot_update["update_id"] = faker.random_int(min=10**8, max=10**9 - 1)
bot_update["message"] = {
"message_id": faker.random_int(min=10**8, max=10**9 - 1),
"from": BotUserFactory()._asdict(),
"chat": BotChatFactory()._asdict(),
"date": time.time(),
"text": "/chatid",
"entities": [BotEntitleFactory()],
}
return bot_update
async def test_help_command(
main_application: Application,
test_settings: AppSettings,
) -> None:
with mock.patch.object(
telegram._bot.Bot, "send_message", return_value=lambda *args, **kwargs: (args, kwargs)
) as mocked_send_message:
bot_update = BotUpdateFactory(message=BotMessageFactory.create_instance(text="/help"))
await main_application.bot_app.application.process_update(
update=Update.de_json(data=bot_update, bot=main_application.bot_app.bot)
)
assert_that(mocked_send_message.call_args.kwargs).is_equal_to(
{
"text": "Help!",
"api_kwargs": {"text": "Список основных команд:"},
"chat_id": bot_update["message"]["chat"]["id"],
"reply_markup": InlineKeyboardMarkup(
inline_keyboard=(
(
InlineKeyboardButton(callback_data="about_me", text="Обо мне"),
InlineKeyboardButton(callback_data="website", text="Веб версия"),
),
(
InlineKeyboardButton(callback_data="help", text="Помощь"),
InlineKeyboardButton(callback_data="about_bot", text="О боте"),
),
)
),
},
include=["text", "api_kwargs", "chat_id", "reply_markup"],
)
async def test_about_me_callback_action(
main_application: Application,
test_settings: AppSettings,
) -> None:
with mock.patch.object(telegram._message.Message, "reply_text") as mocked_reply_text:
bot_update = BotCallBackQueryFactory(
message=BotMessageFactory.create_instance(text="Список основных команд:"),
callback_query=CallBackFactory(data=BotStagesEnum.about_me),
)
await main_application.bot_app.application.process_update(
update=Update.de_json(data=bot_update, bot=main_application.bot_app.bot)
)
assert mocked_reply_text.call_args.args == ("Автор бота: *Дмитрий Афанасьев*\n\nTg nickname: *Balshtg*",)
assert mocked_reply_text.call_args.kwargs == {"parse_mode": "MarkdownV2"}
async def test_about_bot_callback_action(
main_application: Application,
test_settings: AppSettings,
) -> None:
with mock.patch.object(telegram._message.Message, "reply_text") as mocked_reply_text:
bot_update = BotCallBackQueryFactory(
message=BotMessageFactory.create_instance(text="Список основных команд:"),
callback_query=CallBackFactory(data=BotStagesEnum.about_bot),
)
await main_application.bot_app.application.process_update(
update=Update.de_json(data=bot_update, bot=main_application.bot_app.bot)
)
assert mocked_reply_text.call_args.args == (
"Бот использует бесплатную модель Chat-GPT3.5 для ответов на вопросы. Принимает запросы на разных языках. "
"\n\nБот так же умеет переводить голосовые сообщения в текст. Просто пришлите голосовуху и получите поток "
"сознания без запятых в виде текста",
)
assert mocked_reply_text.call_args.kwargs == {"parse_mode": "Markdown"}
async def test_website_callback_action(
main_application: Application,
test_settings: AppSettings,
) -> None:
with mock.patch.object(telegram._message.Message, "reply_text") as mocked_reply_text:
bot_update = BotCallBackQueryFactory(
message=BotMessageFactory.create_instance(text="Список основных команд:"),
callback_query=CallBackFactory(data=BotStagesEnum.website),
)
await main_application.bot_app.application.process_update(
update=Update.de_json(data=bot_update, bot=main_application.bot_app.bot)
)
assert mocked_reply_text.call_args.args == ("Веб версия: http://localhost/chat/",)
async def test_ask_question_action(
main_application: Application,
test_settings: AppSettings,
) -> None:
with mock.patch.object(
telegram._bot.Bot, "send_message", return_value=lambda *args, **kwargs: (args, kwargs)
) as mocked_send_message, mocked_ask_question_api(host=test_settings.GPT_BASE_HOST):
bot_update = BotUpdateFactory(message=BotMessageFactory.create_instance(text="Привет!"))
bot_update["message"].pop("entities")
await main_application.bot_app.application.process_update(
update=Update.de_json(data=bot_update, bot=main_application.bot_app.bot)
)
assert_that(mocked_send_message.call_args.kwargs).is_equal_to(
{
"text": "Привет! Как я могу помочь вам сегодня?",
"chat_id": bot_update["message"]["chat"]["id"],
},
include=["text", "chat_id"],
)

View File

@@ -1,8 +1,12 @@
import string
import time
from typing import Any
import factory
import factory.fuzzy
from faker import Faker
from constants import BotStagesEnum
from tests.integration.factories.models import Chat, User
faker = Faker("ru_RU")
@@ -55,3 +59,38 @@ class BotEntitleFactory(factory.DictFactory):
type = "bot_command"
offset = 0
length = 7
class BotMessageFactory(factory.DictFactory):
message_id = factory.Faker("random_int", min=10**8, max=10**9 - 1)
chat = factory.LazyFunction(lambda: BotChatFactory()._asdict())
date = time.time()
text = factory.Faker("text")
entities = factory.LazyFunction(lambda: [BotEntitleFactory()])
@classmethod
def create_instance(cls, **kwargs: Any) -> dict[str, Any]:
data = {**cls.build(**kwargs), "from": BotUserFactory()._asdict()}
return data
class BotUpdateFactory(factory.DictFactory):
update_id = factory.Faker("random_int", min=10**8, max=10**9 - 1)
message = factory.LazyFunction(lambda: BotMessageFactory.create_instance())
class CallBackFactory(factory.DictFactory):
id = factory.Faker("bothify", text="###################")
chat_instance = factory.Faker("bothify", text="###################")
message = factory.LazyFunction(lambda: BotMessageFactory.create_instance())
data = factory.fuzzy.FuzzyChoice(BotStagesEnum)
@classmethod
def create_instance(cls, **kwargs: Any) -> dict[str, Any]:
data = {**cls.build(**kwargs), "from": BotUserFactory()._asdict()}
return data
class BotCallBackQueryFactory(factory.DictFactory):
update_id = factory.Faker("random_int", min=10**8, max=10**9 - 1)
callback_query = factory.LazyFunction(lambda: BotMessageFactory.create_instance())