mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-17 00:55:07 +00:00
28e18497b5
ProfileController::destroy() validates current_password and soft-deletes. It never asks StaffAccounts::guardLastAdministrator(), and every other door does: Staff update(), guardDeletable(), and both directions of the role conversion. This is the one door where the account being removed is certainly signed in. An installation with a single administrator therefore had a button that emptied it. Measured on main: DELETE /settings/profile 302, the account is gone live staff rows 0 (the row is trashed, not removed) anonymous GET / 302 -> /setup anonymous POST /setup a new active System Administrator EnsureSetupIsComplete asks ->exists(), which excludes trashed rows, and routes/web.php registers GET and POST setup with no auth and no guest middleware -- correctly, since a fresh installation has nobody to authenticate. SetupController::store() re-checks the same condition, so both halves agreed with each other and both were wrong once the last staff row was trashed. Two locks, because one of them is asked at five doors and the other at one. First: destroy() now asks guardLastAdministrator(), the same call with the same message as everywhere else. An administrator with a colleague still goes, a non-administrator staff member still goes, and a client still closes their own account. Second: "has this installation been set up" is not the same question as "does it have a working administrator right now", and only the first one belongs in EnsureSetupIsComplete. A trashed staff row is still evidence that setup happened, so it now counts -- in the middleware and in SetupController::setupIsComplete(), which have to agree or the result is either a redirect loop or an open form. That second lock holds even if a future door forgets the first one. Measured with the guard bypassed entirely and the row trashed directly: GET / answers with the login screen and POST /setup creates nothing. Worth stating plainly: an installation that has already lost its last administrator will now find setup shut rather than open. That is the point -- the recovery path for it is `php artisan projectsend:admin`, which is also how every unattended container installs itself, not a form that anybody on the internet can reach. Six tests, two measured red against the unfixed code (2 failed / 4 passed) -- one per lock. The other four are the boundaries: a colleague present, a staff member who is not an administrator, a client, and a genuinely fresh installation that must still reach setup. Two existing tests needed saying more clearly rather than changing: ProfileUpdateTest's deletion cases now create a second administrator, so that what they assert is self-deletion and not this new refusal; and GettingStartedTest's "fresh installation" cases forceDelete rather than delete, because a soft-deleted staff row is no longer a fresh installation -- which is the whole of the second lock. Full suite passes (2054 passed / 2 skipped), PHPStan level 8 clean.
135 lines
4.0 KiB
PHP
135 lines
4.0 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Settings;
|
|
|
|
use App\Models\User;
|
|
use App\Modules\Platform\Settings\Setting;
|
|
use App\Modules\Platform\Settings\Settings;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Tests\TestCase;
|
|
|
|
class ProfileUpdateTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
public function test_profile_page_is_displayed()
|
|
{
|
|
$user = User::factory()->create();
|
|
|
|
$response = $this
|
|
->actingAs($user)
|
|
->get('/settings/profile');
|
|
|
|
$response->assertOk();
|
|
}
|
|
|
|
public function test_profile_information_can_be_updated()
|
|
{
|
|
$user = User::factory()->create();
|
|
|
|
$response = $this
|
|
->actingAs($user)
|
|
->patch('/settings/profile', [
|
|
'name' => 'Test User',
|
|
'email' => 'test@example.com',
|
|
]);
|
|
|
|
$response
|
|
->assertSessionHasNoErrors()
|
|
->assertRedirect('/settings/profile');
|
|
|
|
$user->refresh();
|
|
|
|
$this->assertSame('Test User', $user->name);
|
|
$this->assertSame('test@example.com', $user->email);
|
|
$this->assertNull($user->email_verified_at);
|
|
}
|
|
|
|
public function test_email_verification_status_is_unchanged_when_the_email_address_is_unchanged()
|
|
{
|
|
$user = User::factory()->create();
|
|
|
|
$response = $this
|
|
->actingAs($user)
|
|
->patch('/settings/profile', [
|
|
'name' => 'Test User',
|
|
'email' => $user->email,
|
|
]);
|
|
|
|
$response
|
|
->assertSessionHasNoErrors()
|
|
->assertRedirect('/settings/profile');
|
|
|
|
$this->assertNotNull($user->refresh()->email_verified_at);
|
|
}
|
|
|
|
public function test_user_can_delete_their_account()
|
|
{
|
|
$user = User::factory()->create();
|
|
|
|
// A second administrator, so what is under test is self-deletion
|
|
// and not the last-administrator refusal: the factory makes an
|
|
// administrator, and one on their own may no longer remove
|
|
// themselves — see SoleAdministratorSelfDeletionTest.
|
|
User::factory()->create();
|
|
|
|
$response = $this
|
|
->actingAs($user)
|
|
->delete('/settings/profile', [
|
|
'password' => 'password',
|
|
]);
|
|
|
|
$response
|
|
->assertSessionHasNoErrors()
|
|
->assertRedirect('/');
|
|
|
|
$this->assertGuest();
|
|
// Deletion is soft: gone from every query, recoverable by an admin.
|
|
$this->assertNull(User::query()->find($user->id));
|
|
$this->assertNotNull(User::withTrashed()->find($user->id));
|
|
}
|
|
|
|
public function test_correct_password_must_be_provided_to_delete_account()
|
|
{
|
|
$user = User::factory()->create();
|
|
|
|
$response = $this
|
|
->actingAs($user)
|
|
->from('/settings/profile')
|
|
->delete('/settings/profile', [
|
|
'password' => 'wrong-password',
|
|
]);
|
|
|
|
$response
|
|
->assertSessionHasErrors('password')
|
|
->assertRedirect('/settings/profile');
|
|
|
|
$this->assertNotNull($user->fresh());
|
|
}
|
|
|
|
public function test_erasure_grace_period_reflects_the_privacy_setting()
|
|
{
|
|
app(Settings::class)->set(Setting::AccountErasureGraceDays, 5);
|
|
|
|
$user = User::factory()->create();
|
|
|
|
// Same reason as above: this is about the grace period, not about
|
|
// who is allowed to go.
|
|
User::factory()->create();
|
|
|
|
// Deleting your account has its own screen; the profile form no
|
|
// longer carries the block or the grace period behind it.
|
|
$this->actingAs($user)
|
|
->get('/settings/delete-account')
|
|
->assertInertia(fn ($page) => $page
|
|
->component('settings/delete-account')
|
|
->where('erasureGraceDays', 5));
|
|
|
|
$this->actingAs($user)->delete('/settings/profile', ['password' => 'password']);
|
|
|
|
$this->assertTrue(
|
|
User::withTrashed()->find($user->id)->erase_after->isSameDay(now()->addDays(5)),
|
|
);
|
|
}
|
|
}
|