Prove ConfirmsPassword resolves its guard, not the ambient default

actingAs()/Livewire::actingAs() call Auth::shouldUse(), which made every
existing test for this trait pass identically whether confirmPassword() asked
for the right guard explicitly or just read whichever guard happened to be
default — reverting the explicit guard() call to a bare auth()->user() passed
the full suite. This test signs the operator in without actingAs and
deliberately decouples the ambient default guard from their session before
calling confirmPassword(), so only the explicit resolution can succeed.
feat/operator-identity
nexxo 2026-07-28 11:54:13 +02:00
parent 8f8a0ad4f7
commit 73dec7f685
1 changed files with 48 additions and 0 deletions

View File

@ -3,6 +3,7 @@
use App\Livewire\Admin\Secrets;
use App\Models\Operator;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Livewire\Livewire;
beforeEach(fn () => config()->set('admin_access.secrets_key', 'base64:'.base64_encode(random_bytes(32))));
@ -38,3 +39,50 @@ it('still confirms a portal password on the web guard', function () {
->call('confirmPassword')
->assertHasNoErrors();
});
it('resolves the operator explicitly by guard, not by whichever guard happens to be the ambient default', function () {
// actingAs()/Livewire::actingAs() both call Auth::shouldUse($guard) as a
// side effect, which makes the named guard the DEFAULT guard — and under
// that, auth()->user() and auth()->guard('operator')->user() resolve
// identically, so every OTHER test in this file passes just the same
// whether confirmPassword() asks for the right guard explicitly or just
// reads the ambient default. That is not a hypothetical: reverting the
// trait's guard(...)->user() call back to a bare auth()->user() passes
// the entire 834-test suite, this file included — the gap this test
// closes, by making that distinction observable.
//
// Signing in with Auth::guard('operator')->login() alone (no actingAs)
// does NOT reach this test: it proves confirmationGuard is genuinely
// consulted, but Secrets::mount()'s own $this->authorize('secrets.manage')
// ALSO reads the ambient default guard (Laravel's Gate resolves the
// current user the same bare way) — so mount() itself 403s before
// confirmPassword() is ever reached, on the very account this test means
// to prove works. Confirmed directly: Auth::guard('web')->check() is
// false, Auth::guard('operator')->check() is true, and still
// Gate::check('secrets.manage') is false in that state.
//
// So: mount through actingAs() as usual — authorize() runs exactly once,
// while shouldUse('operator') is still in effect from actingAs, and
// legitimately passes. mount() does not run again on a later ->call().
// Only THEN, before touching confirmPassword() specifically, undo the
// masking by hand: Auth::shouldUse('web') switches the ambient default
// back to 'web' — genuinely empty, not merely relabelled — while the
// operator's own session on the 'operator' guard is untouched (setUser()
// on a guard does not depend on which guard is "default"). That is
// exactly the real shape of a console request: a genuine operator sign-in
// (App\Livewire\Auth\OperatorLogin::completeLogin()) never touches the
// web guard, default or not.
$operator = Operator::factory()->role('Owner')->create(['password' => 'operator-passwort']);
$page = Livewire::actingAs($operator, 'operator')->test(Secrets::class);
Auth::shouldUse('web');
expect(Auth::guard('web')->check())->toBeFalse()
->and(Auth::guard('operator')->check())->toBeTrue()
->and(auth()->check())->toBeFalse();
$page->set('confirmablePassword', 'operator-passwort')
->call('confirmPassword')
->assertHasNoErrors();
});