mirror of
https://gitlab.archlinux.org/archlinux/aurweb.git
synced 2025-02-03 10:43:03 +01:00
feat(python): catch all exceptions thrown through fastapi route paths
This commit does quite a bit: - Catches unhandled exceptions raised in the route handler and produces a 500 Internal Server Error Arch-themed response. - Each unhandled exception causes a notification to be sent to new `notifications.postmaster` email with a "Traceback ID." - Traceback ID is logged to the server along with the traceback which caused the 500: `docker-compose logs fastapi | grep '<traceback_id>'` - If `options.traceback` is set to `1`, traceback is displayed in the new 500.html template. Signed-off-by: Kevin Morris <kevr@0cost.org>
This commit is contained in:
parent
c775e8a692
commit
d675c0dc26
10 changed files with 230 additions and 14 deletions
|
@ -1,19 +1,28 @@
|
|||
import http
|
||||
import os
|
||||
import re
|
||||
|
||||
from unittest import mock
|
||||
|
||||
import fastapi
|
||||
import pytest
|
||||
|
||||
from fastapi import HTTPException
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
import aurweb.asgi
|
||||
import aurweb.config
|
||||
import aurweb.redis
|
||||
|
||||
from aurweb.testing.email import Email
|
||||
from aurweb.testing.requests import Request
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def setup(db_test, email_test):
|
||||
return
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_asgi_startup_session_secret_exception(monkeypatch):
|
||||
""" Test that we get an IOError on app_startup when we cannot
|
||||
|
@ -66,3 +75,45 @@ async def test_asgi_app_unsupported_backends():
|
|||
expr = r"^.*\(sqlite\) is unsupported.*$"
|
||||
with pytest.raises(ValueError, match=expr):
|
||||
await aurweb.asgi.app_startup()
|
||||
|
||||
|
||||
def test_internal_server_error(setup: None,
|
||||
caplog: pytest.LogCaptureFixture):
|
||||
config_getboolean = aurweb.config.getboolean
|
||||
|
||||
def mock_getboolean(section: str, key: str) -> bool:
|
||||
if section == "options" and key == "traceback":
|
||||
return True
|
||||
return config_getboolean(section, key)
|
||||
|
||||
@aurweb.asgi.app.get("/internal_server_error")
|
||||
async def internal_server_error(request: fastapi.Request):
|
||||
raise ValueError("test exception")
|
||||
|
||||
with mock.patch("aurweb.config.getboolean", side_effect=mock_getboolean):
|
||||
with TestClient(app=aurweb.asgi.app) as request:
|
||||
resp = request.get("/internal_server_error")
|
||||
assert resp.status_code == int(http.HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||
|
||||
# Let's assert that a notification was sent out to the postmaster.
|
||||
assert Email.count() == 1
|
||||
|
||||
aur_location = aurweb.config.get("options", "aur_location")
|
||||
email = Email(1)
|
||||
assert f"Location: {aur_location}" in email.body
|
||||
assert "Traceback ID:" in email.body
|
||||
assert "Version:" in email.body
|
||||
assert "Datetime:" in email.body
|
||||
assert f"[1] {aur_location}" in email.body
|
||||
|
||||
# Assert that the exception got logged with with its traceback id.
|
||||
expr = r"FATAL\[.{7}\]"
|
||||
assert re.search(expr, caplog.text)
|
||||
|
||||
# Let's do it again; no email should be sent the next time,
|
||||
# since the hash is stored in redis.
|
||||
with mock.patch("aurweb.config.getboolean", side_effect=mock_getboolean):
|
||||
with TestClient(app=aurweb.asgi.app) as request:
|
||||
resp = request.get("/internal_server_error")
|
||||
assert resp.status_code == int(http.HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||
assert Email.count() == 1
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import re
|
||||
|
||||
from datetime import datetime
|
||||
from http import HTTPStatus
|
||||
from unittest import mock
|
||||
|
@ -322,9 +324,13 @@ def test_generate_unique_sid_exhausted(client: TestClient, user: User,
|
|||
response = request.post("/login", data=post_data, cookies={})
|
||||
assert response.status_code == int(HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||
|
||||
expected = "Unable to generate a unique session ID"
|
||||
assert expected in response.text
|
||||
assert "500 - Internal Server Error" in response.text
|
||||
|
||||
# Make sure an IntegrityError from the DB got logged out.
|
||||
# Make sure an IntegrityError from the DB got logged out
|
||||
# with a FATAL traceback ID.
|
||||
expr = r"FATAL\[.{7}\]"
|
||||
assert re.search(expr, caplog.text)
|
||||
assert "IntegrityError" in caplog.text
|
||||
|
||||
expr = r"Duplicate entry .+ for key .+SessionID.+"
|
||||
assert re.search(expr, response.text)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue