Deleting a staff or client account is two writes: soft-delete the account,
then cascade or reassign the files and folders it owns. All four destroy()
paths (Users + Clients, web + API) ran them one after the other with nothing
tying them together.
If the second write throws, the account is already gone but its content is
not handled. The concrete way in is the reassign branch: validate() checks
reassign_to_id with exists(active), but apply() re-resolves it with
findOrFail() a moment later (AccountContentDeletion:108), so a target
deactivated or deleted in between throws — leaving a soft-deleted account
whose files still point at it, and a UserDeleted log for a deletion that did
not finish.
Wrap the delete()+apply() pair in a single DB::transaction() in each of the
four destroy() methods. validate() and the authorization guards stay outside
it: they are read-only and must be able to reject before anything is written.
cascadeDelete()/reassignTo() already open their own transaction, which nests
as a savepoint under this one, so the account soft-delete, its activity log,
and the content work now commit or roll back together.
Tests: a DeletedAccountContent double that reports content to handle and then
throws while handling it (tests/Helpers.php) drives one test per destroy()
endpoint asserting the account survives the failure and no UserDeleted entry
is written; each goes red against the un-wrapped controller.
It is used by both OAuth mail test files, which --parallel runs in
separate processes — exactly the situation tests/Helpers.php exists
for, as its own header explains. CI caught what a whole-suite serial
run hides.
publicListingFile() and publicListingImageFile() were defined in
PublicGroupsTest.php and used from PublicFilePreviewTest.php too. Pest
declares a test file's functions as ordinary globals, so that works only
once the defining file has been loaded — which under --parallel depends
on how the runner happens to distribute files across processes. Adding
any unrelated test file anywhere in the suite reshuffles that and takes
PublicFilePreviewTest.php down with "Call to undefined function", and
running it on its own with --filter never worked at all.
tests/Helpers.php exists for exactly this and its docblock describes this
failure; these two had just been missed. publicPageProps() stays where it
is, since only one file uses it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Client file sharing, rebuilt from the ground up: a private area per
client, resumable uploads, folders, groups and categories, sharing with
expiry dates and download limits, comments, file versions, an activity
log, a REST API, and sixteen languages.
This repository begins here. ProjectSend 2 was developed privately, and
that development history is not published — the previous generation
remains available, with its own history, at projectsend/legacy.
Free software under the GNU General Public License v2, or (at your
option) any later version.