change: report unhandled tracebacks to a repository

As repeats of these traceback notifications were annoying some of
the devops staff, and it took coordination to share tracebacks with
developers, this commit removes that responsibility off of devops
by reporting tracebacks to Gitlab repositories in the form of issues.

- removed ServerErrorNotification
- removed notifications.postmaster configuration option
- added notifications.gitlab-instance option
- added notifications.error-project option
- added notifications.error-token option
- added aurweb.exceptions.handle_form_exceptions, a POST route decorator

Issues are filed confidentially. This change will need updates
in infrastructure's ansible configuration before this can be
applied to aur.archlinux.org.

Signed-off-by: Kevin Morris <kevr@0cost.org>
This commit is contained in:
Kevin Morris 2022-02-09 23:35:08 -08:00
parent e2eb3a7ded
commit 7485cc231e
No known key found for this signature in database
GPG key ID: F7E46DED420788F3
14 changed files with 254 additions and 85 deletions

View file

@ -2,6 +2,7 @@ import http
import os
import re
from typing import Callable
from unittest import mock
import fastapi
@ -14,13 +15,39 @@ import aurweb.asgi
import aurweb.config
import aurweb.redis
from aurweb.testing.email import Email
from aurweb.exceptions import handle_form_exceptions
from aurweb.testing.requests import Request
@pytest.fixture
def setup(db_test, email_test):
return
aurweb.redis.redis_connection().flushall()
yield
aurweb.redis.redis_connection().flushall()
@pytest.fixture
def mock_glab_request(monkeypatch):
def wrapped(return_value=None, side_effect=None):
def what_to_return(*args, **kwargs):
if side_effect:
return side_effect # pragma: no cover
return return_value
monkeypatch.setattr("requests.post", what_to_return)
return wrapped
def mock_glab_config(project: str = "test/project", token: str = "test-token"):
config_get = aurweb.config.get
def wrapper(section: str, key: str) -> str:
if section == "notifications":
if key == "error-project":
return project
elif key == "error-token":
return token
return config_get(section, key)
return wrapper
@pytest.mark.asyncio
@ -77,8 +104,8 @@ async def test_asgi_app_unsupported_backends():
await aurweb.asgi.app_startup()
def test_internal_server_error(setup: None,
caplog: pytest.LogCaptureFixture):
@pytest.fixture
def use_traceback():
config_getboolean = aurweb.config.getboolean
def mock_getboolean(section: str, key: str) -> bool:
@ -86,34 +113,100 @@ def test_internal_server_error(setup: None,
return True
return config_getboolean(section, key)
with mock.patch("aurweb.config.getboolean", side_effect=mock_getboolean):
yield
class FakeResponse:
def __init__(self, status_code: int = 201, text: str = "{}"):
self.status_code = status_code
self.text = text
def test_internal_server_error_bad_glab(setup: None, use_traceback: None,
mock_glab_request: Callable,
caplog: pytest.LogCaptureFixture):
@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 mock.patch("aurweb.config.get", side_effect=mock_glab_config()):
with TestClient(app=aurweb.asgi.app) as request:
mock_glab_request(FakeResponse(status_code=404))
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
expr = r"ERROR.*Unable to report exception to"
assert re.search(expr, caplog.text)
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
expr = r"FATAL\[.{7}\]"
assert re.search(expr, caplog.text)
def test_internal_server_error_no_token(setup: None, use_traceback: None,
mock_glab_request: Callable,
caplog: pytest.LogCaptureFixture):
@aurweb.asgi.app.get("/internal_server_error")
async def internal_server_error(request: fastapi.Request):
raise ValueError("test exception")
mock_get = mock_glab_config(token="set-me")
with mock.patch("aurweb.config.get", side_effect=mock_get):
with TestClient(app=aurweb.asgi.app) as request:
mock_glab_request(FakeResponse())
resp = request.get("/internal_server_error")
assert resp.status_code == int(http.HTTPStatus.INTERNAL_SERVER_ERROR)
expr = r"WARNING.*Unable to report an exception found"
assert re.search(expr, caplog.text)
expr = r"FATAL\[.{7}\]"
assert re.search(expr, caplog.text)
def test_internal_server_error(setup: None, use_traceback: None,
mock_glab_request: Callable,
caplog: pytest.LogCaptureFixture):
@aurweb.asgi.app.get("/internal_server_error")
async def internal_server_error(request: fastapi.Request):
raise ValueError("test exception")
with mock.patch("aurweb.config.get", side_effect=mock_glab_config()):
with TestClient(app=aurweb.asgi.app) as request:
mock_glab_request(FakeResponse())
# Test with a ?query=string to cover the request.url.query path.
resp = request.get("/internal_server_error?query=string")
assert resp.status_code == int(http.HTTPStatus.INTERNAL_SERVER_ERROR)
# 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):
# Let's do it again to exercise the cached path.
caplog.clear()
with mock.patch("aurweb.config.get", side_effect=mock_glab_config()):
with TestClient(app=aurweb.asgi.app) as request:
mock_glab_request(FakeResponse())
resp = request.get("/internal_server_error")
assert resp.status_code == int(http.HTTPStatus.INTERNAL_SERVER_ERROR)
assert Email.count() == 1
assert "FATAL" not in caplog.text
def test_internal_server_error_post(setup: None, use_traceback: None,
mock_glab_request: Callable,
caplog: pytest.LogCaptureFixture):
@aurweb.asgi.app.post("/internal_server_error")
@handle_form_exceptions
async def internal_server_error(request: fastapi.Request):
raise ValueError("test exception")
data = {"some": "data"}
with mock.patch("aurweb.config.get", side_effect=mock_glab_config()):
with TestClient(app=aurweb.asgi.app) as request:
mock_glab_request(FakeResponse())
# Test with a ?query=string to cover the request.url.query path.
resp = request.post("/internal_server_error", data=data)
assert resp.status_code == int(http.HTTPStatus.INTERNAL_SERVER_ERROR)
expr = r"FATAL\[.{7}\]"
assert re.search(expr, caplog.text)