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'), ], ]);