config()->set('admin_access.secrets_key', 'base64:'.base64_encode(random_bytes(32)))); it('confirms a console password against the operator record, not a portal user', function () { $operator = Operator::factory()->role('Owner')->create(['password' => 'operator-passwort']); Livewire::actingAs($operator, 'operator')->test(Secrets::class) ->set('confirmablePassword', 'operator-passwort') ->call('confirmPassword') ->assertHasNoErrors(); }); it('rejects a password that belongs to a portal user with the same address', function () { // The trap: two tables, one address. Confirming against the wrong one would // let a customer's password unlock the console. $operator = Operator::factory()->role('Owner')->create([ 'email' => 'doppelt@clupilot.test', 'password' => 'operator-passwort', ]); User::factory()->create(['email' => 'doppelt@clupilot.test', 'password' => 'kunden-passwort']); Livewire::actingAs($operator, 'operator')->test(Secrets::class) ->set('confirmablePassword', 'kunden-passwort') ->call('confirmPassword') ->assertHasErrors('confirmablePassword'); }); it('still confirms a portal password on the web guard', function () { $user = User::factory()->create(['password' => 'kunden-passwort']); Livewire::actingAs($user)->test(App\Livewire\Settings::class) ->set('confirmablePassword', 'kunden-passwort') ->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(); });