mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-16 16:45:07 +00:00
fc5651faad
Three middleware answer before HandleInertiaRequests and so have to repeat its 302→303 upgrade themselves: EnsureSetupIsComplete, EnsureUserIsActive and EnforceTwoFactor. This file has a write case for each, and the rule has a second half -- a read still gets a plain 302, because a 303 there would be an upgrade nobody asked for. That half was checked once, on the deactivation door, under a name that said otherwise: "leaves a read alone in every one of those cases". The setup door and the two-factor door were not covered at all, so a change that upgraded reads at either of them would have gone through with the suite green and this test's name still claiming it would not. Both are covered now, as a dataset with one case per door. The setup case reads a guest-reachable GET for the same reason the write case posts to /timezone: anything behind `auth` is answered by the guest redirect before EnsureSetupIsComplete sees it. No production code changes; today all three doors answer a read with 302, which is what the new cases assert. Demonstrated by mutation rather than reversion: making EnsureSetupIsComplete upgrade every redirect to 303 fails this file (1 failed / 8 passed) and passes the old one (7 passed).
120 lines
4.7 KiB
PHP
120 lines
4.7 KiB
PHP
<?php
|
|
|
|
use App\Models\User;
|
|
use App\Modules\Identity\TwoFactor\TwoFactorEnforcement;
|
|
use App\Modules\Platform\Settings\Setting;
|
|
use App\Modules\Platform\Settings\Settings;
|
|
|
|
// A redirect answered to a PUT/PATCH/DELETE must be a 303, not a 302:
|
|
// browsers follow a 302 by replaying the same method on the redirect
|
|
// target (only POST is downgraded to GET), so "your session expired,
|
|
// please sign in" turns into PUT /login — and /login only takes GET and
|
|
// POST, so the user sees an unexplainable 405 instead of the login page.
|
|
// Redirects born in exception handling (the guest redirect above all)
|
|
// never pass back through Inertia's middleware, which normally does this
|
|
// upgrade — bootstrap/app.php repeats it for them. See issue #1673.
|
|
|
|
it('answers an unauthenticated write with 303 so the browser lands on the login page', function () {
|
|
$this->put(route('dashboard.widgets.update'), [])
|
|
->assertStatus(303)
|
|
->assertRedirect(route('login'));
|
|
});
|
|
|
|
it('answers an unauthenticated delete with 303 as well', function () {
|
|
$user = User::factory()->create();
|
|
|
|
$this->delete(route('users.destroy', $user))
|
|
->assertStatus(303)
|
|
->assertRedirect(route('login'));
|
|
});
|
|
|
|
it('keeps the plain 302 for unauthenticated reads', function () {
|
|
$this->get(route('dashboard'))
|
|
->assertStatus(302)
|
|
->assertRedirect(route('login'));
|
|
});
|
|
|
|
// The three middleware below answer *before* HandleInertiaRequests, so a
|
|
// response they return never unwinds through Inertia's 302→303 upgrade
|
|
// either — the same 405 as above, reached a different way. Flagged in
|
|
// #1680 as deliberately out of scope there; these cover it.
|
|
|
|
it('answers a write with 303 when the installation has no administrator yet', function () {
|
|
// Deliberately no staff user: that is what EnsureSetupIsComplete
|
|
// reacts to, and every other test in the suite creates one.
|
|
User::query()->delete();
|
|
|
|
// PUT /timezone rather than a dashboard route: it is one of only two
|
|
// writes a guest can reach, and the only one EnsureSetupIsComplete
|
|
// does not exempt. Anything behind `auth` is answered by the guest
|
|
// redirect first, which is the case #1680 already covers.
|
|
$this->put(route('timezone.update'), ['timezone' => 'UTC'])
|
|
->assertStatus(303)
|
|
->assertRedirect(route('setup'));
|
|
});
|
|
|
|
it('answers a write with 303 when the account was deactivated mid-session', function () {
|
|
$user = User::factory()->create();
|
|
$this->actingAs($user);
|
|
|
|
$user->update(['active' => false]);
|
|
|
|
$this->put(route('dashboard.widgets.update'), [])
|
|
->assertStatus(303)
|
|
->assertRedirect(route('login'));
|
|
});
|
|
|
|
it('answers a write with 303 when two-factor enrolment is being enforced', function () {
|
|
$user = User::factory()->create();
|
|
|
|
// Set explicitly rather than relying on the default: the settings
|
|
// cache outlives a database rollback in this suite.
|
|
app(Settings::class)->set(Setting::TwoFactorEnforcement, TwoFactorEnforcement::All->value);
|
|
|
|
$this->actingAs($user)
|
|
->put(route('dashboard.widgets.update'), [])
|
|
->assertStatus(303)
|
|
->assertRedirect(route('two-factor.show'));
|
|
});
|
|
|
|
// The other half of the rule, and the half that says the upgrade is
|
|
// targeted: a read still gets the plain 302. Once per door, because "every
|
|
// one of those cases" was one of them — the deactivation — and a change
|
|
// that upgraded reads at either of the other two would have been invisible
|
|
// here while this name said otherwise.
|
|
it('leaves a read alone at every one of those doors', function (Closure $arrange, string $read, Closure $target) {
|
|
$arrange();
|
|
|
|
$this->get($read)
|
|
->assertStatus(302)
|
|
->assertRedirect($target());
|
|
})->with([
|
|
// A guest-reachable GET, for the same reason the write case uses
|
|
// PUT /timezone: anything behind `auth` is answered by the guest
|
|
// redirect before EnsureSetupIsComplete ever sees it.
|
|
'no administrator yet' => [
|
|
fn () => User::query()->delete(),
|
|
'/login',
|
|
fn () => route('setup'),
|
|
],
|
|
'deactivated mid-session' => [
|
|
function (): void {
|
|
$user = User::factory()->create();
|
|
$user->update(['active' => false]);
|
|
test()->actingAs($user);
|
|
},
|
|
'/dashboard',
|
|
fn () => route('login'),
|
|
],
|
|
'two-factor enrolment enforced' => [
|
|
function (): void {
|
|
// Set explicitly rather than relying on the default: the
|
|
// settings cache outlives a database rollback in this suite.
|
|
app(Settings::class)->set(Setting::TwoFactorEnforcement, TwoFactorEnforcement::All->value);
|
|
test()->actingAs(User::factory()->create());
|
|
},
|
|
'/dashboard',
|
|
fn () => route('two-factor.show'),
|
|
],
|
|
]);
|