mirror of
https://gitlab.archlinux.org/archlinux/aurweb.git
synced 2025-02-03 10:43:03 +01:00
fix(requests): rework handling of requests
This commit changes several things about how we were handling package requests. Modifications (requests): ------------- - `/requests/{id}/close` no longer provides an Accepted selection. All manual request closures will cause a rejection. - Relevent `pkgbase` actions now trigger request closures: `/pkgbase/{name}/delete` (deletion), `/pkgbase/{name}/merge` (merge) and `/pkgbase/{name}/disown` (orphan). - Comment fields have been added to `/pkgbase/{name}/{delete,merge,disown}`, which is used to set the `PackageRequest.ClosureComment` on pending requests. If the comment field is left blank, a closure comment is autogenerated. - Autogenerated request notifications are only sent out once as a closure notification. - Some markup has been fixed. Modifications (disown/orphan): ----------------------------- - Orphan requests are now handled through the same path as deletion/merge. - We now check for due date when disowning as non-maintainer; previously, this was only done for display and not functionally. This check applies to Trusted Users' disowning of a package. This style of notification flow does reduce our visibility, but accounting can still be done via the close request; it includes the action, pkgbase name and the user who accepted it. Closes #204 Signed-off-by: Kevin Morris <kevr@0cost.org>
This commit is contained in:
parent
bad57ba502
commit
26b1674c9e
10 changed files with 1044 additions and 354 deletions
240
aurweb/packages/requests.py
Normal file
240
aurweb/packages/requests.py
Normal file
|
@ -0,0 +1,240 @@
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import List, Optional, Set
|
||||||
|
|
||||||
|
from fastapi import Request
|
||||||
|
from sqlalchemy import and_, orm
|
||||||
|
|
||||||
|
from aurweb import config, db, l10n, util
|
||||||
|
from aurweb.exceptions import InvariantError
|
||||||
|
from aurweb.models import PackageBase, PackageRequest, User
|
||||||
|
from aurweb.models.package_request import ACCEPTED_ID, PENDING_ID, REJECTED_ID
|
||||||
|
from aurweb.models.request_type import DELETION, DELETION_ID, MERGE, MERGE_ID, ORPHAN, ORPHAN_ID
|
||||||
|
from aurweb.scripts import notify
|
||||||
|
|
||||||
|
|
||||||
|
class ClosureFactory:
|
||||||
|
""" A factory class used to autogenerate closure comments. """
|
||||||
|
|
||||||
|
REQTYPE_NAMES = {
|
||||||
|
DELETION_ID: DELETION,
|
||||||
|
MERGE_ID: MERGE,
|
||||||
|
ORPHAN_ID: ORPHAN
|
||||||
|
}
|
||||||
|
|
||||||
|
def _deletion_closure(self, requester: User,
|
||||||
|
pkgbase: PackageBase,
|
||||||
|
target: PackageBase = None):
|
||||||
|
return (f"[Autogenerated] Accepted deletion for {pkgbase.Name}.")
|
||||||
|
|
||||||
|
def _merge_closure(self, requester: User,
|
||||||
|
pkgbase: PackageBase,
|
||||||
|
target: PackageBase = None):
|
||||||
|
return (f"[Autogenerated] Accepted merge for {pkgbase.Name} "
|
||||||
|
"into {target.Name}.")
|
||||||
|
|
||||||
|
def _orphan_closure(self, requester: User,
|
||||||
|
pkgbase: PackageBase,
|
||||||
|
target: PackageBase = None):
|
||||||
|
return (f"[Autogenerated] Accepted orphan for {pkgbase.Name}.")
|
||||||
|
|
||||||
|
def _rejected_merge_closure(self, requester: User,
|
||||||
|
pkgbase: PackageBase,
|
||||||
|
target: PackageBase = None):
|
||||||
|
return (f"[Autogenerated] Another request to merge {pkgbase.Name} "
|
||||||
|
f"into {target.Name} has rendered this request invalid.")
|
||||||
|
|
||||||
|
def get_closure(self, reqtype_id: int,
|
||||||
|
requester: User,
|
||||||
|
pkgbase: PackageBase,
|
||||||
|
target: PackageBase = None,
|
||||||
|
status: int = ACCEPTED_ID) -> str:
|
||||||
|
"""
|
||||||
|
Return a closure comment handled by this class.
|
||||||
|
|
||||||
|
:param reqtype_id: RequestType.ID
|
||||||
|
:param requester: User who is closing a request
|
||||||
|
:param pkgbase: PackageBase instance related to the request
|
||||||
|
:param target: Merge request target PackageBase instance
|
||||||
|
:param status: PackageRequest.Status
|
||||||
|
"""
|
||||||
|
reqtype = ClosureFactory.REQTYPE_NAMES.get(reqtype_id)
|
||||||
|
|
||||||
|
partial = str()
|
||||||
|
if status == REJECTED_ID:
|
||||||
|
partial = "_rejected"
|
||||||
|
|
||||||
|
try:
|
||||||
|
handler = getattr(self, f"{partial}_{reqtype}_closure")
|
||||||
|
except AttributeError:
|
||||||
|
raise NotImplementedError("Unsupported 'reqtype_id' value.")
|
||||||
|
return handler(requester, pkgbase, target)
|
||||||
|
|
||||||
|
|
||||||
|
def update_closure_comment(pkgbase: PackageBase, reqtype_id: int,
|
||||||
|
comments: str, target: PackageBase = None) -> None:
|
||||||
|
"""
|
||||||
|
Update all pending requests related to `pkgbase` with a closure comment.
|
||||||
|
|
||||||
|
In order to persist closure comments through `handle_request`'s
|
||||||
|
algorithm, we must set `PackageRequest.ClosureComment` before calling
|
||||||
|
it. This function can be used to update the closure comment of all
|
||||||
|
package requests related to `pkgbase` and `reqtype_id`.
|
||||||
|
|
||||||
|
If an empty `comments` string is provided, we no-op out of this.
|
||||||
|
|
||||||
|
:param pkgbase: PackageBase instance
|
||||||
|
:param reqtype_id: RequestType.ID
|
||||||
|
:param comments: PackageRequest.ClosureComment to update to
|
||||||
|
:param target: Merge request target PackageBase instance
|
||||||
|
"""
|
||||||
|
if not comments:
|
||||||
|
return
|
||||||
|
|
||||||
|
query = pkgbase.requests.filter(
|
||||||
|
and_(PackageRequest.ReqTypeID == reqtype_id,
|
||||||
|
PackageRequest.Status == PENDING_ID))
|
||||||
|
if reqtype_id == MERGE_ID:
|
||||||
|
query = query.filter(PackageRequest.MergeBaseName == target.Name)
|
||||||
|
|
||||||
|
for pkgreq in query:
|
||||||
|
pkgreq.ClosureComment = comments
|
||||||
|
|
||||||
|
|
||||||
|
def verify_orphan_request(user: User, pkgbase: PackageBase):
|
||||||
|
""" Verify that an undue orphan request exists in `requests`. """
|
||||||
|
is_maint = user == pkgbase.Maintainer
|
||||||
|
if is_maint:
|
||||||
|
return True
|
||||||
|
|
||||||
|
requests = pkgbase.requests.filter(
|
||||||
|
PackageRequest.ReqTypeID == ORPHAN_ID)
|
||||||
|
for pkgreq in requests:
|
||||||
|
idle_time = config.getint("options", "request_idle_time")
|
||||||
|
time_delta = int(datetime.utcnow().timestamp()) - pkgreq.RequestTS
|
||||||
|
is_due = pkgreq.Status == PENDING_ID and time_delta > idle_time
|
||||||
|
if is_due:
|
||||||
|
# If the requester is the pkgbase maintainer or the
|
||||||
|
# request is already due, we're good to go: return True.
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def close_pkgreq(pkgreq: PackageRequest, closer: User,
|
||||||
|
pkgbase: PackageBase, target: Optional[PackageBase],
|
||||||
|
status: int) -> None:
|
||||||
|
"""
|
||||||
|
Close a package request with `pkgreq`.Status == `status`.
|
||||||
|
|
||||||
|
:param pkgreq: PackageRequest instance
|
||||||
|
:param closer: `pkgreq`.Closer User instance to update to
|
||||||
|
:param pkgbase: PackageBase instance which `pkgreq` is about
|
||||||
|
:param target: Optional PackageBase instance to merge into
|
||||||
|
:param status: `pkgreq`.Status value to update to
|
||||||
|
"""
|
||||||
|
now = int(datetime.utcnow().timestamp())
|
||||||
|
pkgreq.Status = status
|
||||||
|
pkgreq.Closer = closer
|
||||||
|
pkgreq.ClosureComment = (
|
||||||
|
pkgreq.ClosureComment or ClosureFactory().get_closure(
|
||||||
|
pkgreq.ReqTypeID, closer, pkgbase, target, status)
|
||||||
|
)
|
||||||
|
pkgreq.ClosedTS = now
|
||||||
|
|
||||||
|
|
||||||
|
def handle_request(request: Request, reqtype_id: int,
|
||||||
|
pkgbase: PackageBase,
|
||||||
|
target: PackageBase = None) -> List[notify.Notification]:
|
||||||
|
"""
|
||||||
|
Handle package requests before performing an action.
|
||||||
|
|
||||||
|
The actions we're interested in are disown (orphan), delete and
|
||||||
|
merge. There is now an automated request generation and closure
|
||||||
|
notification when a privileged user performs one of these actions
|
||||||
|
without a pre-existing request. They all commit changes to the
|
||||||
|
database, and thus before calling, state should be verified to
|
||||||
|
avoid leaked database records regarding these requests.
|
||||||
|
|
||||||
|
Otherwise, we accept and reject requests based on their state
|
||||||
|
and send out the relevent notifications.
|
||||||
|
|
||||||
|
:param requester: User who needs this a `pkgbase` request handled
|
||||||
|
:param reqtype_id: RequestType.ID
|
||||||
|
:param pkgbase: PackageBase which the request is about
|
||||||
|
:param target: Optional target to merge into
|
||||||
|
"""
|
||||||
|
notifs: List[notify.Notification] = []
|
||||||
|
|
||||||
|
# If it's an orphan request, perform further verification
|
||||||
|
# regarding existing requests.
|
||||||
|
if reqtype_id == ORPHAN_ID:
|
||||||
|
if not verify_orphan_request(request.user, pkgbase):
|
||||||
|
_ = l10n.get_translator_for_request(request)
|
||||||
|
raise InvariantError(_(
|
||||||
|
"No due existing orphan requests to accept for %s."
|
||||||
|
) % pkgbase.Name)
|
||||||
|
|
||||||
|
# Produce a base query for requests related to `pkgbase`, based
|
||||||
|
# on ReqTypeID matching `reqtype_id`, pending status and a correct
|
||||||
|
# PackagBaseName column.
|
||||||
|
query: orm.Query = pkgbase.requests.filter(
|
||||||
|
and_(PackageRequest.ReqTypeID == reqtype_id,
|
||||||
|
PackageRequest.Status == PENDING_ID,
|
||||||
|
PackageRequest.PackageBaseName == pkgbase.Name))
|
||||||
|
|
||||||
|
# Build a query for records we should accept. For merge requests,
|
||||||
|
# this is specific to a matching MergeBaseName. For others, this
|
||||||
|
# just ends up becoming `query`.
|
||||||
|
accept_query: orm.Query = query
|
||||||
|
if target:
|
||||||
|
# If a `target` was supplied, filter by MergeBaseName
|
||||||
|
accept_query = query.filter(
|
||||||
|
PackageRequest.MergeBaseName == target.Name)
|
||||||
|
|
||||||
|
# Build an accept list out of `accept_query`.
|
||||||
|
to_accept: List[PackageRequest] = accept_query.all()
|
||||||
|
accepted_ids: Set[int] = set(p.ID for p in to_accept)
|
||||||
|
|
||||||
|
# Build a reject list out of `query` filtered by IDs not found
|
||||||
|
# in `to_accept`. That is, unmatched records of the same base
|
||||||
|
# query properties.
|
||||||
|
to_reject: List[PackageRequest] = query.filter(
|
||||||
|
~PackageRequest.ID.in_(accepted_ids)
|
||||||
|
).all()
|
||||||
|
|
||||||
|
# If we have no requests to accept, create a new one.
|
||||||
|
# This is done to increase tracking of actions occurring
|
||||||
|
# through the website.
|
||||||
|
if not to_accept:
|
||||||
|
with db.begin():
|
||||||
|
pkgreq = db.create(PackageRequest,
|
||||||
|
ReqTypeID=reqtype_id,
|
||||||
|
User=request.user,
|
||||||
|
PackageBase=pkgbase,
|
||||||
|
PackageBaseName=pkgbase.Name,
|
||||||
|
Comments="Autogenerated by aurweb.",
|
||||||
|
ClosureComment=str())
|
||||||
|
|
||||||
|
# If it's a merge request, set MergeBaseName to `target`.Name.
|
||||||
|
if pkgreq.ReqTypeID == MERGE_ID:
|
||||||
|
pkgreq.MergeBaseName = target.Name
|
||||||
|
|
||||||
|
# Add the new request to `to_accept` and allow standard
|
||||||
|
# flow to continue afterward.
|
||||||
|
to_accept.append(pkgreq)
|
||||||
|
|
||||||
|
# Update requests with their new status and closures.
|
||||||
|
with db.begin():
|
||||||
|
util.apply_all(to_accept, lambda p: close_pkgreq(
|
||||||
|
p, request.user, pkgbase, target, ACCEPTED_ID))
|
||||||
|
util.apply_all(to_reject, lambda p: close_pkgreq(
|
||||||
|
p, request.user, pkgbase, target, REJECTED_ID))
|
||||||
|
|
||||||
|
# Create RequestCloseNotifications for all requests involved.
|
||||||
|
for pkgreq in (to_accept + to_reject):
|
||||||
|
notif = notify.RequestCloseNotification(
|
||||||
|
request.user.ID, pkgreq.ID, pkgreq.status_display())
|
||||||
|
notifs.append(notif)
|
||||||
|
|
||||||
|
# Return notifications to the caller for sending.
|
||||||
|
return notifs
|
|
@ -4,19 +4,20 @@ from typing import Any, Dict, List
|
||||||
|
|
||||||
from fastapi import APIRouter, Form, HTTPException, Query, Request, Response
|
from fastapi import APIRouter, Form, HTTPException, Query, Request, Response
|
||||||
from fastapi.responses import JSONResponse, RedirectResponse
|
from fastapi.responses import JSONResponse, RedirectResponse
|
||||||
from sqlalchemy import case
|
from sqlalchemy import and_, case
|
||||||
|
|
||||||
import aurweb.filters
|
import aurweb.filters
|
||||||
import aurweb.packages.util
|
import aurweb.packages.util
|
||||||
|
|
||||||
from aurweb import db, defaults, l10n, logging, models, util
|
from aurweb import db, defaults, l10n, logging, models, util
|
||||||
from aurweb.auth import auth_required, creds
|
from aurweb.auth import auth_required, creds
|
||||||
from aurweb.exceptions import ValidationError
|
from aurweb.exceptions import InvariantError, ValidationError
|
||||||
from aurweb.models.package_request import ACCEPTED_ID, PENDING_ID, REJECTED_ID
|
from aurweb.models.package_request import ACCEPTED_ID, PENDING_ID, REJECTED_ID
|
||||||
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.models.request_type import DELETION_ID, MERGE, MERGE_ID
|
from aurweb.models.request_type import DELETION_ID, MERGE_ID, ORPHAN_ID
|
||||||
from aurweb.packages import util as pkgutil
|
from aurweb.packages import util as pkgutil
|
||||||
from aurweb.packages import validate
|
from aurweb.packages import validate
|
||||||
|
from aurweb.packages.requests import handle_request, update_closure_comment
|
||||||
from aurweb.packages.search import PackageSearch
|
from aurweb.packages.search import PackageSearch
|
||||||
from aurweb.packages.util import get_pkg_or_base, get_pkgbase_comment, get_pkgreq_by_id, query_notified, query_voted
|
from aurweb.packages.util import get_pkg_or_base, get_pkgbase_comment, get_pkgreq_by_id, query_notified, query_voted
|
||||||
from aurweb.scripts import notify, popupdate
|
from aurweb.scripts import notify, popupdate
|
||||||
|
@ -115,66 +116,30 @@ async def packages(request: Request) -> Response:
|
||||||
return await packages_get(request, context)
|
return await packages_get(request, context)
|
||||||
|
|
||||||
|
|
||||||
def create_request_if_missing(requests: List[models.PackageRequest],
|
def delete_package(request: Request, package: models.Package,
|
||||||
reqtype: models.RequestType,
|
merge_into: models.PackageBase = None,
|
||||||
user: models.User,
|
comments: str = str()):
|
||||||
package: models.Package):
|
|
||||||
now = int(datetime.utcnow().timestamp())
|
|
||||||
pkgreq = db.query(models.PackageRequest).filter(
|
|
||||||
models.PackageRequest.PackageBaseName == package.PackageBase.Name
|
|
||||||
).first()
|
|
||||||
if not pkgreq:
|
|
||||||
# No PackageRequest existed. Create one.
|
|
||||||
comments = "Automatically generated by aurweb."
|
|
||||||
closure_comment = "Deleted by aurweb."
|
|
||||||
pkgreq = db.create(models.PackageRequest,
|
|
||||||
RequestType=reqtype,
|
|
||||||
PackageBase=package.PackageBase,
|
|
||||||
PackageBaseName=package.PackageBase.Name,
|
|
||||||
User=user,
|
|
||||||
Status=ACCEPTED_ID,
|
|
||||||
Comments=comments,
|
|
||||||
ClosureComment=closure_comment,
|
|
||||||
ClosedTS=now,
|
|
||||||
Closer=user)
|
|
||||||
requests.append(pkgreq)
|
|
||||||
return pkgreq
|
|
||||||
|
|
||||||
|
|
||||||
def delete_package(deleter: models.User, package: models.Package):
|
|
||||||
notifications = []
|
|
||||||
requests = []
|
|
||||||
bases_to_delete = []
|
bases_to_delete = []
|
||||||
|
|
||||||
# In all cases, though, just delete the Package in question.
|
target = db.query(models.PackageBase).filter(
|
||||||
if package.PackageBase.packages.count() == 1:
|
models.PackageBase.Name == merge_into
|
||||||
reqtype = db.query(models.RequestType).filter(
|
|
||||||
models.RequestType.ID == DELETION_ID
|
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
with db.begin():
|
notifs = []
|
||||||
pkgreq = create_request_if_missing(
|
# In all cases, though, just delete the Package in question.
|
||||||
requests, reqtype, deleter, package)
|
if package.PackageBase.packages.count() == 1:
|
||||||
pkgreq.Status = ACCEPTED_ID
|
notifs = handle_request(request, DELETION_ID, package.PackageBase,
|
||||||
|
target=target)
|
||||||
|
|
||||||
bases_to_delete.append(package.PackageBase)
|
bases_to_delete.append(package.PackageBase)
|
||||||
|
|
||||||
# Prepare DeleteNotification.
|
with db.begin():
|
||||||
notifications.append(
|
update_closure_comment(package.PackageBase, DELETION_ID, comments,
|
||||||
notify.DeleteNotification(deleter.ID, package.PackageBase.ID)
|
target=target)
|
||||||
)
|
|
||||||
|
|
||||||
# For each PackageRequest created, mock up an open and close notification.
|
# Prepare DeleteNotification.
|
||||||
basename = package.PackageBase.Name
|
notifs.append(
|
||||||
for pkgreq in requests:
|
notify.DeleteNotification(request.user.ID, package.PackageBase.ID)
|
||||||
notifications.append(
|
|
||||||
notify.RequestOpenNotification(
|
|
||||||
deleter.ID, pkgreq.ID, reqtype.Name,
|
|
||||||
pkgreq.PackageBase.ID, merge_into=basename or None)
|
|
||||||
)
|
|
||||||
notifications.append(
|
|
||||||
notify.RequestCloseNotification(
|
|
||||||
deleter.ID, pkgreq.ID, pkgreq.status_display())
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Perform all the deletions.
|
# Perform all the deletions.
|
||||||
|
@ -183,7 +148,7 @@ def delete_package(deleter: models.User, package: models.Package):
|
||||||
db.delete_all(bases_to_delete)
|
db.delete_all(bases_to_delete)
|
||||||
|
|
||||||
# Send out all the notifications.
|
# Send out all the notifications.
|
||||||
util.apply_all(notifications, lambda n: n.send())
|
util.apply_all(notifs, lambda n: n.send())
|
||||||
|
|
||||||
|
|
||||||
async def make_single_context(request: Request,
|
async def make_single_context(request: Request,
|
||||||
|
@ -676,22 +641,26 @@ async def pkgbase_request_post(request: Request, name: str,
|
||||||
auto_orphan_age = aurweb.config.getint("options", "auto_orphan_age")
|
auto_orphan_age = aurweb.config.getint("options", "auto_orphan_age")
|
||||||
auto_delete_age = aurweb.config.getint("options", "auto_delete_age")
|
auto_delete_age = aurweb.config.getint("options", "auto_delete_age")
|
||||||
|
|
||||||
flagged = pkgbase.OutOfDateTS and pkgbase.OutOfDateTS >= auto_orphan_age
|
ood_ts = pkgbase.OutOfDateTS or 0
|
||||||
|
flagged = ood_ts and (now - ood_ts) >= auto_orphan_age
|
||||||
is_maintainer = pkgbase.Maintainer == request.user
|
is_maintainer = pkgbase.Maintainer == request.user
|
||||||
outdated = now - pkgbase.SubmittedTS <= auto_delete_age
|
outdated = (now - pkgbase.SubmittedTS) <= auto_delete_age
|
||||||
|
|
||||||
if type == "orphan" and flagged:
|
if type == "orphan" and flagged:
|
||||||
|
# This request should be auto-accepted.
|
||||||
with db.begin():
|
with db.begin():
|
||||||
pkgbase.Maintainer = None
|
pkgbase.Maintainer = None
|
||||||
pkgreq.Status = ACCEPTED_ID
|
pkgreq.Status = ACCEPTED_ID
|
||||||
db.refresh(pkgreq)
|
|
||||||
notif = notify.RequestCloseNotification(
|
notif = notify.RequestCloseNotification(
|
||||||
request.user.ID, pkgreq.ID, pkgreq.status_display())
|
request.user.ID, pkgreq.ID, pkgreq.status_display())
|
||||||
notif.send()
|
notif.send()
|
||||||
|
logger.debug(f"New request #{pkgreq.ID} is marked for auto-orphan.")
|
||||||
elif type == "deletion" and is_maintainer and outdated:
|
elif type == "deletion" and is_maintainer and outdated:
|
||||||
|
# This request should be auto-accepted.
|
||||||
packages = pkgbase.packages.all()
|
packages = pkgbase.packages.all()
|
||||||
for package in packages:
|
for package in packages:
|
||||||
delete_package(request.user, package)
|
delete_package(request, package)
|
||||||
|
logger.debug(f"New request #{pkgreq.ID} is marked for auto-deletion.")
|
||||||
|
|
||||||
# Redirect the submitting user to /packages.
|
# Redirect the submitting user to /packages.
|
||||||
return RedirectResponse("/packages", status_code=HTTPStatus.SEE_OTHER)
|
return RedirectResponse("/packages", status_code=HTTPStatus.SEE_OTHER)
|
||||||
|
@ -713,31 +682,24 @@ async def requests_close(request: Request, id: int):
|
||||||
@router.post("/requests/{id}/close")
|
@router.post("/requests/{id}/close")
|
||||||
@auth_required()
|
@auth_required()
|
||||||
async def requests_close_post(request: Request, id: int,
|
async def requests_close_post(request: Request, id: int,
|
||||||
reason: int = Form(default=0),
|
|
||||||
comments: str = Form(default=str())):
|
comments: str = Form(default=str())):
|
||||||
pkgreq = get_pkgreq_by_id(id)
|
pkgreq = get_pkgreq_by_id(id)
|
||||||
if not request.user.is_elevated() and request.user != pkgreq.User:
|
|
||||||
|
# `pkgreq`.User can close their own request.
|
||||||
|
approved = [pkgreq.User]
|
||||||
|
if not request.user.has_credential(creds.PKGREQ_CLOSE, approved=approved):
|
||||||
# Request user doesn't have permission here: redirect to '/'.
|
# Request user doesn't have permission here: redirect to '/'.
|
||||||
return RedirectResponse("/", status_code=HTTPStatus.SEE_OTHER)
|
return RedirectResponse("/", status_code=HTTPStatus.SEE_OTHER)
|
||||||
|
|
||||||
context = make_context(request, "Close Request")
|
context = make_context(request, "Close Request")
|
||||||
context["pkgreq"] = pkgreq
|
context["pkgreq"] = pkgreq
|
||||||
|
|
||||||
if reason not in {ACCEPTED_ID, REJECTED_ID}:
|
now = int(datetime.utcnow().timestamp())
|
||||||
# If the provided reason is not valid, send the user back to
|
|
||||||
# the closure form with a BAD_REQUEST status.
|
|
||||||
return render_template(request, "requests/close.html", context,
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST)
|
|
||||||
|
|
||||||
if not request.user.is_elevated():
|
|
||||||
# If we're closing the request as the user who created it,
|
|
||||||
# the reason should just be a REJECTION.
|
|
||||||
reason = REJECTED_ID
|
|
||||||
|
|
||||||
with db.begin():
|
with db.begin():
|
||||||
pkgreq.Closer = request.user
|
pkgreq.Closer = request.user
|
||||||
pkgreq.Status = reason
|
|
||||||
pkgreq.ClosureComment = comments
|
pkgreq.ClosureComment = comments
|
||||||
|
pkgreq.ClosedTS = now
|
||||||
|
pkgreq.Status = REJECTED_ID
|
||||||
|
|
||||||
notify_ = notify.RequestCloseNotification(
|
notify_ = notify.RequestCloseNotification(
|
||||||
request.user.ID, pkgreq.ID, pkgreq.status_display())
|
request.user.ID, pkgreq.ID, pkgreq.status_display())
|
||||||
|
@ -932,7 +894,8 @@ async def pkgbase_unvote(request: Request, name: str):
|
||||||
|
|
||||||
def pkgbase_disown_instance(request: Request, pkgbase: models.PackageBase):
|
def pkgbase_disown_instance(request: Request, pkgbase: models.PackageBase):
|
||||||
disowner = request.user
|
disowner = request.user
|
||||||
notif = notify.DisownNotification(disowner.ID, pkgbase.ID)
|
notifs = [notify.DisownNotification(disowner.ID, pkgbase.ID)]
|
||||||
|
notifs += handle_request(request, ORPHAN_ID, pkgbase)
|
||||||
|
|
||||||
if disowner != pkgbase.Maintainer:
|
if disowner != pkgbase.Maintainer:
|
||||||
with db.begin():
|
with db.begin():
|
||||||
|
@ -949,7 +912,7 @@ def pkgbase_disown_instance(request: Request, pkgbase: models.PackageBase):
|
||||||
else:
|
else:
|
||||||
pkgbase.Maintainer = None
|
pkgbase.Maintainer = None
|
||||||
|
|
||||||
notif.send()
|
util.apply_all(notifs, lambda n: n.send())
|
||||||
|
|
||||||
|
|
||||||
@router.get("/pkgbase/{name}/disown")
|
@router.get("/pkgbase/{name}/disown")
|
||||||
|
@ -971,6 +934,7 @@ async def pkgbase_disown_get(request: Request, name: str):
|
||||||
@router.post("/pkgbase/{name}/disown")
|
@router.post("/pkgbase/{name}/disown")
|
||||||
@auth_required()
|
@auth_required()
|
||||||
async def pkgbase_disown_post(request: Request, name: str,
|
async def pkgbase_disown_post(request: Request, name: str,
|
||||||
|
comments: str = Form(default=str()),
|
||||||
confirm: bool = Form(default=False)):
|
confirm: bool = Form(default=False)):
|
||||||
pkgbase = get_pkg_or_base(name, models.PackageBase)
|
pkgbase = get_pkg_or_base(name, models.PackageBase)
|
||||||
|
|
||||||
|
@ -980,15 +944,24 @@ async def pkgbase_disown_post(request: Request, name: str,
|
||||||
return RedirectResponse(f"/pkgbase/{name}",
|
return RedirectResponse(f"/pkgbase/{name}",
|
||||||
HTTPStatus.SEE_OTHER)
|
HTTPStatus.SEE_OTHER)
|
||||||
|
|
||||||
if not confirm:
|
|
||||||
context = make_context(request, "Disown Package")
|
context = make_context(request, "Disown Package")
|
||||||
context["pkgbase"] = pkgbase
|
context["pkgbase"] = pkgbase
|
||||||
|
if not confirm:
|
||||||
context["errors"] = [("The selected packages have not been disowned, "
|
context["errors"] = [("The selected packages have not been disowned, "
|
||||||
"check the confirmation checkbox.")]
|
"check the confirmation checkbox.")]
|
||||||
return render_template(request, "packages/disown.html", context,
|
return render_template(request, "packages/disown.html", context,
|
||||||
status_code=HTTPStatus.BAD_REQUEST)
|
status_code=HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
|
with db.begin():
|
||||||
|
update_closure_comment(pkgbase, ORPHAN_ID, comments)
|
||||||
|
|
||||||
|
try:
|
||||||
pkgbase_disown_instance(request, pkgbase)
|
pkgbase_disown_instance(request, pkgbase)
|
||||||
|
except InvariantError as exc:
|
||||||
|
context["errors"] = [str(exc)]
|
||||||
|
return render_template(request, "packages/disown.html", context,
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
return RedirectResponse(f"/pkgbase/{name}",
|
return RedirectResponse(f"/pkgbase/{name}",
|
||||||
status_code=HTTPStatus.SEE_OTHER)
|
status_code=HTTPStatus.SEE_OTHER)
|
||||||
|
|
||||||
|
@ -1032,7 +1005,8 @@ async def pkgbase_delete_get(request: Request, name: str):
|
||||||
@router.post("/pkgbase/{name}/delete")
|
@router.post("/pkgbase/{name}/delete")
|
||||||
@auth_required()
|
@auth_required()
|
||||||
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),
|
||||||
|
comments: str = Form(default=str())):
|
||||||
pkgbase = get_pkg_or_base(name, models.PackageBase)
|
pkgbase = get_pkg_or_base(name, models.PackageBase)
|
||||||
|
|
||||||
if not request.user.has_credential(creds.PKGBASE_DELETE):
|
if not request.user.has_credential(creds.PKGBASE_DELETE):
|
||||||
|
@ -1047,10 +1021,20 @@ async def pkgbase_delete_post(request: Request, name: str,
|
||||||
return render_template(request, "packages/delete.html", context,
|
return render_template(request, "packages/delete.html", context,
|
||||||
status_code=HTTPStatus.BAD_REQUEST)
|
status_code=HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
|
if comments:
|
||||||
|
# Update any existing deletion requests' ClosureComment.
|
||||||
|
with db.begin():
|
||||||
|
requests = pkgbase.requests.filter(
|
||||||
|
and_(models.PackageRequest.Status == PENDING_ID,
|
||||||
|
models.PackageRequest.ReqTypeID == DELETION_ID)
|
||||||
|
)
|
||||||
|
for pkgreq in requests:
|
||||||
|
pkgreq.ClosureComment = comments
|
||||||
|
|
||||||
# Obtain deletion locks and delete the packages.
|
# Obtain deletion locks and delete the packages.
|
||||||
packages = pkgbase.packages.all()
|
packages = pkgbase.packages.all()
|
||||||
for package in packages:
|
for package in packages:
|
||||||
delete_package(request.user, package)
|
delete_package(request, package, comments=comments)
|
||||||
|
|
||||||
return RedirectResponse("/packages", status_code=HTTPStatus.SEE_OTHER)
|
return RedirectResponse("/packages", status_code=HTTPStatus.SEE_OTHER)
|
||||||
|
|
||||||
|
@ -1190,6 +1174,17 @@ async def packages_adopt(request: Request, package_ids: List[int] = [],
|
||||||
return (True, ["The selected packages have been adopted."])
|
return (True, ["The selected packages have been adopted."])
|
||||||
|
|
||||||
|
|
||||||
|
def disown_all(request: Request, pkgbases: List[models.PackageBase]) \
|
||||||
|
-> List[str]:
|
||||||
|
errors = []
|
||||||
|
for pkgbase in pkgbases:
|
||||||
|
try:
|
||||||
|
pkgbase_disown_instance(request, pkgbase)
|
||||||
|
except InvariantError as exc:
|
||||||
|
errors.append(str(exc))
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
async def packages_disown(request: Request, package_ids: List[int] = [],
|
async def packages_disown(request: Request, package_ids: List[int] = [],
|
||||||
confirm: bool = False, **kwargs):
|
confirm: bool = False, **kwargs):
|
||||||
if not package_ids:
|
if not package_ids:
|
||||||
|
@ -1217,9 +1212,9 @@ async def packages_disown(request: Request, package_ids: List[int] = [],
|
||||||
return (False, ["You are not allowed to disown one "
|
return (False, ["You are not allowed to disown one "
|
||||||
"of the packages you selected."])
|
"of the packages you selected."])
|
||||||
|
|
||||||
# Now, really disown the bases.
|
# Now, disown all the bases if we can.
|
||||||
for pkgbase in bases:
|
if errors := disown_all(request, bases):
|
||||||
pkgbase_disown_instance(request, pkgbase)
|
return (False, errors)
|
||||||
|
|
||||||
return (True, ["The selected packages have been disowned."])
|
return (True, ["The selected packages have been disowned."])
|
||||||
|
|
||||||
|
@ -1256,7 +1251,7 @@ async def packages_delete(request: Request, package_ids: List[int] = [],
|
||||||
# using the same method we use in our /pkgbase/{name}/delete route.
|
# using the same method we use in our /pkgbase/{name}/delete route.
|
||||||
for pkg in packages:
|
for pkg in packages:
|
||||||
deleted_pkgs.append(pkg.Name)
|
deleted_pkgs.append(pkg.Name)
|
||||||
delete_package(request.user, pkg)
|
delete_package(request, pkg)
|
||||||
|
|
||||||
# Log out the fact that this happened for accountability.
|
# Log out the fact that this happened for accountability.
|
||||||
logger.info(f"Privileged user '{request.user.Username}' deleted the "
|
logger.info(f"Privileged user '{request.user.Username}' deleted the "
|
||||||
|
@ -1341,83 +1336,46 @@ async def pkgbase_merge_get(request: Request, name: str,
|
||||||
|
|
||||||
|
|
||||||
def pkgbase_merge_instance(request: Request, pkgbase: models.PackageBase,
|
def pkgbase_merge_instance(request: Request, pkgbase: models.PackageBase,
|
||||||
target: models.PackageBase):
|
target: models.PackageBase, comments: str = str()):
|
||||||
pkgbasename = str(pkgbase.Name)
|
pkgbasename = str(pkgbase.Name)
|
||||||
|
|
||||||
# Collect requests related to this merge.
|
# Create notifications.
|
||||||
query = pkgbase.requests.filter(
|
notifs = handle_request(request, MERGE_ID, pkgbase, target)
|
||||||
models.PackageRequest.ReqTypeID == MERGE_ID
|
|
||||||
)
|
|
||||||
|
|
||||||
requests = query.filter(
|
# Target votes and notifications sets of user IDs that are
|
||||||
models.PackageRequest.MergeBaseName == target.Name).all()
|
# looking to be migrated.
|
||||||
reject_requests = query.filter(
|
target_votes = set(v.UsersID for v in target.package_votes)
|
||||||
models.PackageRequest.MergeBaseName != target.Name).all()
|
target_notifs = set(n.UserID for n in target.notifications)
|
||||||
|
|
||||||
notifs = [] # Used to keep track of notifications over the function.
|
|
||||||
closure_comment = (f"Merged into package base {target.Name} by "
|
|
||||||
f"{request.user.Username}.")
|
|
||||||
rejected_closure_comment = ("Rejected because another merge request "
|
|
||||||
"for the same package base was accepted.")
|
|
||||||
|
|
||||||
if not requests:
|
|
||||||
# If there are no requests, create one owned by request.user.
|
|
||||||
with db.begin():
|
|
||||||
pkgreq = db.create(models.PackageRequest,
|
|
||||||
ReqTypeID=MERGE_ID,
|
|
||||||
User=request.user,
|
|
||||||
PackageBase=pkgbase,
|
|
||||||
PackageBaseName=pkgbasename,
|
|
||||||
MergeBaseName=target.Name,
|
|
||||||
Comments="Generated by aurweb.",
|
|
||||||
Status=ACCEPTED_ID,
|
|
||||||
ClosureComment=closure_comment)
|
|
||||||
requests.append(pkgreq)
|
|
||||||
|
|
||||||
# Add a notification about the opening to our notifs array.
|
|
||||||
notif = notify.RequestOpenNotification(
|
|
||||||
request.user.ID, pkgreq.ID, MERGE,
|
|
||||||
pkgbase.ID, merge_into=target.Name)
|
|
||||||
notifs.append(notif)
|
|
||||||
|
|
||||||
with db.begin():
|
with db.begin():
|
||||||
# Merge pkgbase's comments, notifications and votes into target.
|
# Merge pkgbase's comments.
|
||||||
for comment in pkgbase.comments:
|
for comment in pkgbase.comments:
|
||||||
comment.PackageBase = target
|
comment.PackageBase = target
|
||||||
|
|
||||||
|
# Merge notifications that don't yet exist in the target.
|
||||||
for notif in pkgbase.notifications:
|
for notif in pkgbase.notifications:
|
||||||
|
if notif.UserID not in target_notifs:
|
||||||
notif.PackageBase = target
|
notif.PackageBase = target
|
||||||
|
|
||||||
|
# Merge votes that don't yet exist in the target.
|
||||||
for vote in pkgbase.package_votes:
|
for vote in pkgbase.package_votes:
|
||||||
|
if vote.UsersID not in target_votes:
|
||||||
vote.PackageBase = target
|
vote.PackageBase = target
|
||||||
|
|
||||||
|
# Run popupdate.
|
||||||
|
popupdate.run_single(target)
|
||||||
|
|
||||||
with db.begin():
|
with db.begin():
|
||||||
# Delete pkgbase and its packages now that everything's merged.
|
# Delete pkgbase and its packages now that everything's merged.
|
||||||
for pkg in pkgbase.packages:
|
for pkg in pkgbase.packages:
|
||||||
db.delete(pkg)
|
db.delete(pkg)
|
||||||
db.delete(pkgbase)
|
db.delete(pkgbase)
|
||||||
|
|
||||||
# Accept merge requests related to this pkgbase and target.
|
|
||||||
for pkgreq in requests:
|
|
||||||
pkgreq.Status = ACCEPTED_ID
|
|
||||||
pkgreq.ClosureComment = closure_comment
|
|
||||||
pkgreq.Closer = request.user
|
|
||||||
|
|
||||||
for pkgreq in reject_requests:
|
|
||||||
pkgreq.Status = REJECTED_ID
|
|
||||||
pkgreq.ClosureComment = rejected_closure_comment
|
|
||||||
pkgreq.Closer = request.user
|
|
||||||
|
|
||||||
all_requests = requests + reject_requests
|
|
||||||
for pkgreq in all_requests:
|
|
||||||
# Create notifications for request closure.
|
|
||||||
notif = notify.RequestCloseNotification(
|
|
||||||
request.user.ID, pkgreq.ID, pkgreq.status_display())
|
|
||||||
notifs.append(notif)
|
|
||||||
|
|
||||||
# Log this out for accountability purposes.
|
# Log this out for accountability purposes.
|
||||||
logger.info(f"Trusted User '{request.user.Username}' merged "
|
logger.info(f"Trusted User '{request.user.Username}' merged "
|
||||||
f"'{pkgbasename}' into '{target.Name}'.")
|
f"'{pkgbasename}' into '{target.Name}'.")
|
||||||
|
|
||||||
# Send our notifications array.
|
# Send notifications.
|
||||||
util.apply_all(notifs, lambda n: n.send())
|
util.apply_all(notifs, lambda n: n.send())
|
||||||
|
|
||||||
|
|
||||||
|
@ -1425,6 +1383,7 @@ def pkgbase_merge_instance(request: Request, pkgbase: models.PackageBase,
|
||||||
@auth_required()
|
@auth_required()
|
||||||
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()),
|
||||||
|
comments: str = Form(default=str()),
|
||||||
confirm: bool = Form(default=False),
|
confirm: bool = Form(default=False),
|
||||||
next: str = Form(default=str())):
|
next: str = Form(default=str())):
|
||||||
|
|
||||||
|
@ -1458,8 +1417,11 @@ async def pkgbase_merge_post(request: Request, name: str,
|
||||||
return render_template(request, "pkgbase/merge.html", context,
|
return render_template(request, "pkgbase/merge.html", context,
|
||||||
status_code=HTTPStatus.BAD_REQUEST)
|
status_code=HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
|
with db.begin():
|
||||||
|
update_closure_comment(pkgbase, MERGE_ID, comments, target=target)
|
||||||
|
|
||||||
# Merge pkgbase into target.
|
# Merge pkgbase into target.
|
||||||
pkgbase_merge_instance(request, pkgbase, target)
|
pkgbase_merge_instance(request, pkgbase, target, comments=comments)
|
||||||
|
|
||||||
# Run popupdate on the target.
|
# Run popupdate on the target.
|
||||||
popupdate.run_single(target)
|
popupdate.run_single(target)
|
||||||
|
|
|
@ -2300,3 +2300,7 @@ msgstr ""
|
||||||
#: aurweb/routers/accounts.py
|
#: aurweb/routers/accounts.py
|
||||||
msgid "You do not have permission to change this user's account type to %s."
|
msgid "You do not have permission to change this user's account type to %s."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: aurweb/packages/requests.py
|
||||||
|
msgid "No due existing orphan requests to accept for %s."
|
||||||
|
msgstr ""
|
||||||
|
|
|
@ -43,6 +43,12 @@
|
||||||
</label>
|
</label>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label for="id_comments">{{ "Comments" | tr }}:</label>
|
||||||
|
<textarea id="id_comments" name="comments"
|
||||||
|
rows="5" cols="50"></textarea>
|
||||||
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<input class="button"
|
<input class="button"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|
|
@ -36,6 +36,12 @@
|
||||||
|
|
||||||
<form action="/pkgbase/{{ pkgbase.Name }}/disown" method="post">
|
<form action="/pkgbase/{{ pkgbase.Name }}/disown" method="post">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
|
<p>
|
||||||
|
<label for="id_comments">{{ "Comments" | tr }}:</label>
|
||||||
|
<textarea id="id_comments" name="comments"
|
||||||
|
rows="5" cols="50"></textarea>
|
||||||
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<label class="confirmation">
|
<label class="confirmation">
|
||||||
<input type="checkbox" name="confirm" value="1" />
|
<input type="checkbox" name="confirm" value="1" />
|
||||||
|
|
|
@ -49,6 +49,12 @@
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label for="id_comments">{{ "Comments" | tr }}:</label>
|
||||||
|
<textarea id="id_comments" name="comments"
|
||||||
|
rows="5" cols="50"></textarea>
|
||||||
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<label class="confirmation">
|
<label class="confirmation">
|
||||||
<input type="checkbox" name="confirm" />
|
<input type="checkbox" name="confirm" />
|
||||||
|
|
|
@ -76,7 +76,6 @@
|
||||||
{% set action = "merge" %}
|
{% set action = "merge" %}
|
||||||
{# Add the 'via' url query parameter. #}
|
{# Add the 'via' url query parameter. #}
|
||||||
{% set temp_q = temp_q | extend_query(
|
{% set temp_q = temp_q | extend_query(
|
||||||
["via", result.ID],
|
|
||||||
["into", result.MergeBaseName]
|
["into", result.MergeBaseName]
|
||||||
) %}
|
) %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -23,24 +23,6 @@
|
||||||
|
|
||||||
<form action="/requests/{{ pkgreq.ID }}/close" method="post">
|
<form action="/requests/{{ pkgreq.ID }}/close" method="post">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<p>
|
|
||||||
<label for="id_reason">{{ "Reason" | tr }}:</label>
|
|
||||||
<select id="id_reason" name="reason">
|
|
||||||
{# Value 2 == "Accepted" status.
|
|
||||||
See aurweb.models.package_request. #}
|
|
||||||
{% if request.user.is_elevated() %}
|
|
||||||
<option value="2">
|
|
||||||
{{ "Accepted" | tr }}
|
|
||||||
</option>
|
|
||||||
{% endif %}
|
|
||||||
{# Value 3 == "Rejected" status.
|
|
||||||
See aurweb.models.package_request. #}
|
|
||||||
<option value="3">
|
|
||||||
{{ "Rejected" | tr }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<label for="id_comments">{{ "Comments" | tr }}:</label>
|
<label for="id_comments">{{ "Comments" | tr }}:</label>
|
||||||
<textarea id="id_comments" name="comments"
|
<textarea id="id_comments" name="comments"
|
||||||
|
|
|
@ -10,7 +10,7 @@ import pytest
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
from sqlalchemy import and_
|
from sqlalchemy import and_
|
||||||
|
|
||||||
from aurweb import asgi, config, db, defaults
|
from aurweb import asgi, db, defaults
|
||||||
from aurweb.models import License, PackageLicense
|
from aurweb.models import License, PackageLicense
|
||||||
from aurweb.models.account_type import USER_ID, AccountType
|
from aurweb.models.account_type import USER_ID, AccountType
|
||||||
from aurweb.models.dependency_type import DependencyType
|
from aurweb.models.dependency_type import DependencyType
|
||||||
|
@ -28,6 +28,7 @@ from aurweb.models.package_vote import PackageVote
|
||||||
from aurweb.models.relation_type import CONFLICTS_ID, PROVIDES_ID, REPLACES_ID, RelationType
|
from aurweb.models.relation_type import CONFLICTS_ID, PROVIDES_ID, REPLACES_ID, RelationType
|
||||||
from aurweb.models.request_type import DELETION_ID, MERGE_ID, RequestType
|
from aurweb.models.request_type import DELETION_ID, MERGE_ID, RequestType
|
||||||
from aurweb.models.user import User
|
from aurweb.models.user import User
|
||||||
|
from aurweb.testing.email import Email
|
||||||
from aurweb.testing.html import get_errors, get_successes, parse_root
|
from aurweb.testing.html import get_errors, get_successes, parse_root
|
||||||
from aurweb.testing.requests import Request
|
from aurweb.testing.requests import Request
|
||||||
|
|
||||||
|
@ -126,6 +127,39 @@ def package(maintainer: User) -> Package:
|
||||||
yield package
|
yield package
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def pkgbase(package: Package) -> PackageBase:
|
||||||
|
yield package.PackageBase
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def target(maintainer: User) -> PackageBase:
|
||||||
|
""" Merge target. """
|
||||||
|
now = int(datetime.utcnow().timestamp())
|
||||||
|
with db.begin():
|
||||||
|
pkgbase = db.create(PackageBase, Name="target-package",
|
||||||
|
Maintainer=maintainer,
|
||||||
|
Packager=maintainer,
|
||||||
|
Submitter=maintainer,
|
||||||
|
ModifiedTS=now)
|
||||||
|
db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name)
|
||||||
|
yield pkgbase
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def pkgreq(user: User, pkgbase: PackageBase) -> PackageRequest:
|
||||||
|
""" Yield a PackageRequest related to `pkgbase`. """
|
||||||
|
with db.begin():
|
||||||
|
pkgreq = db.create(PackageRequest,
|
||||||
|
ReqTypeID=DELETION_ID,
|
||||||
|
User=user,
|
||||||
|
PackageBase=pkgbase,
|
||||||
|
PackageBaseName=pkgbase.Name,
|
||||||
|
Comments=f"Deletion request for {pkgbase.Name}",
|
||||||
|
ClosureComment=str())
|
||||||
|
yield pkgreq
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def comment(user: User, package: Package) -> PackageComment:
|
def comment(user: User, package: Package) -> PackageComment:
|
||||||
pkgbase = package.PackageBase
|
pkgbase = package.PackageBase
|
||||||
|
@ -296,15 +330,7 @@ def test_package_comments(client: TestClient, user: User, package: Package):
|
||||||
|
|
||||||
|
|
||||||
def test_package_requests_display(client: TestClient, user: User,
|
def test_package_requests_display(client: TestClient, user: User,
|
||||||
package: Package):
|
package: Package, pkgreq: PackageRequest):
|
||||||
type_ = db.query(RequestType, RequestType.ID == DELETION_ID).first()
|
|
||||||
with db.begin():
|
|
||||||
db.create(PackageRequest, PackageBase=package.PackageBase,
|
|
||||||
PackageBaseName=package.PackageBase.Name,
|
|
||||||
User=user, RequestType=type_,
|
|
||||||
Comments="Test comment.",
|
|
||||||
ClosureComment=str())
|
|
||||||
|
|
||||||
# Test that a single request displays "1 pending request".
|
# Test that a single request displays "1 pending request".
|
||||||
with client as request:
|
with client as request:
|
||||||
resp = request.get(package_endpoint(package))
|
resp = request.get(package_endpoint(package))
|
||||||
|
@ -1516,115 +1542,6 @@ def test_pkgbase_request(client: TestClient, user: User, package: Package):
|
||||||
assert resp.status_code == int(HTTPStatus.OK)
|
assert resp.status_code == int(HTTPStatus.OK)
|
||||||
|
|
||||||
|
|
||||||
def test_pkgbase_request_post_deletion(client: TestClient, user: User,
|
|
||||||
package: Package):
|
|
||||||
endpoint = f"/pkgbase/{package.PackageBase.Name}/request"
|
|
||||||
cookies = {"AURSID": user.login(Request(), "testPassword")}
|
|
||||||
with client as request:
|
|
||||||
resp = request.post(endpoint, data={
|
|
||||||
"type": "deletion",
|
|
||||||
"comments": "We want to delete this."
|
|
||||||
}, cookies=cookies, allow_redirects=False)
|
|
||||||
assert resp.status_code == int(HTTPStatus.SEE_OTHER)
|
|
||||||
|
|
||||||
pkgreq = db.query(PackageRequest).filter(
|
|
||||||
PackageRequest.PackageBaseID == package.PackageBase.ID
|
|
||||||
).first()
|
|
||||||
assert pkgreq is not None
|
|
||||||
assert pkgreq.RequestType.Name == "deletion"
|
|
||||||
assert pkgreq.PackageBaseName == package.PackageBase.Name
|
|
||||||
assert pkgreq.Comments == "We want to delete this."
|
|
||||||
|
|
||||||
|
|
||||||
def test_pkgbase_request_post_maintainer_deletion(
|
|
||||||
client: TestClient, maintainer: User, package: Package):
|
|
||||||
pkgbasename = package.PackageBase.Name
|
|
||||||
endpoint = f"/pkgbase/{package.PackageBase.Name}/request"
|
|
||||||
cookies = {"AURSID": maintainer.login(Request(), "testPassword")}
|
|
||||||
with client as request:
|
|
||||||
resp = request.post(endpoint, data={
|
|
||||||
"type": "deletion",
|
|
||||||
"comments": "We want to delete this."
|
|
||||||
}, cookies=cookies, allow_redirects=False)
|
|
||||||
assert resp.status_code == int(HTTPStatus.SEE_OTHER)
|
|
||||||
|
|
||||||
pkgreq = db.query(PackageRequest).filter(
|
|
||||||
PackageRequest.PackageBaseName == pkgbasename
|
|
||||||
).first()
|
|
||||||
assert pkgreq.Status == ACCEPTED_ID
|
|
||||||
|
|
||||||
|
|
||||||
def test_pkgbase_request_post_orphan(client: TestClient, user: User,
|
|
||||||
package: Package):
|
|
||||||
endpoint = f"/pkgbase/{package.PackageBase.Name}/request"
|
|
||||||
cookies = {"AURSID": user.login(Request(), "testPassword")}
|
|
||||||
with client as request:
|
|
||||||
resp = request.post(endpoint, data={
|
|
||||||
"type": "orphan",
|
|
||||||
"comments": "We want to disown this."
|
|
||||||
}, cookies=cookies, allow_redirects=False)
|
|
||||||
assert resp.status_code == int(HTTPStatus.SEE_OTHER)
|
|
||||||
|
|
||||||
pkgreq = db.query(PackageRequest).filter(
|
|
||||||
PackageRequest.PackageBaseID == package.PackageBase.ID
|
|
||||||
).first()
|
|
||||||
assert pkgreq is not None
|
|
||||||
assert pkgreq.RequestType.Name == "orphan"
|
|
||||||
assert pkgreq.PackageBaseName == package.PackageBase.Name
|
|
||||||
assert pkgreq.Comments == "We want to disown this."
|
|
||||||
|
|
||||||
|
|
||||||
def test_pkgbase_request_post_auto_orphan(client: TestClient, user: User,
|
|
||||||
package: Package):
|
|
||||||
now = int(datetime.utcnow().timestamp())
|
|
||||||
auto_orphan_age = config.getint("options", "auto_orphan_age")
|
|
||||||
with db.begin():
|
|
||||||
package.PackageBase.OutOfDateTS = now - auto_orphan_age - 1
|
|
||||||
|
|
||||||
endpoint = f"/pkgbase/{package.PackageBase.Name}/request"
|
|
||||||
cookies = {"AURSID": user.login(Request(), "testPassword")}
|
|
||||||
with client as request:
|
|
||||||
resp = request.post(endpoint, data={
|
|
||||||
"type": "orphan",
|
|
||||||
"comments": "We want to disown this."
|
|
||||||
}, cookies=cookies, allow_redirects=False)
|
|
||||||
assert resp.status_code == int(HTTPStatus.SEE_OTHER)
|
|
||||||
|
|
||||||
pkgreq = db.query(PackageRequest).filter(
|
|
||||||
PackageRequest.PackageBaseID == package.PackageBase.ID
|
|
||||||
).first()
|
|
||||||
assert pkgreq is not None
|
|
||||||
assert pkgreq.Status == ACCEPTED_ID
|
|
||||||
|
|
||||||
|
|
||||||
def test_pkgbase_request_post_merge(client: TestClient, user: User,
|
|
||||||
package: Package):
|
|
||||||
with db.begin():
|
|
||||||
pkgbase2 = db.create(PackageBase, Name="new-pkgbase",
|
|
||||||
Submitter=user, Maintainer=user, Packager=user)
|
|
||||||
target = db.create(Package, PackageBase=pkgbase2,
|
|
||||||
Name=pkgbase2.Name, Version="1.0.0")
|
|
||||||
|
|
||||||
endpoint = f"/pkgbase/{package.PackageBase.Name}/request"
|
|
||||||
cookies = {"AURSID": user.login(Request(), "testPassword")}
|
|
||||||
with client as request:
|
|
||||||
resp = request.post(endpoint, data={
|
|
||||||
"type": "merge",
|
|
||||||
"merge_into": target.PackageBase.Name,
|
|
||||||
"comments": "We want to merge this."
|
|
||||||
}, cookies=cookies, allow_redirects=False)
|
|
||||||
assert resp.status_code == int(HTTPStatus.SEE_OTHER)
|
|
||||||
|
|
||||||
pkgreq = db.query(PackageRequest).filter(
|
|
||||||
PackageRequest.PackageBaseID == package.PackageBase.ID
|
|
||||||
).first()
|
|
||||||
assert pkgreq is not None
|
|
||||||
assert pkgreq.RequestType.Name == "merge"
|
|
||||||
assert pkgreq.PackageBaseName == package.PackageBase.Name
|
|
||||||
assert pkgreq.MergeBaseName == target.PackageBase.Name
|
|
||||||
assert pkgreq.Comments == "We want to merge this."
|
|
||||||
|
|
||||||
|
|
||||||
def test_pkgbase_request_post_not_found(client: TestClient, user: User):
|
def test_pkgbase_request_post_not_found(client: TestClient, user: User):
|
||||||
cookies = {"AURSID": user.login(Request(), "testPassword")}
|
cookies = {"AURSID": user.login(Request(), "testPassword")}
|
||||||
with client as request:
|
with client as request:
|
||||||
|
@ -1718,22 +1635,6 @@ def test_pkgbase_request_post_merge_self_error(client: TestClient, user: User,
|
||||||
assert error.text.strip() == expected
|
assert error.text.strip() == expected
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def pkgreq(user: User, package: Package) -> PackageRequest:
|
|
||||||
reqtype = db.query(RequestType).filter(
|
|
||||||
RequestType.ID == DELETION_ID
|
|
||||||
).first()
|
|
||||||
with db.begin():
|
|
||||||
pkgreq = db.create(PackageRequest,
|
|
||||||
RequestType=reqtype,
|
|
||||||
User=user,
|
|
||||||
PackageBase=package.PackageBase,
|
|
||||||
PackageBaseName=package.PackageBase.Name,
|
|
||||||
Comments=str(),
|
|
||||||
ClosureComment=str())
|
|
||||||
yield pkgreq
|
|
||||||
|
|
||||||
|
|
||||||
def test_requests_close(client: TestClient, user: User,
|
def test_requests_close(client: TestClient, user: User,
|
||||||
pkgreq: PackageRequest):
|
pkgreq: PackageRequest):
|
||||||
cookies = {"AURSID": user.login(Request(), "testPassword")}
|
cookies = {"AURSID": user.login(Request(), "testPassword")}
|
||||||
|
@ -1753,16 +1654,6 @@ def test_requests_close_unauthorized(client: TestClient, maintainer: User,
|
||||||
assert resp.headers.get("location") == "/"
|
assert resp.headers.get("location") == "/"
|
||||||
|
|
||||||
|
|
||||||
def test_requests_close_post_invalid_reason(client: TestClient, user: User,
|
|
||||||
pkgreq: PackageRequest):
|
|
||||||
cookies = {"AURSID": user.login(Request(), "testPassword")}
|
|
||||||
with client as request:
|
|
||||||
resp = request.post(f"/requests/{pkgreq.ID}/close", data={
|
|
||||||
"reason": 0
|
|
||||||
}, cookies=cookies, allow_redirects=False)
|
|
||||||
assert resp.status_code == int(HTTPStatus.BAD_REQUEST)
|
|
||||||
|
|
||||||
|
|
||||||
def test_requests_close_post_unauthorized(client: TestClient, maintainer: User,
|
def test_requests_close_post_unauthorized(client: TestClient, maintainer: User,
|
||||||
pkgreq: PackageRequest):
|
pkgreq: PackageRequest):
|
||||||
cookies = {"AURSID": maintainer.login(Request(), "testPassword")}
|
cookies = {"AURSID": maintainer.login(Request(), "testPassword")}
|
||||||
|
@ -1778,9 +1669,8 @@ def test_requests_close_post(client: TestClient, user: User,
|
||||||
pkgreq: PackageRequest):
|
pkgreq: PackageRequest):
|
||||||
cookies = {"AURSID": user.login(Request(), "testPassword")}
|
cookies = {"AURSID": user.login(Request(), "testPassword")}
|
||||||
with client as request:
|
with client as request:
|
||||||
resp = request.post(f"/requests/{pkgreq.ID}/close", data={
|
resp = request.post(f"/requests/{pkgreq.ID}/close",
|
||||||
"reason": REJECTED_ID
|
cookies=cookies, allow_redirects=False)
|
||||||
}, cookies=cookies, allow_redirects=False)
|
|
||||||
assert resp.status_code == int(HTTPStatus.SEE_OTHER)
|
assert resp.status_code == int(HTTPStatus.SEE_OTHER)
|
||||||
|
|
||||||
assert pkgreq.Status == REJECTED_ID
|
assert pkgreq.Status == REJECTED_ID
|
||||||
|
@ -1792,9 +1682,8 @@ def test_requests_close_post_rejected(client: TestClient, user: User,
|
||||||
pkgreq: PackageRequest):
|
pkgreq: PackageRequest):
|
||||||
cookies = {"AURSID": user.login(Request(), "testPassword")}
|
cookies = {"AURSID": user.login(Request(), "testPassword")}
|
||||||
with client as request:
|
with client as request:
|
||||||
resp = request.post(f"/requests/{pkgreq.ID}/close", data={
|
resp = request.post(f"/requests/{pkgreq.ID}/close",
|
||||||
"reason": REJECTED_ID
|
cookies=cookies, allow_redirects=False)
|
||||||
}, cookies=cookies, allow_redirects=False)
|
|
||||||
assert resp.status_code == int(HTTPStatus.SEE_OTHER)
|
assert resp.status_code == int(HTTPStatus.SEE_OTHER)
|
||||||
|
|
||||||
assert pkgreq.Status == REJECTED_ID
|
assert pkgreq.Status == REJECTED_ID
|
||||||
|
@ -1971,18 +1860,6 @@ def test_pkgbase_vote(client: TestClient, user: User, package: Package):
|
||||||
assert pkgbase.NumVotes == 0
|
assert pkgbase.NumVotes == 0
|
||||||
|
|
||||||
|
|
||||||
def test_pkgbase_disown_as_tu(client: TestClient, tu_user: User,
|
|
||||||
package: Package):
|
|
||||||
cookies = {"AURSID": tu_user.login(Request(), "testPassword")}
|
|
||||||
pkgbase = package.PackageBase
|
|
||||||
endpoint = f"/pkgbase/{pkgbase.Name}/disown"
|
|
||||||
|
|
||||||
# But we do here.
|
|
||||||
with client as request:
|
|
||||||
resp = request.post(endpoint, data={"confirm": True}, cookies=cookies)
|
|
||||||
assert resp.status_code == int(HTTPStatus.SEE_OTHER)
|
|
||||||
|
|
||||||
|
|
||||||
def test_pkgbase_disown_as_sole_maintainer(client: TestClient,
|
def test_pkgbase_disown_as_sole_maintainer(client: TestClient,
|
||||||
maintainer: User,
|
maintainer: User,
|
||||||
package: Package):
|
package: Package):
|
||||||
|
@ -2114,6 +1991,38 @@ def test_pkgbase_delete(client: TestClient, tu_user: User, package: Package):
|
||||||
).first()
|
).first()
|
||||||
assert record is None
|
assert record is None
|
||||||
|
|
||||||
|
# Two emails should've been sent out; an autogenerated
|
||||||
|
# request's accepted notification and a deletion notification.
|
||||||
|
assert Email.count() == 1
|
||||||
|
|
||||||
|
req_close = Email(1).parse()
|
||||||
|
expr = r"^\[PRQ#\d+\] Deletion Request for [^ ]+ Accepted$"
|
||||||
|
subject = req_close.headers.get("Subject")
|
||||||
|
assert re.match(expr, subject)
|
||||||
|
|
||||||
|
|
||||||
|
def test_pkgbase_delete_with_request(client: TestClient, tu_user: User,
|
||||||
|
pkgbase: PackageBase,
|
||||||
|
pkgreq: PackageRequest):
|
||||||
|
# TODO: Test that a previously existing request gets Accepted when
|
||||||
|
# a TU deleted the package.
|
||||||
|
|
||||||
|
# Delete the package as `tu_user` via POST request.
|
||||||
|
cookies = {"AURSID": tu_user.login(Request(), "testPassword")}
|
||||||
|
endpoint = f"/pkgbase/{pkgbase.Name}/delete"
|
||||||
|
with client as request:
|
||||||
|
resp = request.post(endpoint, data={"confirm": True}, cookies=cookies)
|
||||||
|
assert resp.status_code == int(HTTPStatus.SEE_OTHER)
|
||||||
|
assert resp.headers.get("location") == "/packages"
|
||||||
|
|
||||||
|
# We should've just sent one closure email since `pkgreq` exists.
|
||||||
|
assert Email.count() == 1
|
||||||
|
|
||||||
|
# Make sure it was a closure for the deletion request.
|
||||||
|
email = Email(1).parse()
|
||||||
|
expr = r"^\[PRQ#\d+\] Deletion Request for [^ ]+ Accepted$"
|
||||||
|
assert re.match(expr, email.headers.get("Subject"))
|
||||||
|
|
||||||
|
|
||||||
def test_packages_post_unknown_action(client: TestClient, user: User,
|
def test_packages_post_unknown_action(client: TestClient, user: User,
|
||||||
package: Package):
|
package: Package):
|
||||||
|
@ -2371,9 +2280,11 @@ def test_packages_post_adopt(client: TestClient, user: User,
|
||||||
assert successes[0].text.strip() == expected
|
assert successes[0].text.strip() == expected
|
||||||
|
|
||||||
|
|
||||||
def test_packages_post_disown(client: TestClient, user: User,
|
def test_packages_post_disown_as_maintainer(client: TestClient, user: User,
|
||||||
maintainer: User, package: Package):
|
maintainer: User,
|
||||||
# Initially prove that we have a maintainer: `maintainer`.
|
package: Package):
|
||||||
|
""" Disown packages as a maintainer. """
|
||||||
|
# Initially prove that we have a maintainer.
|
||||||
assert package.PackageBase.Maintainer is not None
|
assert package.PackageBase.Maintainer is not None
|
||||||
assert package.PackageBase.Maintainer == maintainer
|
assert package.PackageBase.Maintainer == maintainer
|
||||||
|
|
||||||
|
@ -2430,9 +2341,24 @@ def test_packages_post_disown(client: TestClient, user: User,
|
||||||
assert successes[0].text.strip() == expected
|
assert successes[0].text.strip() == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_packages_post_disown(client: TestClient, tu_user: User,
|
||||||
|
maintainer: User, package: Package):
|
||||||
|
""" Disown packages as a Trusted User, which cannot bypass idle time. """
|
||||||
|
cookies = {"AURSID": tu_user.login(Request(), "testPassword")}
|
||||||
|
with client as request:
|
||||||
|
resp = request.post("/packages", data={
|
||||||
|
"action": "disown",
|
||||||
|
"IDs": [package.ID],
|
||||||
|
"confirm": True
|
||||||
|
}, cookies=cookies)
|
||||||
|
|
||||||
|
errors = get_errors(resp.text)
|
||||||
|
expected = r"^No due existing orphan requests to accept for .+\.$"
|
||||||
|
assert re.match(expected, errors[0].text.strip())
|
||||||
|
|
||||||
|
|
||||||
def test_packages_post_delete(caplog: pytest.fixture, client: TestClient,
|
def test_packages_post_delete(caplog: pytest.fixture, client: TestClient,
|
||||||
user: User, tu_user: User, package: Package):
|
user: User, tu_user: User, package: Package):
|
||||||
|
|
||||||
# First, let's try to use the delete action with no packages IDs.
|
# First, let's try to use the delete action with no packages IDs.
|
||||||
user_cookies = {"AURSID": user.login(Request(), "testPassword")}
|
user_cookies = {"AURSID": user.login(Request(), "testPassword")}
|
||||||
with client as request:
|
with client as request:
|
||||||
|
@ -2556,23 +2482,19 @@ def test_pkgbase_merge_post_self_invalid(client: TestClient, tu_user: User,
|
||||||
|
|
||||||
|
|
||||||
def test_pkgbase_merge_post(client: TestClient, tu_user: User,
|
def test_pkgbase_merge_post(client: TestClient, tu_user: User,
|
||||||
packages: List[Package]):
|
package: Package,
|
||||||
package, target = packages[:2]
|
pkgbase: PackageBase,
|
||||||
|
target: PackageBase,
|
||||||
|
pkgreq: PackageRequest):
|
||||||
pkgname = package.Name
|
pkgname = package.Name
|
||||||
pkgbasename = package.PackageBase.Name
|
pkgbasename = pkgbase.Name
|
||||||
|
|
||||||
# Create a merge request destined for another target.
|
# Create a merge request destined for another target.
|
||||||
# This will allow our test code to exercise closing
|
# This will allow our test code to exercise closing
|
||||||
# such a request after merging the pkgbase in question.
|
# such a request after merging the pkgbase in question.
|
||||||
with db.begin():
|
with db.begin():
|
||||||
pkgreq = db.create(PackageRequest,
|
pkgreq.ReqTypeID = MERGE_ID
|
||||||
User=tu_user,
|
pkgreq.MergeBaseName = target.Name
|
||||||
ReqTypeID=MERGE_ID,
|
|
||||||
PackageBase=package.PackageBase,
|
|
||||||
PackageBaseName=pkgbasename,
|
|
||||||
MergeBaseName="test",
|
|
||||||
Comments="Test comment.",
|
|
||||||
ClosureComment="Test closure.")
|
|
||||||
|
|
||||||
# Vote for the package.
|
# Vote for the package.
|
||||||
cookies = {"AURSID": tu_user.login(Request(), "testPassword")}
|
cookies = {"AURSID": tu_user.login(Request(), "testPassword")}
|
||||||
|
@ -2604,32 +2526,37 @@ def test_pkgbase_merge_post(client: TestClient, tu_user: User,
|
||||||
endpoint = f"/pkgbase/{package.PackageBase.Name}/merge"
|
endpoint = f"/pkgbase/{package.PackageBase.Name}/merge"
|
||||||
with client as request:
|
with client as request:
|
||||||
resp = request.post(endpoint, data={
|
resp = request.post(endpoint, data={
|
||||||
"into": target.PackageBase.Name,
|
"into": target.Name,
|
||||||
"confirm": True
|
"confirm": True
|
||||||
}, cookies=cookies)
|
}, cookies=cookies)
|
||||||
assert resp.status_code == int(HTTPStatus.SEE_OTHER)
|
assert resp.status_code == int(HTTPStatus.SEE_OTHER)
|
||||||
loc = resp.headers.get("location")
|
loc = resp.headers.get("location")
|
||||||
assert loc == f"/pkgbase/{target.PackageBase.Name}"
|
assert loc == f"/pkgbase/{target.Name}"
|
||||||
|
|
||||||
|
# Two emails should've been sent out.
|
||||||
|
assert Email.count() == 1
|
||||||
|
email_body = Email(1).parse().glue()
|
||||||
|
assert f"Merge Request for {pkgbasename} Accepted" in email_body
|
||||||
|
|
||||||
# Assert that the original comments, notifs and votes we setup
|
# Assert that the original comments, notifs and votes we setup
|
||||||
# got migrated to target as intended.
|
# got migrated to target as intended.
|
||||||
assert comments == target.PackageBase.comments.all()
|
assert comments == target.comments.all()
|
||||||
assert notifs == target.PackageBase.notifications.all()
|
assert notifs == target.notifications.all()
|
||||||
assert votes == target.PackageBase.package_votes.all()
|
assert votes == target.package_votes.all()
|
||||||
|
|
||||||
# ...and that the package got deleted.
|
# ...and that the package got deleted.
|
||||||
package = db.query(Package).filter(Package.Name == pkgname).first()
|
package = db.query(Package).filter(Package.Name == pkgname).first()
|
||||||
assert package is None
|
assert package is None
|
||||||
|
|
||||||
# Our fake target request should have gotten rejected.
|
# Our previously-made request should have gotten accepted.
|
||||||
assert pkgreq.Status == REJECTED_ID
|
assert pkgreq.Status == ACCEPTED_ID
|
||||||
assert pkgreq.Closer is not None
|
assert pkgreq.Closer is not None
|
||||||
|
|
||||||
# A PackageRequest is always created when merging this way.
|
# A PackageRequest is always created when merging this way.
|
||||||
pkgreq = db.query(PackageRequest).filter(
|
pkgreq = db.query(PackageRequest).filter(
|
||||||
and_(PackageRequest.ReqTypeID == MERGE_ID,
|
and_(PackageRequest.ReqTypeID == MERGE_ID,
|
||||||
PackageRequest.PackageBaseName == pkgbasename,
|
PackageRequest.PackageBaseName == pkgbasename,
|
||||||
PackageRequest.MergeBaseName == target.PackageBase.Name)
|
PackageRequest.MergeBaseName == target.Name)
|
||||||
).first()
|
).first()
|
||||||
assert pkgreq is not None
|
assert pkgreq is not None
|
||||||
|
|
||||||
|
|
558
test/test_requests.py
Normal file
558
test/test_requests.py
Normal file
|
@ -0,0 +1,558 @@
|
||||||
|
import re
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from http import HTTPStatus
|
||||||
|
from logging import DEBUG
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from aurweb import asgi, config, db
|
||||||
|
from aurweb.models import Package, PackageBase, PackageRequest, User
|
||||||
|
from aurweb.models.account_type import TRUSTED_USER_ID, USER_ID
|
||||||
|
from aurweb.models.package_notification import PackageNotification
|
||||||
|
from aurweb.models.package_request import ACCEPTED_ID, PENDING_ID, REJECTED_ID
|
||||||
|
from aurweb.models.request_type import DELETION_ID, MERGE_ID, ORPHAN_ID
|
||||||
|
from aurweb.packages.requests import ClosureFactory
|
||||||
|
from aurweb.testing.email import Email
|
||||||
|
from aurweb.testing.html import get_errors
|
||||||
|
from aurweb.testing.requests import Request
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def setup(db_test) -> None:
|
||||||
|
""" Setup the database. """
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client() -> TestClient:
|
||||||
|
""" Yield a TestClient. """
|
||||||
|
yield TestClient(app=asgi.app)
|
||||||
|
|
||||||
|
|
||||||
|
def create_user(username: str, email: str) -> User:
|
||||||
|
"""
|
||||||
|
Create a user based on `username` and `email`.
|
||||||
|
|
||||||
|
:param username: User.Username
|
||||||
|
:param email: User.Email
|
||||||
|
:return: User instance
|
||||||
|
"""
|
||||||
|
with db.begin():
|
||||||
|
user = db.create(User, Username=username, Email=email,
|
||||||
|
Passwd="testPassword", AccountTypeID=USER_ID)
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def user() -> User:
|
||||||
|
""" Yield a User instance. """
|
||||||
|
user = create_user("test", "test@example.org")
|
||||||
|
yield user
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def auser(user: User) -> User:
|
||||||
|
""" Yield an authenticated User instance. """
|
||||||
|
cookies = {"AURSID": user.login(Request(), "testPassword")}
|
||||||
|
user.cookies = cookies
|
||||||
|
yield user
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def user2() -> User:
|
||||||
|
""" Yield a secondary non-maintainer User instance. """
|
||||||
|
user = create_user("test2", "test2@example.org")
|
||||||
|
yield user
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def auser2(user2: User) -> User:
|
||||||
|
""" Yield an authenticated secondary non-maintainer User instance. """
|
||||||
|
cookies = {"AURSID": user2.login(Request(), "testPassword")}
|
||||||
|
user2.cookies = cookies
|
||||||
|
yield user2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def tu_user() -> User:
|
||||||
|
""" Yield an authenticated Trusted User instance. """
|
||||||
|
user = create_user("test_tu", "test_tu@example.org")
|
||||||
|
with db.begin():
|
||||||
|
user.AccountTypeID = TRUSTED_USER_ID
|
||||||
|
cookies = {"AURSID": user.login(Request(), "testPassword")}
|
||||||
|
user.cookies = cookies
|
||||||
|
yield user
|
||||||
|
|
||||||
|
|
||||||
|
def create_pkgbase(user: User, name: str) -> PackageBase:
|
||||||
|
"""
|
||||||
|
Create a package base based on `user` and `name`.
|
||||||
|
|
||||||
|
This function also creates a matching Package record.
|
||||||
|
|
||||||
|
:param user: User instance
|
||||||
|
:param name: PackageBase.Name
|
||||||
|
:return: PackageBase instance
|
||||||
|
"""
|
||||||
|
now = int(datetime.utcnow().timestamp())
|
||||||
|
with db.begin():
|
||||||
|
pkgbase = db.create(PackageBase, Name=name,
|
||||||
|
Maintainer=user, Packager=user,
|
||||||
|
SubmittedTS=now, ModifiedTS=now)
|
||||||
|
db.create(Package, Name=pkgbase.Name, PackageBase=pkgbase)
|
||||||
|
return pkgbase
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def pkgbase(user: User) -> PackageBase:
|
||||||
|
""" Yield a package base. """
|
||||||
|
pkgbase = create_pkgbase(user, "test-package")
|
||||||
|
yield pkgbase
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def target(user: User) -> PackageBase:
|
||||||
|
""" Yield a merge target (package base). """
|
||||||
|
with db.begin():
|
||||||
|
target = db.create(PackageBase, Name="target-package",
|
||||||
|
Maintainer=user, Packager=user)
|
||||||
|
yield target
|
||||||
|
|
||||||
|
|
||||||
|
def create_request(reqtype_id: int, user: User, pkgbase: PackageBase,
|
||||||
|
comments: str) -> PackageRequest:
|
||||||
|
"""
|
||||||
|
Create a package request based on `reqtype_id`, `user`,
|
||||||
|
`pkgbase` and `comments`.
|
||||||
|
|
||||||
|
:param reqtype_id: RequestType.ID
|
||||||
|
:param user: User instance
|
||||||
|
:param pkgbase: PackageBase instance
|
||||||
|
:param comments: PackageRequest.Comments
|
||||||
|
:return: PackageRequest instance
|
||||||
|
"""
|
||||||
|
now = int(datetime.utcnow().timestamp())
|
||||||
|
with db.begin():
|
||||||
|
pkgreq = db.create(PackageRequest, ReqTypeID=reqtype_id,
|
||||||
|
User=user, PackageBase=pkgbase,
|
||||||
|
PackageBaseName=pkgbase.Name,
|
||||||
|
RequestTS=now,
|
||||||
|
Comments=comments,
|
||||||
|
ClosureComment=str())
|
||||||
|
return pkgreq
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def pkgreq(user: User, pkgbase: PackageBase):
|
||||||
|
""" Yield a package request. """
|
||||||
|
pkgreq = create_request(DELETION_ID, user, pkgbase, "Test request.")
|
||||||
|
yield pkgreq
|
||||||
|
|
||||||
|
|
||||||
|
def create_notification(user: User, pkgbase: PackageBase):
|
||||||
|
""" Create a notification for a `user` on `pkgbase`. """
|
||||||
|
with db.begin():
|
||||||
|
notif = db.create(PackageNotification, User=user, PackageBase=pkgbase)
|
||||||
|
return notif
|
||||||
|
|
||||||
|
|
||||||
|
def test_request(client: TestClient, auser: User, pkgbase: PackageBase):
|
||||||
|
""" Test the standard pkgbase request route GET method. """
|
||||||
|
endpoint = f"/pkgbase/{pkgbase.Name}/request"
|
||||||
|
with client as request:
|
||||||
|
resp = request.get(endpoint, cookies=auser.cookies)
|
||||||
|
assert resp.status_code == int(HTTPStatus.OK)
|
||||||
|
|
||||||
|
|
||||||
|
def test_request_post_deletion(client: TestClient, auser2: User,
|
||||||
|
pkgbase: PackageBase):
|
||||||
|
""" Test the POST route for creating a deletion request works. """
|
||||||
|
endpoint = f"/pkgbase/{pkgbase.Name}/request"
|
||||||
|
data = {"comments": "Test request.", "type": "deletion"}
|
||||||
|
with client as request:
|
||||||
|
resp = request.post(endpoint, data=data, cookies=auser2.cookies)
|
||||||
|
assert resp.status_code == int(HTTPStatus.SEE_OTHER)
|
||||||
|
|
||||||
|
pkgreq = pkgbase.requests.first()
|
||||||
|
assert pkgreq is not None
|
||||||
|
assert pkgreq.ReqTypeID == DELETION_ID
|
||||||
|
assert pkgreq.Status == PENDING_ID
|
||||||
|
|
||||||
|
# A RequestOpenNotification should've been sent out.
|
||||||
|
assert Email.count() == 1
|
||||||
|
email = Email(1)
|
||||||
|
expr = r"^\[PRQ#%d\] Deletion Request for [^ ]+$" % pkgreq.ID
|
||||||
|
assert re.match(expr, email.headers.get("Subject"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_request_post_deletion_as_maintainer(client: TestClient, auser: User,
|
||||||
|
pkgbase: PackageBase):
|
||||||
|
""" Test the POST route for creating a deletion request as maint works. """
|
||||||
|
endpoint = f"/pkgbase/{pkgbase.Name}/request"
|
||||||
|
data = {"comments": "Test request.", "type": "deletion"}
|
||||||
|
with client as request:
|
||||||
|
resp = request.post(endpoint, data=data, cookies=auser.cookies)
|
||||||
|
assert resp.status_code == int(HTTPStatus.SEE_OTHER)
|
||||||
|
|
||||||
|
# Check the pkgreq record got created and accepted.
|
||||||
|
pkgreq = db.query(PackageRequest).first()
|
||||||
|
assert pkgreq is not None
|
||||||
|
assert pkgreq.ReqTypeID == DELETION_ID
|
||||||
|
assert pkgreq.Status == ACCEPTED_ID
|
||||||
|
|
||||||
|
# Should've gotten two emails.
|
||||||
|
assert Email.count() == 2
|
||||||
|
|
||||||
|
# A RequestOpenNotification should've been sent out.
|
||||||
|
email = Email(1)
|
||||||
|
expr = r"^\[PRQ#%d\] Deletion Request for [^ ]+$" % pkgreq.ID
|
||||||
|
assert re.match(expr, email.headers.get("Subject"))
|
||||||
|
|
||||||
|
# Check the content of the close notification.
|
||||||
|
email = Email(2)
|
||||||
|
expr = r"^\[PRQ#%d\] Deletion Request for [^ ]+ Accepted$" % pkgreq.ID
|
||||||
|
assert re.match(expr, email.headers.get("Subject"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_request_post_deletion_autoaccept(client: TestClient, auser: User,
|
||||||
|
pkgbase: PackageBase,
|
||||||
|
caplog: pytest.LogCaptureFixture):
|
||||||
|
""" Test the request route for deletion as maintainer. """
|
||||||
|
caplog.set_level(DEBUG)
|
||||||
|
|
||||||
|
now = int(datetime.utcnow().timestamp())
|
||||||
|
auto_delete_age = config.getint("options", "auto_delete_age")
|
||||||
|
with db.begin():
|
||||||
|
pkgbase.ModifiedTS = now - auto_delete_age + 100
|
||||||
|
|
||||||
|
endpoint = f"/pkgbase/{pkgbase.Name}/request"
|
||||||
|
data = {"comments": "Test request.", "type": "deletion"}
|
||||||
|
with client as request:
|
||||||
|
resp = request.post(endpoint, data=data, cookies=auser.cookies)
|
||||||
|
assert resp.status_code == int(HTTPStatus.SEE_OTHER)
|
||||||
|
|
||||||
|
pkgreq = db.query(PackageRequest).filter(
|
||||||
|
PackageRequest.PackageBaseName == pkgbase.Name
|
||||||
|
).first()
|
||||||
|
assert pkgreq is not None
|
||||||
|
assert pkgreq.ReqTypeID == DELETION_ID
|
||||||
|
assert pkgreq.Status == ACCEPTED_ID
|
||||||
|
|
||||||
|
# A RequestOpenNotification should've been sent out.
|
||||||
|
assert Email.count() == 2
|
||||||
|
Email.dump()
|
||||||
|
|
||||||
|
# Check the content of the open notification.
|
||||||
|
email = Email(1)
|
||||||
|
expr = r"^\[PRQ#%d\] Deletion Request for [^ ]+$" % pkgreq.ID
|
||||||
|
assert re.match(expr, email.headers.get("Subject"))
|
||||||
|
|
||||||
|
# Check the content of the close notification.
|
||||||
|
email = Email(2)
|
||||||
|
expr = r"^\[PRQ#%d\] Deletion Request for [^ ]+ Accepted$" % pkgreq.ID
|
||||||
|
assert re.match(expr, email.headers.get("Subject"))
|
||||||
|
|
||||||
|
# Check logs.
|
||||||
|
expr = r"New request #\d+ is marked for auto-deletion."
|
||||||
|
assert re.search(expr, caplog.text)
|
||||||
|
|
||||||
|
|
||||||
|
def test_request_post_merge(client: TestClient, auser: User,
|
||||||
|
pkgbase: PackageBase, target: PackageBase):
|
||||||
|
""" Test the request route for merge as maintainer. """
|
||||||
|
endpoint = f"/pkgbase/{pkgbase.Name}/request"
|
||||||
|
data = {
|
||||||
|
"type": "merge",
|
||||||
|
"merge_into": target.Name,
|
||||||
|
"comments": "Test request.",
|
||||||
|
}
|
||||||
|
with client as request:
|
||||||
|
resp = request.post(endpoint, data=data, cookies=auser.cookies)
|
||||||
|
assert resp.status_code == int(HTTPStatus.SEE_OTHER)
|
||||||
|
|
||||||
|
pkgreq = pkgbase.requests.first()
|
||||||
|
assert pkgreq is not None
|
||||||
|
assert pkgreq.ReqTypeID == MERGE_ID
|
||||||
|
assert pkgreq.Status == PENDING_ID
|
||||||
|
assert pkgreq.MergeBaseName == target.Name
|
||||||
|
|
||||||
|
# A RequestOpenNotification should've been sent out.
|
||||||
|
assert Email.count() == 1
|
||||||
|
email = Email(1)
|
||||||
|
expr = r"^\[PRQ#%d\] Merge Request for [^ ]+$" % pkgreq.ID
|
||||||
|
assert re.match(expr, email.headers.get("Subject"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_request_post_orphan(client: TestClient, auser: User,
|
||||||
|
pkgbase: PackageBase):
|
||||||
|
""" Test the POST route for creating an orphan request works. """
|
||||||
|
endpoint = f"/pkgbase/{pkgbase.Name}/request"
|
||||||
|
data = {
|
||||||
|
"type": "orphan",
|
||||||
|
"comments": "Test request.",
|
||||||
|
}
|
||||||
|
with client as request:
|
||||||
|
resp = request.post(endpoint, data=data, cookies=auser.cookies)
|
||||||
|
assert resp.status_code == int(HTTPStatus.SEE_OTHER)
|
||||||
|
|
||||||
|
pkgreq = pkgbase.requests.first()
|
||||||
|
assert pkgreq is not None
|
||||||
|
assert pkgreq.ReqTypeID == ORPHAN_ID
|
||||||
|
assert pkgreq.Status == PENDING_ID
|
||||||
|
|
||||||
|
# A RequestOpenNotification should've been sent out.
|
||||||
|
assert Email.count() == 1
|
||||||
|
|
||||||
|
email = Email(1)
|
||||||
|
expr = r"^\[PRQ#%d\] Orphan Request for [^ ]+$" % pkgreq.ID
|
||||||
|
assert re.match(expr, email.headers.get("Subject"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_deletion_request(client: TestClient, user: User, tu_user: User,
|
||||||
|
pkgbase: PackageBase, pkgreq: PackageRequest):
|
||||||
|
""" Test deleting a package with a preexisting request. """
|
||||||
|
# `pkgreq`.ReqTypeID is already DELETION_ID.
|
||||||
|
create_request(DELETION_ID, user, pkgbase, "Other request.")
|
||||||
|
|
||||||
|
# Create a notification record for another user. They should then
|
||||||
|
# also receive a DeleteNotification.
|
||||||
|
user2 = create_user("test2", "test2@example.org")
|
||||||
|
create_notification(user2, pkgbase)
|
||||||
|
|
||||||
|
endpoint = f"/pkgbase/{pkgbase.Name}/delete"
|
||||||
|
comments = "Test closure."
|
||||||
|
data = {"comments": comments, "confirm": True}
|
||||||
|
with client as request:
|
||||||
|
resp = request.post(endpoint, data=data, cookies=tu_user.cookies)
|
||||||
|
assert resp.status_code == int(HTTPStatus.SEE_OTHER)
|
||||||
|
assert resp.headers.get("location") == "/packages"
|
||||||
|
|
||||||
|
# Ensure that `pkgreq`.ClosureComment was left alone when specified.
|
||||||
|
assert pkgreq.ClosureComment == comments
|
||||||
|
|
||||||
|
# We should've gotten three emails. Two accepted requests and
|
||||||
|
# a DeleteNotification.
|
||||||
|
assert Email.count() == 3
|
||||||
|
|
||||||
|
# Both requests should have gotten accepted and had a notification
|
||||||
|
# sent out for them.
|
||||||
|
for i in range(Email.count() - 1):
|
||||||
|
email = Email(i + 1).parse()
|
||||||
|
expr = r"^\[PRQ#\d+\] Deletion Request for [^ ]+ Accepted$"
|
||||||
|
assert re.match(expr, email.headers.get("Subject"))
|
||||||
|
|
||||||
|
# We should've also had a DeleteNotification sent out.
|
||||||
|
email = Email(3).parse()
|
||||||
|
subject = r"^AUR Package deleted: [^ ]+$"
|
||||||
|
assert re.match(subject, email.headers.get("Subject"))
|
||||||
|
body = r"%s [1] deleted %s [2]." % (tu_user.Username, pkgbase.Name)
|
||||||
|
assert body in email.body
|
||||||
|
|
||||||
|
|
||||||
|
def test_deletion_autorequest(client: TestClient, tu_user: User,
|
||||||
|
pkgbase: PackageBase):
|
||||||
|
""" Test deleting a package without a request. """
|
||||||
|
# `pkgreq`.ReqTypeID is already DELETION_ID.
|
||||||
|
endpoint = f"/pkgbase/{pkgbase.Name}/delete"
|
||||||
|
data = {"confirm": True}
|
||||||
|
with client as request:
|
||||||
|
resp = request.post(endpoint, data=data, cookies=tu_user.cookies)
|
||||||
|
assert resp.status_code == int(HTTPStatus.SEE_OTHER)
|
||||||
|
|
||||||
|
assert resp.headers.get("location") == "/packages"
|
||||||
|
assert Email.count() == 1
|
||||||
|
|
||||||
|
email = Email(1).parse()
|
||||||
|
subject = r"^\[PRQ#\d+\] Deletion Request for [^ ]+ Accepted$"
|
||||||
|
assert re.match(subject, email.headers.get("Subject"))
|
||||||
|
assert "[Autogenerated]" in email.body
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_request(client: TestClient, user: User, tu_user: User,
|
||||||
|
pkgbase: PackageBase, target: PackageBase,
|
||||||
|
pkgreq: PackageRequest):
|
||||||
|
""" Test merging a package with a pre - existing request. """
|
||||||
|
with db.begin():
|
||||||
|
pkgreq.ReqTypeID = MERGE_ID
|
||||||
|
pkgreq.MergeBaseName = target.Name
|
||||||
|
|
||||||
|
other_target = create_pkgbase(user, "other-target")
|
||||||
|
other_request = create_request(MERGE_ID, user, pkgbase, "Other request.")
|
||||||
|
other_target2 = create_pkgbase(user, "other-target2")
|
||||||
|
other_request2 = create_request(MERGE_ID, user, pkgbase, "Other request2.")
|
||||||
|
with db.begin():
|
||||||
|
other_request.MergeBaseName = other_target.Name
|
||||||
|
other_request2.MergeBaseName = other_target2.Name
|
||||||
|
|
||||||
|
# `pkgreq`.ReqTypeID is already DELETION_ID.
|
||||||
|
endpoint = f"/pkgbase/{pkgbase.Name}/merge"
|
||||||
|
comments = "Test merge closure."
|
||||||
|
data = {"into": target.Name, "comments": comments, "confirm": True}
|
||||||
|
with client as request:
|
||||||
|
resp = request.post(endpoint, data=data, cookies=tu_user.cookies)
|
||||||
|
assert resp.status_code == int(HTTPStatus.SEE_OTHER)
|
||||||
|
assert resp.headers.get("location") == f"/pkgbase/{target.Name}"
|
||||||
|
|
||||||
|
# Ensure that `pkgreq`.ClosureComment was left alone when specified.
|
||||||
|
assert pkgreq.ClosureComment == comments
|
||||||
|
|
||||||
|
# We should've gotten 3 emails: an accepting and two rejections.
|
||||||
|
assert Email.count() == 3
|
||||||
|
|
||||||
|
# Assert specific IDs match up in the subjects.
|
||||||
|
accepted = Email(1).parse()
|
||||||
|
subj = r"^\[PRQ#%d\] Merge Request for [^ ]+ Accepted$" % pkgreq.ID
|
||||||
|
assert re.match(subj, accepted.headers.get("Subject"))
|
||||||
|
|
||||||
|
# In the accepted case, we already supplied a closure comment,
|
||||||
|
# which stops one from being autogenerated by the algorithm.
|
||||||
|
assert "[Autogenerated]" not in accepted.body
|
||||||
|
|
||||||
|
# Test rejection emails, which do have autogenerated closures.
|
||||||
|
rejected = Email(2).parse()
|
||||||
|
subj = r"^\[PRQ#%d\] Merge Request for [^ ]+ Rejected$" % other_request.ID
|
||||||
|
assert re.match(subj, rejected.headers.get("Subject"))
|
||||||
|
assert "[Autogenerated]" in rejected.body
|
||||||
|
|
||||||
|
rejected = Email(3).parse()
|
||||||
|
subj = r"^\[PRQ#%d\] Merge Request for [^ ]+ Rejected$" % other_request2.ID
|
||||||
|
assert re.match(subj, rejected.headers.get("Subject"))
|
||||||
|
assert "[Autogenerated]" in rejected.body
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_autorequest(client: TestClient, user: User, tu_user: User,
|
||||||
|
pkgbase: PackageBase, target: PackageBase):
|
||||||
|
""" Test merging a package without a request. """
|
||||||
|
with db.begin():
|
||||||
|
pkgreq.ReqTypeID = MERGE_ID
|
||||||
|
pkgreq.MergeBaseName = target.Name
|
||||||
|
|
||||||
|
# `pkgreq`.ReqTypeID is already DELETION_ID.
|
||||||
|
endpoint = f"/pkgbase/{pkgbase.Name}/merge"
|
||||||
|
data = {"into": target.Name, "confirm": True}
|
||||||
|
with client as request:
|
||||||
|
resp = request.post(endpoint, data=data, cookies=tu_user.cookies)
|
||||||
|
assert resp.status_code == int(HTTPStatus.SEE_OTHER)
|
||||||
|
assert resp.headers.get("location") == f"/pkgbase/{target.Name}"
|
||||||
|
|
||||||
|
# Should've gotten one email; an [Autogenerated] one.
|
||||||
|
assert Email.count() == 1
|
||||||
|
|
||||||
|
# Test accepted merge request notification.
|
||||||
|
email = Email(1).parse()
|
||||||
|
subj = r"^\[PRQ#\d+\] Merge Request for [^ ]+ Accepted$"
|
||||||
|
assert re.match(subj, email.headers.get("Subject"))
|
||||||
|
assert "[Autogenerated]" in email.body
|
||||||
|
|
||||||
|
|
||||||
|
def test_orphan_request(client: TestClient, user: User, tu_user: User,
|
||||||
|
pkgbase: PackageBase, pkgreq: PackageRequest):
|
||||||
|
""" Test the standard orphan request route. """
|
||||||
|
idle_time = config.getint("options", "request_idle_time")
|
||||||
|
now = int(datetime.utcnow().timestamp())
|
||||||
|
with db.begin():
|
||||||
|
pkgreq.ReqTypeID = ORPHAN_ID
|
||||||
|
# Set the request time so it's seen as due (idle_time has passed).
|
||||||
|
pkgreq.RequestTS = now - idle_time - 10
|
||||||
|
|
||||||
|
endpoint = f"/pkgbase/{pkgbase.Name}/disown"
|
||||||
|
comments = "Test orphan closure."
|
||||||
|
data = {"comments": comments, "confirm": True}
|
||||||
|
with client as request:
|
||||||
|
resp = request.post(endpoint, data=data, cookies=tu_user.cookies)
|
||||||
|
assert resp.status_code == int(HTTPStatus.SEE_OTHER)
|
||||||
|
assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}"
|
||||||
|
|
||||||
|
# Ensure that `pkgreq`.ClosureComment was left alone when specified.
|
||||||
|
assert pkgreq.ClosureComment == comments
|
||||||
|
|
||||||
|
# Check the email we expect.
|
||||||
|
assert Email.count() == 1
|
||||||
|
email = Email(1).parse()
|
||||||
|
subj = r"^\[PRQ#%d\] Orphan Request for [^ ]+ Accepted$" % pkgreq.ID
|
||||||
|
assert re.match(subj, email.headers.get("Subject"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_request_post_orphan_autoaccept(client: TestClient, auser: User,
|
||||||
|
pkgbase: PackageBase,
|
||||||
|
caplog: pytest.LogCaptureFixture):
|
||||||
|
""" Test the standard pkgbase request route GET method. """
|
||||||
|
caplog.set_level(DEBUG)
|
||||||
|
now = int(datetime.utcnow().timestamp())
|
||||||
|
auto_orphan_age = config.getint("options", "auto_orphan_age")
|
||||||
|
with db.begin():
|
||||||
|
pkgbase.OutOfDateTS = now - auto_orphan_age - 100
|
||||||
|
|
||||||
|
endpoint = f"/pkgbase/{pkgbase.Name}/request"
|
||||||
|
data = {
|
||||||
|
"type": "orphan",
|
||||||
|
"comments": "Test request.",
|
||||||
|
}
|
||||||
|
with client as request:
|
||||||
|
resp = request.post(endpoint, data=data, cookies=auser.cookies)
|
||||||
|
assert resp.status_code == int(HTTPStatus.SEE_OTHER)
|
||||||
|
|
||||||
|
pkgreq = pkgbase.requests.first()
|
||||||
|
assert pkgreq is not None
|
||||||
|
assert pkgreq.ReqTypeID == ORPHAN_ID
|
||||||
|
|
||||||
|
# A Request(Open|Close)Notification should've been sent out.
|
||||||
|
assert Email.count() == 2
|
||||||
|
|
||||||
|
# Check the first email; should be our open request.
|
||||||
|
email = Email(1)
|
||||||
|
expr = r"^\[PRQ#%d\] Orphan Request for [^ ]+$" % pkgreq.ID
|
||||||
|
assert re.match(expr, email.headers.get("Subject"))
|
||||||
|
|
||||||
|
# And the second should be the automated closure.
|
||||||
|
email = Email(2)
|
||||||
|
expr = r"^\[PRQ#%d\] Orphan Request for [^ ]+ Accepted$" % pkgreq.ID
|
||||||
|
assert re.match(expr, email.headers.get("Subject"))
|
||||||
|
|
||||||
|
# Check logs.
|
||||||
|
expr = r"New request #\d+ is marked for auto-orphan."
|
||||||
|
assert re.search(expr, caplog.text)
|
||||||
|
|
||||||
|
|
||||||
|
def test_orphan_as_maintainer(client: TestClient, auser: User,
|
||||||
|
pkgbase: PackageBase):
|
||||||
|
endpoint = f"/pkgbase/{pkgbase.Name}/disown"
|
||||||
|
data = {"confirm": True}
|
||||||
|
with client as request:
|
||||||
|
resp = request.post(endpoint, data=data, cookies=auser.cookies)
|
||||||
|
assert resp.status_code == int(HTTPStatus.SEE_OTHER)
|
||||||
|
assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}"
|
||||||
|
|
||||||
|
assert Email.count() == 1
|
||||||
|
email = Email(1)
|
||||||
|
expr = r"^\[PRQ#\d+\] Orphan Request for [^ ]+ Accepted$"
|
||||||
|
assert re.match(expr, email.headers.get("Subject"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_orphan_without_requests(client: TestClient, tu_user: User,
|
||||||
|
pkgbase: PackageBase):
|
||||||
|
""" Test orphans are automatically accepted past a certain date. """
|
||||||
|
endpoint = f"/pkgbase/{pkgbase.Name}/disown"
|
||||||
|
data = {"confirm": True}
|
||||||
|
with client as request:
|
||||||
|
resp = request.post(endpoint, data=data, cookies=tu_user.cookies)
|
||||||
|
assert resp.status_code == int(HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
|
errors = get_errors(resp.text)
|
||||||
|
expected = r"^No due existing orphan requests to accept for .+\.$"
|
||||||
|
assert re.match(expected, errors[0].text.strip())
|
||||||
|
|
||||||
|
assert Email.count() == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_closure_factory_invalid_reqtype_id():
|
||||||
|
""" Test providing an invalid reqtype_id raises NotImplementedError. """
|
||||||
|
automated = ClosureFactory()
|
||||||
|
match = r"^Unsupported '.+' value\.$"
|
||||||
|
with pytest.raises(NotImplementedError, match=match):
|
||||||
|
automated.get_closure(666, None, None, None, ACCEPTED_ID)
|
||||||
|
with pytest.raises(NotImplementedError, match=match):
|
||||||
|
automated.get_closure(666, None, None, None, REJECTED_ID)
|
Loading…
Add table
Reference in a new issue