Merge branch 'master' into live

This commit is contained in:
Kevin Morris 2022-02-10 13:56:50 -08:00
commit e85870363c
No known key found for this signature in database
GPG key ID: F7E46DED420788F3
19 changed files with 260 additions and 89 deletions

View file

@ -9,6 +9,8 @@ import typing
from urllib.parse import quote_plus
import requests
from fastapi import FastAPI, HTTPException, Request, Response
from fastapi.responses import RedirectResponse
from fastapi.staticfiles import StaticFiles
@ -33,7 +35,6 @@ from aurweb.packages.util import get_pkg_or_base
from aurweb.prometheus import instrumentator
from aurweb.redis import redis_connection
from aurweb.routers import APP_ROUTES
from aurweb.scripts import notify
from aurweb.templates import make_context, render_template
logger = logging.get_logger(__name__)
@ -109,6 +110,10 @@ async def internal_server_error(request: Request, exc: Exception) -> Response:
:param request: FastAPI Request
:return: Rendered 500.html template with status_code 500
"""
repo = aurweb.config.get("notifications", "gitlab-instance")
project = aurweb.config.get("notifications", "error-project")
token = aurweb.config.get("notifications", "error-token")
context = make_context(request, "Internal Server Error")
# Print out the exception via `traceback` and store the value
@ -120,39 +125,80 @@ async def internal_server_error(request: Request, exc: Exception) -> Response:
# Produce a SHA1 hash of the traceback string.
tb_hash = hashlib.sha1(tb.encode()).hexdigest()
# Use the first 7 characters of the sha1 for the traceback id.
# We will use this to log and include in the notification.
tb_id = tb_hash[:7]
redis = redis_connection()
pipe = redis.pipeline()
key = f"tb:{tb_hash}"
pipe.get(key)
retval, = pipe.execute()
retval = redis.get(key)
if not retval:
# Expire in one hour; this is just done to make sure we
# don't infinitely store these values, but reduce the number
# of automated reports (notification below). At this time of
# writing, unexpected exceptions are not common, thus this
# will not produce a large memory footprint in redis.
pipe = redis.pipeline()
pipe.set(key, tb)
pipe.expire(key, 3600)
pipe.expire(key, 86400) # One day.
pipe.execute()
# Send out notification about it.
notif = notify.ServerErrorNotification(
tb_id, context.get("version"), context.get("utcnow"))
notif.send()
if "set-me" not in (project, token):
proj = quote_plus(project)
endp = f"{repo}/api/v4/projects/{proj}/issues"
retval = tb
base = f"{request.url.scheme}://{request.url.netloc}"
title = f"Traceback [{tb_id}]: {base}{request.url.path}"
desc = [
"DISCLAIMER",
"----------",
"**This issue is confidential** and should be sanitized "
"before sharing with users or developers. Please ensure "
"you've completed the following tasks:",
"- [ ] I have removed any sensitive data and "
"the description history.",
"",
"Exception Details",
"-----------------",
f"- Route: `{request.url.path}`",
f"- User: `{request.user.Username}`",
f"- Email: `{request.user.Email}`",
]
# Add method-specific information to the description.
if request.method.lower() == "get":
# get
if request.url.query:
desc = desc + [f"- Query: `{request.url.query}`"]
desc += ["", f"```{tb}```"]
else:
# post
form_data = str(dict(request.state.form_data))
desc = desc + [
f"- Data: `{form_data}`"
] + ["", f"```{tb}```"]
headers = {"Authorization": f"Bearer {token}"}
data = {
"title": title,
"description": "\n".join(desc),
"labels": ["triage"],
"confidential": True,
}
logger.info(endp)
resp = requests.post(endp, json=data, headers=headers)
if resp.status_code != http.HTTPStatus.CREATED:
logger.error(
f"Unable to report exception to {repo}: {resp.text}")
else:
logger.warning("Unable to report an exception found due to "
"unset notifications.error-{{project,token}}")
# Log details about the exception traceback.
logger.error(f"FATAL[{tb_id}]: An unexpected exception has occurred.")
logger.error(tb)
else:
retval = retval.decode()
# Log details about the exception traceback.
logger.error(f"FATAL[{tb_id}]: An unexpected exception has occurred.")
logger.error(retval)
return render_template(request, "errors/500.html", context,
status_code=http.HTTPStatus.INTERNAL_SERVER_ERROR)

View file

@ -33,6 +33,8 @@ class AnonymousUser:
makes a request against FastAPI. """
# Stub attributes used to mimic a real user.
ID = 0
Username = "N/A"
Email = "N/A"
class AccountType:
""" A stubbed AccountType static class. In here, we use an ID

View file

@ -6,7 +6,7 @@ from typing import Any
# Publicly visible version of aurweb. This is used to display
# aurweb versioning in the footer and must be maintained.
# Todo: Make this dynamic/automated.
AURWEB_VERSION = "v6.0.9"
AURWEB_VERSION = "v6.0.10"
_parser = None

View file

@ -1,4 +1,8 @@
from typing import Any
import functools
from typing import Any, Callable
import fastapi
class AurwebException(Exception):
@ -90,3 +94,19 @@ class ValidationError(AurwebException):
class InvariantError(AurwebException):
pass
def handle_form_exceptions(route: Callable) -> fastapi.Response:
"""
A decorator required when fastapi POST routes are defined.
This decorator populates fastapi's `request.state` with a `form_data`
attribute, which is then used to report form data when exceptions
are caught and reported.
"""
@functools.wraps(route)
async def wrapper(request: fastapi.Request, *args, **kwargs):
request.state.form_data = await request.form()
return await route(request, *args, **kwargs)
return wrapper

View file

@ -13,7 +13,7 @@ import aurweb.config
from aurweb import cookies, db, l10n, logging, models, util
from aurweb.auth import account_type_required, requires_auth, requires_guest
from aurweb.captcha import get_captcha_salts
from aurweb.exceptions import ValidationError
from aurweb.exceptions import ValidationError, handle_form_exceptions
from aurweb.l10n import get_translator_for_request
from aurweb.models import account_type as at
from aurweb.models.ssh_pub_key import get_fingerprint
@ -35,6 +35,7 @@ async def passreset(request: Request):
@router.post("/passreset", response_class=HTMLResponse)
@handle_form_exceptions
@requires_guest
async def passreset_post(request: Request,
user: str = Form(...),
@ -253,6 +254,7 @@ async def account_register(request: Request,
@router.post("/register", response_class=HTMLResponse)
@handle_form_exceptions
@requires_guest
async def account_register_post(request: Request,
U: str = Form(default=str()), # Username
@ -369,6 +371,7 @@ async def account_edit(request: Request, username: str):
@router.post("/account/{username}/edit", response_class=HTMLResponse)
@handle_form_exceptions
@requires_auth
async def account_edit_post(request: Request,
username: str,
@ -492,6 +495,7 @@ async def accounts(request: Request):
@router.post("/accounts")
@handle_form_exceptions
@requires_auth
@account_type_required({at.TRUSTED_USER,
at.DEVELOPER,
@ -601,6 +605,7 @@ async def terms_of_service(request: Request):
@router.post("/tos")
@handle_form_exceptions
@requires_auth
async def terms_of_service_post(request: Request,
accept: bool = Form(default=False)):

View file

@ -8,6 +8,7 @@ import aurweb.config
from aurweb import cookies, db, time
from aurweb.auth import requires_auth, requires_guest
from aurweb.exceptions import handle_form_exceptions
from aurweb.l10n import get_translator_for_request
from aurweb.models import User
from aurweb.templates import make_variable_context, render_template
@ -29,6 +30,7 @@ async def login_get(request: Request, next: str = "/"):
@router.post("/login", response_class=HTMLResponse)
@handle_form_exceptions
@requires_guest
async def login_post(request: Request,
next: str = Form(...),
@ -82,6 +84,7 @@ async def login_post(request: Request,
@router.post("/logout")
@handle_form_exceptions
@requires_auth
async def logout(request: Request, next: str = Form(default="/")):
if request.user.is_authenticated():

View file

@ -15,6 +15,7 @@ import aurweb.models.package_request
from aurweb import cookies, db, models, time, util
from aurweb.cache import db_count_cache
from aurweb.exceptions import handle_form_exceptions
from aurweb.models.account_type import TRUSTED_USER_AND_DEV_ID, TRUSTED_USER_ID
from aurweb.models.package_request import PENDING_ID
from aurweb.packages.util import query_notified, query_voted, updated_packages
@ -31,6 +32,7 @@ async def favicon(request: Request):
@router.post("/language", response_class=RedirectResponse)
@handle_form_exceptions
async def language(request: Request,
set_lang: str = Form(...),
next: str = Form(...),

View file

@ -8,7 +8,7 @@ import aurweb.filters # noqa: F401
from aurweb import config, db, defaults, logging, models, util
from aurweb.auth import creds, requires_auth
from aurweb.exceptions import InvariantError
from aurweb.exceptions import InvariantError, handle_form_exceptions
from aurweb.models.relation_type import CONFLICTS_ID, PROVIDES_ID, REPLACES_ID
from aurweb.packages import util as pkgutil
from aurweb.packages.search import PackageSearch
@ -416,6 +416,7 @@ PACKAGE_ACTIONS = {
@router.post("/packages")
@handle_form_exceptions
@requires_auth
async def packages_post(request: Request,
IDs: List[int] = Form(default=[]),

View file

@ -6,7 +6,7 @@ from sqlalchemy import and_
from aurweb import config, db, l10n, logging, templates, time, util
from aurweb.auth import creds, requires_auth
from aurweb.exceptions import InvariantError, ValidationError
from aurweb.exceptions import InvariantError, ValidationError, handle_form_exceptions
from aurweb.models import PackageBase
from aurweb.models.package_comment import PackageComment
from aurweb.models.package_keyword import PackageKeyword
@ -88,6 +88,7 @@ async def pkgbase_flag_comment(request: Request, name: str):
@router.post("/pkgbase/{name}/keywords")
@handle_form_exceptions
async def pkgbase_keywords(request: Request, name: str,
keywords: str = Form(default=str())):
pkgbase = get_pkg_or_base(name, PackageBase)
@ -130,6 +131,7 @@ async def pkgbase_flag_get(request: Request, name: str):
@router.post("/pkgbase/{name}/flag")
@handle_form_exceptions
@requires_auth
async def pkgbase_flag_post(request: Request, name: str,
comments: str = Form(default=str())):
@ -158,6 +160,7 @@ async def pkgbase_flag_post(request: Request, name: str,
@router.post("/pkgbase/{name}/comments")
@handle_form_exceptions
@requires_auth
async def pkgbase_comments_post(
request: Request, name: str,
@ -254,6 +257,7 @@ async def pkgbase_comment_edit(request: Request, name: str, id: int,
@router.post("/pkgbase/{name}/comments/{id}")
@handle_form_exceptions
@requires_auth
async def pkgbase_comment_post(
request: Request, name: str, id: int,
@ -294,6 +298,7 @@ async def pkgbase_comment_post(
@router.post("/pkgbase/{name}/comments/{id}/pin")
@handle_form_exceptions
@requires_auth
async def pkgbase_comment_pin(request: Request, name: str, id: int,
next: str = Form(default=None)):
@ -328,6 +333,7 @@ async def pkgbase_comment_pin(request: Request, name: str, id: int,
@router.post("/pkgbase/{name}/comments/{id}/unpin")
@handle_form_exceptions
@requires_auth
async def pkgbase_comment_unpin(request: Request, name: str, id: int,
next: str = Form(default=None)):
@ -361,6 +367,7 @@ async def pkgbase_comment_unpin(request: Request, name: str, id: int,
@router.post("/pkgbase/{name}/comments/{id}/delete")
@handle_form_exceptions
@requires_auth
async def pkgbase_comment_delete(request: Request, name: str, id: int,
next: str = Form(default=None)):
@ -400,6 +407,7 @@ async def pkgbase_comment_delete(request: Request, name: str, id: int,
@router.post("/pkgbase/{name}/comments/{id}/undelete")
@handle_form_exceptions
@requires_auth
async def pkgbase_comment_undelete(request: Request, name: str, id: int,
next: str = Form(default=None)):
@ -438,6 +446,7 @@ async def pkgbase_comment_undelete(request: Request, name: str, id: int,
@router.post("/pkgbase/{name}/vote")
@handle_form_exceptions
@requires_auth
async def pkgbase_vote(request: Request, name: str):
pkgbase = get_pkg_or_base(name, PackageBase)
@ -462,6 +471,7 @@ async def pkgbase_vote(request: Request, name: str):
@router.post("/pkgbase/{name}/unvote")
@handle_form_exceptions
@requires_auth
async def pkgbase_unvote(request: Request, name: str):
pkgbase = get_pkg_or_base(name, PackageBase)
@ -482,6 +492,7 @@ async def pkgbase_unvote(request: Request, name: str):
@router.post("/pkgbase/{name}/notify")
@handle_form_exceptions
@requires_auth
async def pkgbase_notify(request: Request, name: str):
pkgbase = get_pkg_or_base(name, PackageBase)
@ -491,6 +502,7 @@ async def pkgbase_notify(request: Request, name: str):
@router.post("/pkgbase/{name}/unnotify")
@handle_form_exceptions
@requires_auth
async def pkgbase_unnotify(request: Request, name: str):
pkgbase = get_pkg_or_base(name, PackageBase)
@ -500,6 +512,7 @@ async def pkgbase_unnotify(request: Request, name: str):
@router.post("/pkgbase/{name}/unflag")
@handle_form_exceptions
@requires_auth
async def pkgbase_unflag(request: Request, name: str):
pkgbase = get_pkg_or_base(name, PackageBase)
@ -527,6 +540,7 @@ async def pkgbase_disown_get(request: Request, name: str,
@router.post("/pkgbase/{name}/disown")
@handle_form_exceptions
@requires_auth
async def pkgbase_disown_post(request: Request, name: str,
comments: str = Form(default=str()),
@ -565,6 +579,7 @@ async def pkgbase_disown_post(request: Request, name: str,
@router.post("/pkgbase/{name}/adopt")
@handle_form_exceptions
@requires_auth
async def pkgbase_adopt_post(request: Request, name: str):
pkgbase = get_pkg_or_base(name, PackageBase)
@ -607,6 +622,7 @@ async def pkgbase_comaintainers(request: Request, name: str) -> Response:
@router.post("/pkgbase/{name}/comaintainers")
@handle_form_exceptions
@requires_auth
async def pkgbase_comaintainers_post(request: Request, name: str,
users: str = Form(default=str())) \
@ -660,6 +676,7 @@ async def pkgbase_request(request: Request, name: str,
@router.post("/pkgbase/{name}/request")
@handle_form_exceptions
@requires_auth
async def pkgbase_request_post(request: Request, name: str,
type: str = Form(...),
@ -755,6 +772,7 @@ async def pkgbase_delete_get(request: Request, name: str,
@router.post("/pkgbase/{name}/delete")
@handle_form_exceptions
@requires_auth
async def pkgbase_delete_post(request: Request, name: str,
confirm: bool = Form(default=False),
@ -819,6 +837,7 @@ async def pkgbase_merge_get(request: Request, name: str,
@router.post("/pkgbase/{name}/merge")
@handle_form_exceptions
@requires_auth
async def pkgbase_merge_post(request: Request, name: str,
into: str = Form(default=str()),

View file

@ -6,6 +6,7 @@ from sqlalchemy import case
from aurweb import db, defaults, time, util
from aurweb.auth import creds, requires_auth
from aurweb.exceptions import handle_form_exceptions
from aurweb.models import PackageRequest, User
from aurweb.models.package_request import PENDING_ID, REJECTED_ID
from aurweb.requests.util import get_pkgreq_by_id
@ -63,6 +64,7 @@ async def request_close(request: Request, id: int):
@router.post("/requests/{id}/close")
@handle_form_exceptions
@requires_auth
async def request_close_post(request: Request, id: int,
comments: str = Form(default=str())):

View file

@ -11,6 +11,7 @@ from fastapi import APIRouter, Form, Query, Request, Response
from fastapi.responses import JSONResponse
from aurweb import defaults
from aurweb.exceptions import handle_form_exceptions
from aurweb.ratelimit import check_ratelimit
from aurweb.rpc import RPC, documentation
@ -150,6 +151,7 @@ async def rpc(request: Request,
@router.get("/rpc.php") # Temporary! Remove on 03/04
@router.post("/rpc/")
@router.post("/rpc")
@handle_form_exceptions
async def rpc_post(request: Request,
v: Optional[int] = Form(default=None),
type: Optional[str] = Form(default=None),

View file

@ -9,6 +9,7 @@ from sqlalchemy import and_, func, or_
from aurweb import db, l10n, logging, models, time
from aurweb.auth import creds, requires_auth
from aurweb.exceptions import handle_form_exceptions
from aurweb.models import User
from aurweb.models.account_type import TRUSTED_USER_AND_DEV_ID, TRUSTED_USER_ID
from aurweb.templates import make_context, make_variable_context, render_template
@ -173,6 +174,7 @@ async def trusted_user_proposal(request: Request, proposal: int):
@router.post("/tu/{proposal}")
@handle_form_exceptions
@requires_auth
async def trusted_user_proposal_post(request: Request, proposal: int,
decision: str = Form(...)):
@ -245,6 +247,7 @@ async def trusted_user_addvote(request: Request, user: str = str(),
@router.post("/addvote")
@handle_form_exceptions
@requires_auth
async def trusted_user_addvote_post(request: Request,
user: str = Form(default=str()),

View file

@ -7,8 +7,6 @@ import subprocess
import sys
import textwrap
from typing import List, Tuple
from sqlalchemy import and_, or_
import aurweb.config
@ -16,7 +14,7 @@ import aurweb.db
import aurweb.filters
import aurweb.l10n
from aurweb import db, l10n, logging
from aurweb import db, logging
from aurweb.models import PackageBase, User
from aurweb.models.package_comaintainer import PackageComaintainer
from aurweb.models.package_comment import PackageComment
@ -130,48 +128,6 @@ class Notification:
logger.error(str(exc))
class ServerErrorNotification(Notification):
""" A notification used to represent an internal server error. """
def __init__(self, traceback_id: int, version: str, utc: int):
"""
Construct a ServerErrorNotification.
:param traceback_id: Traceback ID
:param version: aurweb version
:param utc: UTC timestamp
"""
self._tb_id = traceback_id
self._version = version
self._utc = utc
postmaster = aurweb.config.get("notifications", "postmaster")
self._to = postmaster
super().__init__()
def get_recipients(self) -> List[Tuple[str, str]]:
from aurweb.auth import AnonymousUser
user = (db.query(User).filter(User.Email == self._to).first()
or AnonymousUser())
return [(self._to, user.LangPreference)]
def get_subject(self, lang: str) -> str:
return l10n.translator.translate("AUR Server Error", lang)
def get_body(self, lang: str) -> str:
""" A forcibly English email body. """
dt = aurweb.filters.timestamp_to_datetime(self._utc)
dts = dt.strftime("%Y-%m-%d %H:%M")
return (f"Traceback ID: {self._tb_id}\n"
f"Location: {aur_location}\n"
f"Version: {self._version}\n"
f"Datetime: {dts} UTC\n")
def get_refs(self):
return (aur_location,)
class ResetKeyNotification(Notification):
def __init__(self, uid):

View file

@ -32,7 +32,7 @@ conventional_commits = true
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "Features"},
{ message = "^fix", group = "Bug Fixes"},
{ message = "^fix", group = "Bugfixes"},
{ message = "^doc", group = "Documentation"},
{ message = "^perf", group = "Performance"},
{ message = "^change", group = "Changes" },

View file

@ -67,9 +67,24 @@ smtp-user =
smtp-password =
sender = notify@aur.archlinux.org
reply-to = noreply@aur.archlinux.org
; Administration email which will receive notifications about
; Gitlab instance base URL. We use this instance to report
; server errors in the form of confidential issues (see error-project).
gitlab-instance = https://gitlab.archlinux.org
; Project URI which will received confidential issues about
; various server details like uncaught exceptions.
postmaster = admin@example.org
; Errors reported will be filed using the 'triage' label, and so
; the 'triage' label must exist in any project URI given.
;
; - must be a valid project URI on notifications.error-repository
; - must contain a 'triage' label
;
error-project = set-me
; Gitlab access token with API privileges to post
; notifications.error-project issues.
error-token = set-me
[fingerprints]
Ed25519 = SHA256:HQ03dn6EasJHNDlt51KpQpFkT3yBX83x7BoIkA1iv2k

View file

@ -8,7 +8,7 @@
#
[tool.poetry]
name = "aurweb"
version = "v6.0.9"
version = "v6.0.10"
license = "GPL-2.0-only"
description = "Source code for the Arch User Repository's website"
homepage = "https://aur.archlinux.org"

View file

@ -63,3 +63,5 @@
{% endif %}
</div>
<!-- Bootstrap typeahead for the homepage. -->
<script type="text/javascript" src="/static/js/typeahead-home.js"></script>

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)

View file

@ -160,7 +160,7 @@ def test_config_main_set_invalid_value():
assert stderr.getvalue().strip() == expected
@ mock.patch("aurweb.config.save", side_effect=noop)
@mock.patch("aurweb.config.save", side_effect=noop)
def test_config_main_set_unknown_section(save: None):
stderr = io.StringIO()