mirror of
https://gitlab.archlinux.org/archlinux/aurweb.git
synced 2025-02-03 10:43:03 +01:00
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:
parent
e2eb3a7ded
commit
7485cc231e
14 changed files with 254 additions and 85 deletions
|
@ -9,6 +9,8 @@ import typing
|
||||||
|
|
||||||
from urllib.parse import quote_plus
|
from urllib.parse import quote_plus
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
from fastapi import FastAPI, HTTPException, Request, Response
|
from fastapi import FastAPI, HTTPException, Request, Response
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
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.prometheus import instrumentator
|
||||||
from aurweb.redis import redis_connection
|
from aurweb.redis import redis_connection
|
||||||
from aurweb.routers import APP_ROUTES
|
from aurweb.routers import APP_ROUTES
|
||||||
from aurweb.scripts import notify
|
|
||||||
from aurweb.templates import make_context, render_template
|
from aurweb.templates import make_context, render_template
|
||||||
|
|
||||||
logger = logging.get_logger(__name__)
|
logger = logging.get_logger(__name__)
|
||||||
|
@ -109,6 +110,10 @@ async def internal_server_error(request: Request, exc: Exception) -> Response:
|
||||||
:param request: FastAPI Request
|
:param request: FastAPI Request
|
||||||
:return: Rendered 500.html template with status_code 500
|
: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")
|
context = make_context(request, "Internal Server Error")
|
||||||
|
|
||||||
# Print out the exception via `traceback` and store the value
|
# Print out the exception via `traceback` and store the value
|
||||||
|
@ -120,38 +125,79 @@ async def internal_server_error(request: Request, exc: Exception) -> Response:
|
||||||
|
|
||||||
# Produce a SHA1 hash of the traceback string.
|
# Produce a SHA1 hash of the traceback string.
|
||||||
tb_hash = hashlib.sha1(tb.encode()).hexdigest()
|
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]
|
tb_id = tb_hash[:7]
|
||||||
|
|
||||||
redis = redis_connection()
|
redis = redis_connection()
|
||||||
pipe = redis.pipeline()
|
|
||||||
key = f"tb:{tb_hash}"
|
key = f"tb:{tb_hash}"
|
||||||
pipe.get(key)
|
retval = redis.get(key)
|
||||||
retval, = pipe.execute()
|
|
||||||
if not retval:
|
if not retval:
|
||||||
# Expire in one hour; this is just done to make sure we
|
# Expire in one hour; this is just done to make sure we
|
||||||
# don't infinitely store these values, but reduce the number
|
# don't infinitely store these values, but reduce the number
|
||||||
# of automated reports (notification below). At this time of
|
# of automated reports (notification below). At this time of
|
||||||
# writing, unexpected exceptions are not common, thus this
|
# writing, unexpected exceptions are not common, thus this
|
||||||
# will not produce a large memory footprint in redis.
|
# will not produce a large memory footprint in redis.
|
||||||
|
pipe = redis.pipeline()
|
||||||
pipe.set(key, tb)
|
pipe.set(key, tb)
|
||||||
pipe.expire(key, 3600)
|
pipe.expire(key, 86400) # One day.
|
||||||
pipe.execute()
|
pipe.execute()
|
||||||
|
|
||||||
# Send out notification about it.
|
# Send out notification about it.
|
||||||
notif = notify.ServerErrorNotification(
|
if "set-me" not in (project, token):
|
||||||
tb_id, context.get("version"), context.get("utcnow"))
|
proj = quote_plus(project)
|
||||||
notif.send()
|
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:
|
else:
|
||||||
retval = retval.decode()
|
# 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.
|
# Log details about the exception traceback.
|
||||||
logger.error(f"FATAL[{tb_id}]: An unexpected exception has occurred.")
|
logger.error(f"FATAL[{tb_id}]: An unexpected exception has occurred.")
|
||||||
logger.error(retval)
|
logger.error(tb)
|
||||||
|
else:
|
||||||
|
retval = retval.decode()
|
||||||
|
|
||||||
return render_template(request, "errors/500.html", context,
|
return render_template(request, "errors/500.html", context,
|
||||||
status_code=http.HTTPStatus.INTERNAL_SERVER_ERROR)
|
status_code=http.HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
|
|
@ -33,6 +33,8 @@ class AnonymousUser:
|
||||||
makes a request against FastAPI. """
|
makes a request against FastAPI. """
|
||||||
# Stub attributes used to mimic a real user.
|
# Stub attributes used to mimic a real user.
|
||||||
ID = 0
|
ID = 0
|
||||||
|
Username = "N/A"
|
||||||
|
Email = "N/A"
|
||||||
|
|
||||||
class AccountType:
|
class AccountType:
|
||||||
""" A stubbed AccountType static class. In here, we use an ID
|
""" A stubbed AccountType static class. In here, we use an ID
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
from typing import Any
|
import functools
|
||||||
|
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
import fastapi
|
||||||
|
|
||||||
|
|
||||||
class AurwebException(Exception):
|
class AurwebException(Exception):
|
||||||
|
@ -90,3 +94,19 @@ class ValidationError(AurwebException):
|
||||||
|
|
||||||
class InvariantError(AurwebException):
|
class InvariantError(AurwebException):
|
||||||
pass
|
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
|
||||||
|
|
|
@ -13,7 +13,7 @@ import aurweb.config
|
||||||
from aurweb import cookies, db, l10n, logging, models, util
|
from aurweb import cookies, db, l10n, logging, models, util
|
||||||
from aurweb.auth import account_type_required, requires_auth, requires_guest
|
from aurweb.auth import account_type_required, requires_auth, requires_guest
|
||||||
from aurweb.captcha import get_captcha_salts
|
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.l10n import get_translator_for_request
|
||||||
from aurweb.models import account_type as at
|
from aurweb.models import account_type as at
|
||||||
from aurweb.models.ssh_pub_key import get_fingerprint
|
from aurweb.models.ssh_pub_key import get_fingerprint
|
||||||
|
@ -35,6 +35,7 @@ async def passreset(request: Request):
|
||||||
|
|
||||||
|
|
||||||
@router.post("/passreset", response_class=HTMLResponse)
|
@router.post("/passreset", response_class=HTMLResponse)
|
||||||
|
@handle_form_exceptions
|
||||||
@requires_guest
|
@requires_guest
|
||||||
async def passreset_post(request: Request,
|
async def passreset_post(request: Request,
|
||||||
user: str = Form(...),
|
user: str = Form(...),
|
||||||
|
@ -253,6 +254,7 @@ async def account_register(request: Request,
|
||||||
|
|
||||||
|
|
||||||
@router.post("/register", response_class=HTMLResponse)
|
@router.post("/register", response_class=HTMLResponse)
|
||||||
|
@handle_form_exceptions
|
||||||
@requires_guest
|
@requires_guest
|
||||||
async def account_register_post(request: Request,
|
async def account_register_post(request: Request,
|
||||||
U: str = Form(default=str()), # Username
|
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)
|
@router.post("/account/{username}/edit", response_class=HTMLResponse)
|
||||||
|
@handle_form_exceptions
|
||||||
@requires_auth
|
@requires_auth
|
||||||
async def account_edit_post(request: Request,
|
async def account_edit_post(request: Request,
|
||||||
username: str,
|
username: str,
|
||||||
|
@ -492,6 +495,7 @@ async def accounts(request: Request):
|
||||||
|
|
||||||
|
|
||||||
@router.post("/accounts")
|
@router.post("/accounts")
|
||||||
|
@handle_form_exceptions
|
||||||
@requires_auth
|
@requires_auth
|
||||||
@account_type_required({at.TRUSTED_USER,
|
@account_type_required({at.TRUSTED_USER,
|
||||||
at.DEVELOPER,
|
at.DEVELOPER,
|
||||||
|
@ -601,6 +605,7 @@ async def terms_of_service(request: Request):
|
||||||
|
|
||||||
|
|
||||||
@router.post("/tos")
|
@router.post("/tos")
|
||||||
|
@handle_form_exceptions
|
||||||
@requires_auth
|
@requires_auth
|
||||||
async def terms_of_service_post(request: Request,
|
async def terms_of_service_post(request: Request,
|
||||||
accept: bool = Form(default=False)):
|
accept: bool = Form(default=False)):
|
||||||
|
|
|
@ -8,6 +8,7 @@ import aurweb.config
|
||||||
|
|
||||||
from aurweb import cookies, db, time
|
from aurweb import cookies, db, time
|
||||||
from aurweb.auth import requires_auth, requires_guest
|
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.l10n import get_translator_for_request
|
||||||
from aurweb.models import User
|
from aurweb.models import User
|
||||||
from aurweb.templates import make_variable_context, render_template
|
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)
|
@router.post("/login", response_class=HTMLResponse)
|
||||||
|
@handle_form_exceptions
|
||||||
@requires_guest
|
@requires_guest
|
||||||
async def login_post(request: Request,
|
async def login_post(request: Request,
|
||||||
next: str = Form(...),
|
next: str = Form(...),
|
||||||
|
@ -82,6 +84,7 @@ async def login_post(request: Request,
|
||||||
|
|
||||||
|
|
||||||
@router.post("/logout")
|
@router.post("/logout")
|
||||||
|
@handle_form_exceptions
|
||||||
@requires_auth
|
@requires_auth
|
||||||
async def logout(request: Request, next: str = Form(default="/")):
|
async def logout(request: Request, next: str = Form(default="/")):
|
||||||
if request.user.is_authenticated():
|
if request.user.is_authenticated():
|
||||||
|
|
|
@ -15,6 +15,7 @@ import aurweb.models.package_request
|
||||||
|
|
||||||
from aurweb import cookies, db, models, time, util
|
from aurweb import cookies, db, models, time, util
|
||||||
from aurweb.cache import db_count_cache
|
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.account_type import TRUSTED_USER_AND_DEV_ID, TRUSTED_USER_ID
|
||||||
from aurweb.models.package_request import PENDING_ID
|
from aurweb.models.package_request import PENDING_ID
|
||||||
from aurweb.packages.util import query_notified, query_voted, updated_packages
|
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)
|
@router.post("/language", response_class=RedirectResponse)
|
||||||
|
@handle_form_exceptions
|
||||||
async def language(request: Request,
|
async def language(request: Request,
|
||||||
set_lang: str = Form(...),
|
set_lang: str = Form(...),
|
||||||
next: str = Form(...),
|
next: str = Form(...),
|
||||||
|
|
|
@ -8,7 +8,7 @@ import aurweb.filters # noqa: F401
|
||||||
|
|
||||||
from aurweb import config, db, defaults, logging, models, util
|
from aurweb import config, db, defaults, logging, models, util
|
||||||
from aurweb.auth import creds, requires_auth
|
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.models.relation_type import CONFLICTS_ID, PROVIDES_ID, REPLACES_ID
|
||||||
from aurweb.packages import util as pkgutil
|
from aurweb.packages import util as pkgutil
|
||||||
from aurweb.packages.search import PackageSearch
|
from aurweb.packages.search import PackageSearch
|
||||||
|
@ -416,6 +416,7 @@ PACKAGE_ACTIONS = {
|
||||||
|
|
||||||
|
|
||||||
@router.post("/packages")
|
@router.post("/packages")
|
||||||
|
@handle_form_exceptions
|
||||||
@requires_auth
|
@requires_auth
|
||||||
async def packages_post(request: Request,
|
async def packages_post(request: Request,
|
||||||
IDs: List[int] = Form(default=[]),
|
IDs: List[int] = Form(default=[]),
|
||||||
|
|
|
@ -6,7 +6,7 @@ from sqlalchemy import and_
|
||||||
|
|
||||||
from aurweb import config, db, l10n, logging, templates, time, util
|
from aurweb import config, db, l10n, logging, templates, time, util
|
||||||
from aurweb.auth import creds, requires_auth
|
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 import PackageBase
|
||||||
from aurweb.models.package_comment import PackageComment
|
from aurweb.models.package_comment import PackageComment
|
||||||
from aurweb.models.package_keyword import PackageKeyword
|
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")
|
@router.post("/pkgbase/{name}/keywords")
|
||||||
|
@handle_form_exceptions
|
||||||
async def pkgbase_keywords(request: Request, name: str,
|
async def pkgbase_keywords(request: Request, name: str,
|
||||||
keywords: str = Form(default=str())):
|
keywords: str = Form(default=str())):
|
||||||
pkgbase = get_pkg_or_base(name, PackageBase)
|
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")
|
@router.post("/pkgbase/{name}/flag")
|
||||||
|
@handle_form_exceptions
|
||||||
@requires_auth
|
@requires_auth
|
||||||
async def pkgbase_flag_post(request: Request, name: str,
|
async def pkgbase_flag_post(request: Request, name: str,
|
||||||
comments: str = Form(default=str())):
|
comments: str = Form(default=str())):
|
||||||
|
@ -158,6 +160,7 @@ async def pkgbase_flag_post(request: Request, name: str,
|
||||||
|
|
||||||
|
|
||||||
@router.post("/pkgbase/{name}/comments")
|
@router.post("/pkgbase/{name}/comments")
|
||||||
|
@handle_form_exceptions
|
||||||
@requires_auth
|
@requires_auth
|
||||||
async def pkgbase_comments_post(
|
async def pkgbase_comments_post(
|
||||||
request: Request, name: str,
|
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}")
|
@router.post("/pkgbase/{name}/comments/{id}")
|
||||||
|
@handle_form_exceptions
|
||||||
@requires_auth
|
@requires_auth
|
||||||
async def pkgbase_comment_post(
|
async def pkgbase_comment_post(
|
||||||
request: Request, name: str, id: int,
|
request: Request, name: str, id: int,
|
||||||
|
@ -294,6 +298,7 @@ async def pkgbase_comment_post(
|
||||||
|
|
||||||
|
|
||||||
@router.post("/pkgbase/{name}/comments/{id}/pin")
|
@router.post("/pkgbase/{name}/comments/{id}/pin")
|
||||||
|
@handle_form_exceptions
|
||||||
@requires_auth
|
@requires_auth
|
||||||
async def pkgbase_comment_pin(request: Request, name: str, id: int,
|
async def pkgbase_comment_pin(request: Request, name: str, id: int,
|
||||||
next: str = Form(default=None)):
|
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")
|
@router.post("/pkgbase/{name}/comments/{id}/unpin")
|
||||||
|
@handle_form_exceptions
|
||||||
@requires_auth
|
@requires_auth
|
||||||
async def pkgbase_comment_unpin(request: Request, name: str, id: int,
|
async def pkgbase_comment_unpin(request: Request, name: str, id: int,
|
||||||
next: str = Form(default=None)):
|
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")
|
@router.post("/pkgbase/{name}/comments/{id}/delete")
|
||||||
|
@handle_form_exceptions
|
||||||
@requires_auth
|
@requires_auth
|
||||||
async def pkgbase_comment_delete(request: Request, name: str, id: int,
|
async def pkgbase_comment_delete(request: Request, name: str, id: int,
|
||||||
next: str = Form(default=None)):
|
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")
|
@router.post("/pkgbase/{name}/comments/{id}/undelete")
|
||||||
|
@handle_form_exceptions
|
||||||
@requires_auth
|
@requires_auth
|
||||||
async def pkgbase_comment_undelete(request: Request, name: str, id: int,
|
async def pkgbase_comment_undelete(request: Request, name: str, id: int,
|
||||||
next: str = Form(default=None)):
|
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")
|
@router.post("/pkgbase/{name}/vote")
|
||||||
|
@handle_form_exceptions
|
||||||
@requires_auth
|
@requires_auth
|
||||||
async def pkgbase_vote(request: Request, name: str):
|
async def pkgbase_vote(request: Request, name: str):
|
||||||
pkgbase = get_pkg_or_base(name, PackageBase)
|
pkgbase = get_pkg_or_base(name, PackageBase)
|
||||||
|
@ -462,6 +471,7 @@ async def pkgbase_vote(request: Request, name: str):
|
||||||
|
|
||||||
|
|
||||||
@router.post("/pkgbase/{name}/unvote")
|
@router.post("/pkgbase/{name}/unvote")
|
||||||
|
@handle_form_exceptions
|
||||||
@requires_auth
|
@requires_auth
|
||||||
async def pkgbase_unvote(request: Request, name: str):
|
async def pkgbase_unvote(request: Request, name: str):
|
||||||
pkgbase = get_pkg_or_base(name, PackageBase)
|
pkgbase = get_pkg_or_base(name, PackageBase)
|
||||||
|
@ -482,6 +492,7 @@ async def pkgbase_unvote(request: Request, name: str):
|
||||||
|
|
||||||
|
|
||||||
@router.post("/pkgbase/{name}/notify")
|
@router.post("/pkgbase/{name}/notify")
|
||||||
|
@handle_form_exceptions
|
||||||
@requires_auth
|
@requires_auth
|
||||||
async def pkgbase_notify(request: Request, name: str):
|
async def pkgbase_notify(request: Request, name: str):
|
||||||
pkgbase = get_pkg_or_base(name, PackageBase)
|
pkgbase = get_pkg_or_base(name, PackageBase)
|
||||||
|
@ -491,6 +502,7 @@ async def pkgbase_notify(request: Request, name: str):
|
||||||
|
|
||||||
|
|
||||||
@router.post("/pkgbase/{name}/unnotify")
|
@router.post("/pkgbase/{name}/unnotify")
|
||||||
|
@handle_form_exceptions
|
||||||
@requires_auth
|
@requires_auth
|
||||||
async def pkgbase_unnotify(request: Request, name: str):
|
async def pkgbase_unnotify(request: Request, name: str):
|
||||||
pkgbase = get_pkg_or_base(name, PackageBase)
|
pkgbase = get_pkg_or_base(name, PackageBase)
|
||||||
|
@ -500,6 +512,7 @@ async def pkgbase_unnotify(request: Request, name: str):
|
||||||
|
|
||||||
|
|
||||||
@router.post("/pkgbase/{name}/unflag")
|
@router.post("/pkgbase/{name}/unflag")
|
||||||
|
@handle_form_exceptions
|
||||||
@requires_auth
|
@requires_auth
|
||||||
async def pkgbase_unflag(request: Request, name: str):
|
async def pkgbase_unflag(request: Request, name: str):
|
||||||
pkgbase = get_pkg_or_base(name, PackageBase)
|
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")
|
@router.post("/pkgbase/{name}/disown")
|
||||||
|
@handle_form_exceptions
|
||||||
@requires_auth
|
@requires_auth
|
||||||
async def pkgbase_disown_post(request: Request, name: str,
|
async def pkgbase_disown_post(request: Request, name: str,
|
||||||
comments: str = Form(default=str()),
|
comments: str = Form(default=str()),
|
||||||
|
@ -565,6 +579,7 @@ async def pkgbase_disown_post(request: Request, name: str,
|
||||||
|
|
||||||
|
|
||||||
@router.post("/pkgbase/{name}/adopt")
|
@router.post("/pkgbase/{name}/adopt")
|
||||||
|
@handle_form_exceptions
|
||||||
@requires_auth
|
@requires_auth
|
||||||
async def pkgbase_adopt_post(request: Request, name: str):
|
async def pkgbase_adopt_post(request: Request, name: str):
|
||||||
pkgbase = get_pkg_or_base(name, PackageBase)
|
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")
|
@router.post("/pkgbase/{name}/comaintainers")
|
||||||
|
@handle_form_exceptions
|
||||||
@requires_auth
|
@requires_auth
|
||||||
async def pkgbase_comaintainers_post(request: Request, name: str,
|
async def pkgbase_comaintainers_post(request: Request, name: str,
|
||||||
users: str = Form(default=str())) \
|
users: str = Form(default=str())) \
|
||||||
|
@ -660,6 +676,7 @@ async def pkgbase_request(request: Request, name: str,
|
||||||
|
|
||||||
|
|
||||||
@router.post("/pkgbase/{name}/request")
|
@router.post("/pkgbase/{name}/request")
|
||||||
|
@handle_form_exceptions
|
||||||
@requires_auth
|
@requires_auth
|
||||||
async def pkgbase_request_post(request: Request, name: str,
|
async def pkgbase_request_post(request: Request, name: str,
|
||||||
type: str = Form(...),
|
type: str = Form(...),
|
||||||
|
@ -755,6 +772,7 @@ async def pkgbase_delete_get(request: Request, name: str,
|
||||||
|
|
||||||
|
|
||||||
@router.post("/pkgbase/{name}/delete")
|
@router.post("/pkgbase/{name}/delete")
|
||||||
|
@handle_form_exceptions
|
||||||
@requires_auth
|
@requires_auth
|
||||||
async def pkgbase_delete_post(request: Request, name: str,
|
async def pkgbase_delete_post(request: Request, name: str,
|
||||||
confirm: bool = Form(default=False),
|
confirm: bool = Form(default=False),
|
||||||
|
@ -819,6 +837,7 @@ async def pkgbase_merge_get(request: Request, name: str,
|
||||||
|
|
||||||
|
|
||||||
@router.post("/pkgbase/{name}/merge")
|
@router.post("/pkgbase/{name}/merge")
|
||||||
|
@handle_form_exceptions
|
||||||
@requires_auth
|
@requires_auth
|
||||||
async def pkgbase_merge_post(request: Request, name: str,
|
async def pkgbase_merge_post(request: Request, name: str,
|
||||||
into: str = Form(default=str()),
|
into: str = Form(default=str()),
|
||||||
|
|
|
@ -6,6 +6,7 @@ from sqlalchemy import case
|
||||||
|
|
||||||
from aurweb import db, defaults, time, util
|
from aurweb import db, defaults, time, util
|
||||||
from aurweb.auth import creds, requires_auth
|
from aurweb.auth import creds, requires_auth
|
||||||
|
from aurweb.exceptions import handle_form_exceptions
|
||||||
from aurweb.models import PackageRequest, User
|
from aurweb.models import PackageRequest, User
|
||||||
from aurweb.models.package_request import PENDING_ID, REJECTED_ID
|
from aurweb.models.package_request import PENDING_ID, REJECTED_ID
|
||||||
from aurweb.requests.util import get_pkgreq_by_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")
|
@router.post("/requests/{id}/close")
|
||||||
|
@handle_form_exceptions
|
||||||
@requires_auth
|
@requires_auth
|
||||||
async def request_close_post(request: Request, id: int,
|
async def request_close_post(request: Request, id: int,
|
||||||
comments: str = Form(default=str())):
|
comments: str = Form(default=str())):
|
||||||
|
|
|
@ -11,6 +11,7 @@ from fastapi import APIRouter, Form, Query, Request, Response
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
from aurweb import defaults
|
from aurweb import defaults
|
||||||
|
from aurweb.exceptions import handle_form_exceptions
|
||||||
from aurweb.ratelimit import check_ratelimit
|
from aurweb.ratelimit import check_ratelimit
|
||||||
from aurweb.rpc import RPC, documentation
|
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.get("/rpc.php") # Temporary! Remove on 03/04
|
||||||
@router.post("/rpc/")
|
@router.post("/rpc/")
|
||||||
@router.post("/rpc")
|
@router.post("/rpc")
|
||||||
|
@handle_form_exceptions
|
||||||
async def rpc_post(request: Request,
|
async def rpc_post(request: Request,
|
||||||
v: Optional[int] = Form(default=None),
|
v: Optional[int] = Form(default=None),
|
||||||
type: Optional[str] = Form(default=None),
|
type: Optional[str] = Form(default=None),
|
||||||
|
|
|
@ -9,6 +9,7 @@ from sqlalchemy import and_, func, or_
|
||||||
|
|
||||||
from aurweb import db, l10n, logging, models, time
|
from aurweb import db, l10n, logging, models, time
|
||||||
from aurweb.auth import creds, requires_auth
|
from aurweb.auth import creds, requires_auth
|
||||||
|
from aurweb.exceptions import handle_form_exceptions
|
||||||
from aurweb.models import User
|
from aurweb.models import User
|
||||||
from aurweb.models.account_type import TRUSTED_USER_AND_DEV_ID, TRUSTED_USER_ID
|
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
|
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}")
|
@router.post("/tu/{proposal}")
|
||||||
|
@handle_form_exceptions
|
||||||
@requires_auth
|
@requires_auth
|
||||||
async def trusted_user_proposal_post(request: Request, proposal: int,
|
async def trusted_user_proposal_post(request: Request, proposal: int,
|
||||||
decision: str = Form(...)):
|
decision: str = Form(...)):
|
||||||
|
@ -245,6 +247,7 @@ async def trusted_user_addvote(request: Request, user: str = str(),
|
||||||
|
|
||||||
|
|
||||||
@router.post("/addvote")
|
@router.post("/addvote")
|
||||||
|
@handle_form_exceptions
|
||||||
@requires_auth
|
@requires_auth
|
||||||
async def trusted_user_addvote_post(request: Request,
|
async def trusted_user_addvote_post(request: Request,
|
||||||
user: str = Form(default=str()),
|
user: str = Form(default=str()),
|
||||||
|
|
|
@ -7,8 +7,6 @@ import subprocess
|
||||||
import sys
|
import sys
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
from typing import List, Tuple
|
|
||||||
|
|
||||||
from sqlalchemy import and_, or_
|
from sqlalchemy import and_, or_
|
||||||
|
|
||||||
import aurweb.config
|
import aurweb.config
|
||||||
|
@ -16,7 +14,7 @@ import aurweb.db
|
||||||
import aurweb.filters
|
import aurweb.filters
|
||||||
import aurweb.l10n
|
import aurweb.l10n
|
||||||
|
|
||||||
from aurweb import db, l10n, logging
|
from aurweb import db, logging
|
||||||
from aurweb.models import PackageBase, User
|
from aurweb.models import PackageBase, User
|
||||||
from aurweb.models.package_comaintainer import PackageComaintainer
|
from aurweb.models.package_comaintainer import PackageComaintainer
|
||||||
from aurweb.models.package_comment import PackageComment
|
from aurweb.models.package_comment import PackageComment
|
||||||
|
@ -130,48 +128,6 @@ class Notification:
|
||||||
logger.error(str(exc))
|
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):
|
class ResetKeyNotification(Notification):
|
||||||
def __init__(self, uid):
|
def __init__(self, uid):
|
||||||
|
|
||||||
|
|
|
@ -67,9 +67,24 @@ smtp-user =
|
||||||
smtp-password =
|
smtp-password =
|
||||||
sender = notify@aur.archlinux.org
|
sender = notify@aur.archlinux.org
|
||||||
reply-to = noreply@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.
|
; 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]
|
[fingerprints]
|
||||||
Ed25519 = SHA256:HQ03dn6EasJHNDlt51KpQpFkT3yBX83x7BoIkA1iv2k
|
Ed25519 = SHA256:HQ03dn6EasJHNDlt51KpQpFkT3yBX83x7BoIkA1iv2k
|
||||||
|
|
|
@ -2,6 +2,7 @@ import http
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
from typing import Callable
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import fastapi
|
import fastapi
|
||||||
|
@ -14,13 +15,39 @@ import aurweb.asgi
|
||||||
import aurweb.config
|
import aurweb.config
|
||||||
import aurweb.redis
|
import aurweb.redis
|
||||||
|
|
||||||
from aurweb.testing.email import Email
|
from aurweb.exceptions import handle_form_exceptions
|
||||||
from aurweb.testing.requests import Request
|
from aurweb.testing.requests import Request
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def setup(db_test, email_test):
|
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
|
@pytest.mark.asyncio
|
||||||
|
@ -77,8 +104,8 @@ async def test_asgi_app_unsupported_backends():
|
||||||
await aurweb.asgi.app_startup()
|
await aurweb.asgi.app_startup()
|
||||||
|
|
||||||
|
|
||||||
def test_internal_server_error(setup: None,
|
@pytest.fixture
|
||||||
caplog: pytest.LogCaptureFixture):
|
def use_traceback():
|
||||||
config_getboolean = aurweb.config.getboolean
|
config_getboolean = aurweb.config.getboolean
|
||||||
|
|
||||||
def mock_getboolean(section: str, key: str) -> bool:
|
def mock_getboolean(section: str, key: str) -> bool:
|
||||||
|
@ -86,34 +113,100 @@ def test_internal_server_error(setup: None,
|
||||||
return True
|
return True
|
||||||
return config_getboolean(section, key)
|
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")
|
@aurweb.asgi.app.get("/internal_server_error")
|
||||||
async def internal_server_error(request: fastapi.Request):
|
async def internal_server_error(request: fastapi.Request):
|
||||||
raise ValueError("test exception")
|
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:
|
with TestClient(app=aurweb.asgi.app) as request:
|
||||||
|
mock_glab_request(FakeResponse(status_code=404))
|
||||||
resp = request.get("/internal_server_error")
|
resp = request.get("/internal_server_error")
|
||||||
assert resp.status_code == int(http.HTTPStatus.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.
|
expr = r"ERROR.*Unable to report exception to"
|
||||||
assert Email.count() == 1
|
assert re.search(expr, caplog.text)
|
||||||
|
|
||||||
aur_location = aurweb.config.get("options", "aur_location")
|
expr = r"FATAL\[.{7}\]"
|
||||||
email = Email(1)
|
assert re.search(expr, caplog.text)
|
||||||
assert f"Location: {aur_location}" in email.body
|
|
||||||
assert "Traceback ID:" in email.body
|
|
||||||
assert "Version:" in email.body
|
def test_internal_server_error_no_token(setup: None, use_traceback: None,
|
||||||
assert "Datetime:" in email.body
|
mock_glab_request: Callable,
|
||||||
assert f"[1] {aur_location}" in email.body
|
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.
|
# Assert that the exception got logged with with its traceback id.
|
||||||
expr = r"FATAL\[.{7}\]"
|
expr = r"FATAL\[.{7}\]"
|
||||||
assert re.search(expr, caplog.text)
|
assert re.search(expr, caplog.text)
|
||||||
|
|
||||||
# Let's do it again; no email should be sent the next time,
|
# Let's do it again to exercise the cached path.
|
||||||
# since the hash is stored in redis.
|
caplog.clear()
|
||||||
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:
|
with TestClient(app=aurweb.asgi.app) as request:
|
||||||
|
mock_glab_request(FakeResponse())
|
||||||
resp = request.get("/internal_server_error")
|
resp = request.get("/internal_server_error")
|
||||||
assert resp.status_code == int(http.HTTPStatus.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)
|
||||||
|
|
Loading…
Add table
Reference in a new issue