add api exception handler (#74)

* try to add exception handler

* improve server error test

* fix lint

* add build_uri util

* fix header file path

---------

Co-authored-by: Dmitry Afanasyev <afanasiev@litres.ru>
This commit is contained in:
Dmitry Afanasyev
2023-12-30 23:50:59 +03:00
committed by GitHub
parent d1ae7f2281
commit bf7a5520dc
10 changed files with 171 additions and 45 deletions

View File

@@ -24,6 +24,20 @@ def test_settings() -> AppSettings:
return get_settings()
# Redefine the event_loop fixture to have a session scope. Otherwise `bot` fixture can't be
# session. See https://github.com/pytest-dev/pytest-asyncio/issues/68 for more details.
@pytest.fixture(scope="session", autouse=True)
def event_loop() -> AbstractEventLoop:
"""
Пересоздаем луп для изоляции тестов. В основном нужно для запуска юнит тестов
в связке с интеграционными, т.к. без этого pytest зависает.
Для интеграционных тестов фикстура определяется дополнительная фикстура на всю сессию.
"""
loop = asyncio.get_event_loop_policy().new_event_loop()
asyncio.set_event_loop(loop)
return loop
@pytest.fixture(scope="session")
def engine(test_settings: AppSettings) -> Generator[Engine, None, None]:
"""
@@ -99,21 +113,6 @@ class PytestApplication(Application): # type: ignore
pass
def make_bot(bot_info: dict[str, Any] | None = None, **kwargs: Any) -> PytestExtBot:
"""
Tests are executed on tg.ext.ExtBot, as that class only extends the functionality of tg.bot
"""
token = kwargs.pop("token", (bot_info or {}).get("token"))
kwargs.pop("token", None)
return PytestExtBot(
token=token,
private_key=None,
request=NonchalantHttpxRequest(connection_pool_size=8),
get_updates_request=NonchalantHttpxRequest(connection_pool_size=1),
**kwargs,
)
async def _mocked_get_me(bot: Bot) -> User:
if bot._bot_user is None:
bot._bot_user = _get_bot_user(bot.token)
@@ -145,20 +144,6 @@ def _get_bot_user(token: str) -> User:
)
# Redefine the event_loop fixture to have a session scope. Otherwise `bot` fixture can't be
# session. See https://github.com/pytest-dev/pytest-asyncio/issues/68 for more details.
@pytest.fixture(scope="session", autouse=True)
def event_loop() -> AbstractEventLoop:
"""
Пересоздаем луп для изоляции тестов. В основном нужно для запуска юнит тестов
в связке с интеграционными, т.к. без этого pytest зависает.
Для интеграционных тестов фикстура определяется дополнительная фикстура на всю сессию.
"""
loop = asyncio.get_event_loop_policy().new_event_loop()
asyncio.set_event_loop(loop)
return loop
@pytest.fixture(scope="session")
def bot_info() -> dict[str, Any]:
return BotInfoFactory()
@@ -183,14 +168,16 @@ async def bot(bot_info: dict[str, Any], bot_application: Any) -> AsyncGenerator[
yield _bot
@pytest.fixture(scope="session")
async def bot_app(test_settings: AppSettings) -> BotApplication:
return BotApplication(settings=test_settings, handlers=bot_event_handlers.handlers)
@pytest.fixture(scope="session")
async def main_application(
bot_application: PytestApplication, test_settings: AppSettings
test_settings: AppSettings,
bot_app: BotApplication,
) -> AsyncGenerator[AppApplication, None]:
bot_app = BotApplication(
settings=test_settings,
handlers=bot_event_handlers.handlers,
)
bot_app.application._initialized = True
bot_app.application.bot = make_bot(BotInfoFactory())
bot_app.application.bot._bot_user = BotUserFactory()
@@ -213,3 +200,18 @@ async def rest_client(main_application: AppApplication) -> AsyncGenerator[AsyncC
headers={"Content-Type": "application/json"},
) as client:
yield client
def make_bot(bot_info: dict[str, Any] | None = None, **kwargs: Any) -> PytestExtBot:
"""
Tests are executed on tg.ext.ExtBot, as that class only extends the functionality of tg.bot
"""
token = kwargs.pop("token", (bot_info or {}).get("token"))
kwargs.pop("token", None)
return PytestExtBot(
token=token,
private_key=None,
request=NonchalantHttpxRequest(connection_pool_size=8),
get_updates_request=NonchalantHttpxRequest(connection_pool_size=1),
**kwargs,
)

View File

@@ -1,11 +1,15 @@
import httpx
import pytest
from faker import Faker
from httpx import AsyncClient, Response
from fastapi.responses import ORJSONResponse
from httpx import ASGITransport, AsyncClient, Response
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from starlette import status
from api.exceptions import BaseAPIException
from core.bot.app import BotApplication
from main import Application as AppApplication
from settings.config import AppSettings
from tests.integration.factories.bot import ChatGptModelFactory
from tests.integration.utils import mocked_ask_question_api
@@ -63,3 +67,32 @@ async def test_bot_healthcheck_not_ok(
):
response = await rest_client.get("/api/bot-healthcheck")
assert response.status_code == httpx.codes.INTERNAL_SERVER_ERROR
async def test_server_error_handler_returns_500_without_traceback_when_debug_disabled(
test_settings: AppSettings,
bot_app: BotApplication,
) -> None:
settings = test_settings.model_copy(update={"DEBUG": False})
fastapi_app = AppApplication(settings=settings, bot_app=bot_app).fastapi_app
route = "/server-error"
@fastapi_app.get(route, response_model=None, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
async def controller() -> ORJSONResponse:
result = 1 / 0
return ORJSONResponse(content=result, status_code=status.HTTP_200_OK)
async with AsyncClient(
transport=ASGITransport(app=fastapi_app, raise_app_exceptions=False), # type: ignore[arg-type]
base_url="http://test",
headers={"Content-Type": "application/json"},
) as client:
response = await client.get(route)
assert response.status_code == 500
data = response.json()
assert data == {"error": {"title": "Something went wrong!", "type": "InternalServerError"}, "status": 500}
replaced_oauth_route = next(
filter(lambda r: r.path == route, fastapi_app.routes) # type: ignore[arg-type, attr-defined]
)
fastapi_app.routes.remove(replaced_oauth_route)

View File

@@ -1,7 +1,9 @@
import time
from typing import Callable
from core.utils import timed_lru_cache
import pytest
from core.utils import build_uri, timed_lru_cache
class TestTimedLruCache:
@@ -58,3 +60,21 @@ class TestTimedLruCache:
) -> None:
for _ in range(call_times):
assert func(first, second) == result
@pytest.mark.parametrize(
"uri_parts, expected_result",
[
(["", "admin"], "/admin"),
(["/gpt", "admin"], "/gpt/admin"),
(["/gpt", "/chat"], "/gpt/chat"),
(["gpt", ""], "/gpt"),
(["gpt"], "/gpt"),
(["gpt", "chat"], "/gpt/chat"),
(["", ""], "/"),
(["gpt", "/chat", "/admin"], "/gpt/chat/admin"),
(["gpt/", "/chat/", "/admin"], "/gpt/chat/admin"),
],
)
def test_build_uri_with_slash_prefix(uri_parts: list[str], expected_result: str) -> None:
assert build_uri(uri_parts) == expected_result