mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-18 09:35:07 +00:00
61c385e423
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.
131 lines
4.8 KiB
PHP
131 lines
4.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\User;
|
|
use App\Modules\Audit\Action;
|
|
use App\Modules\Audit\ActivityLog;
|
|
use App\Modules\Identity\Permissions\SystemRole;
|
|
use App\Modules\Identity\UserType;
|
|
use App\Modules\Platform\Capabilities\Edition;
|
|
use Inertia\Testing\AssertableInertia;
|
|
|
|
beforeEach(function () {
|
|
$this->admin = User::factory()->create();
|
|
});
|
|
|
|
test('the index lists clients only — staff never appear', function () {
|
|
User::factory()->client()->create(['name' => 'A Client']);
|
|
User::factory()->role(SystemRole::Uploader)->create(['name' => 'A Staffer']);
|
|
|
|
$this->actingAs($this->admin)->get('/clients')->assertInertia(
|
|
fn (AssertableInertia $page) => $page
|
|
->component('clients/index')
|
|
->has('clients', 1)
|
|
->where('clients.0.name', 'A Client'),
|
|
);
|
|
});
|
|
|
|
test('a staff-created client is active with the Client role', function () {
|
|
$response = $this->actingAs($this->admin)->post('/clients', [
|
|
'name' => 'Handmade Client',
|
|
'email' => 'made@example.com',
|
|
'password' => 'super-secret-password',
|
|
'password_confirmation' => 'super-secret-password',
|
|
]);
|
|
|
|
$client = User::query()->where('email', 'made@example.com')->sole();
|
|
$response->assertRedirect(route('clients.edit', $client));
|
|
expect($client->type)->toBe(UserType::Client)
|
|
->and($client->active)->toBeTrue()
|
|
->and($client->role?->name)->toBe('Client');
|
|
});
|
|
|
|
test('staff accounts are unreachable through client screens', function () {
|
|
$staffer = User::factory()->role(SystemRole::Uploader)->create();
|
|
|
|
$this->actingAs($this->admin);
|
|
$this->get("/clients/{$staffer->id}")->assertNotFound();
|
|
$this->patch("/clients/{$staffer->id}", [])->assertNotFound();
|
|
$this->delete("/clients/{$staffer->id}")->assertNotFound();
|
|
});
|
|
|
|
test('activating a pending client through the edit screen clears the request flag', function () {
|
|
$pending = User::factory()->pendingClient()->create();
|
|
|
|
$this->actingAs($this->admin)->patch("/clients/{$pending->id}", [
|
|
'name' => $pending->name,
|
|
'email' => $pending->email,
|
|
'active' => true,
|
|
])->assertRedirect();
|
|
|
|
$pending->refresh();
|
|
expect($pending->active)->toBeTrue()
|
|
->and($pending->account_requested)->toBeFalse();
|
|
});
|
|
|
|
test('an account manager can manage clients but an uploader cannot', function () {
|
|
// Account Manager holds create/edit/delete_clients + approve_account_requests.
|
|
$manager = User::factory()->role(SystemRole::AccountManager)->create();
|
|
$this->actingAs($manager);
|
|
$this->get('/account-requests')->assertOk();
|
|
|
|
// manage_clients is NOT in the Account Manager default set (v1 parity):
|
|
// the list needs it, so the index is refused while the queue works.
|
|
$this->get('/clients')->assertForbidden();
|
|
|
|
$uploader = User::factory()->role(SystemRole::Uploader)->create();
|
|
$this->actingAs($uploader);
|
|
$this->get('/clients')->assertForbidden();
|
|
$this->get('/account-requests')->assertForbidden();
|
|
});
|
|
|
|
test('client management exists in the cloud edition too', function () {
|
|
config()->set('projectsend.edition', Edition::Cloud);
|
|
|
|
$this->actingAs($this->admin)->get('/clients')->assertOk();
|
|
$this->actingAs($this->admin)->get('/account-requests')->assertOk();
|
|
});
|
|
|
|
test('staff can update client settings and they take effect', function () {
|
|
$this->actingAs($this->admin);
|
|
|
|
$this->get('/system/settings/clients')->assertInertia(
|
|
fn (AssertableInertia $page) => $page
|
|
->component('system/settings/clients')
|
|
->where('clients_can_register', false),
|
|
);
|
|
|
|
$this->patch('/system/settings/clients', [
|
|
'clients_can_register' => true,
|
|
'clients_auto_approve' => false,
|
|
'clients_auto_group' => 0,
|
|
'clients_can_select_group' => 'none',
|
|
'clients_membership_deny_cooldown_days' => 30,
|
|
'default_client_storage_quota_mb' => 0,
|
|
'clients_can_preview_files' => true,
|
|
])->assertRedirect()->assertSessionDoesntHaveErrors();
|
|
|
|
Auth::logout();
|
|
$this->flushSession();
|
|
$this->get('/register')->assertOk();
|
|
});
|
|
|
|
test('a failure while disposing of a deleted client\'s content rolls the deletion back', function () {
|
|
$client = User::factory()->client()->create();
|
|
$reassignTarget = User::factory()->create();
|
|
|
|
failAccountContentDisposal();
|
|
|
|
$this->actingAs($this->admin)->delete("/clients/{$client->id}", [
|
|
'content_action' => 'reassign',
|
|
'reassign_to_id' => $reassignTarget->id,
|
|
])->assertStatus(500);
|
|
|
|
// The soft-delete and its log share a transaction with the content step,
|
|
// so a failure there leaves the client intact rather than
|
|
// deleted-but-still-owning-files.
|
|
expect(User::query()->find($client->id))->not->toBeNull()
|
|
->and(ActivityLog::query()->where('action', Action::UserDeleted)->exists())->toBeFalse();
|
|
});
|