125 lines
4.8 KiB
PHP
125 lines
4.8 KiB
PHP
<?php
|
|
|
|
use App\Livewire\Auth\OperatorLogin;
|
|
use App\Models\Operator;
|
|
use App\Models\User;
|
|
use App\Support\AdminArea;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Livewire\Livewire;
|
|
|
|
it('offers no way to register — an operator account is not self-served', function () {
|
|
$this->get(route('admin.login'))->assertOk()
|
|
->assertDontSee(route('register'))
|
|
->assertDontSee(__('auth.sign_up'));
|
|
});
|
|
|
|
it('sends an already signed-in operator back to the console instead of the portal', function () {
|
|
// Codex + whole-branch review (M1), independently: guest:operator's
|
|
// authenticated-user redirect had no console-aware override, so it fell
|
|
// to Laravel's own default — the named 'dashboard' route, the PORTAL.
|
|
// The web guard is usually unauthenticated there, so an operator
|
|
// revisiting admin.login bounced straight to the portal's OWN login. In
|
|
// exclusive mode it is worse: dashboard is not a console route at all,
|
|
// so RestrictAdminHost 404s it on the console host.
|
|
$operator = Operator::factory()->role('Owner')->create();
|
|
|
|
$this->actingAs($operator, 'operator')
|
|
->get(route('admin.login'))
|
|
->assertRedirect(route('admin.overview'));
|
|
});
|
|
|
|
it('signs an operator in on the operator guard', function () {
|
|
$operator = Operator::factory()->role('Owner')->create(['password' => 'richtig-langes-pw']);
|
|
|
|
Livewire::test(OperatorLogin::class)
|
|
->set('email', $operator->email)
|
|
->set('password', 'richtig-langes-pw')
|
|
->call('authenticate')
|
|
->assertRedirect(AdminArea::home());
|
|
|
|
expect(Auth::guard('operator')->check())->toBeTrue()
|
|
->and(Auth::guard('web')->check())->toBeFalse();
|
|
});
|
|
|
|
it('refuses a portal account at the console door', function () {
|
|
$user = User::factory()->create(['email' => 'kunde@clupilot.test', 'password' => 'kunden-passwort']);
|
|
|
|
Livewire::test(OperatorLogin::class)
|
|
->set('email', 'kunde@clupilot.test')
|
|
->set('password', 'kunden-passwort')
|
|
->call('authenticate')
|
|
->assertHasErrors('email');
|
|
|
|
expect(Auth::guard('operator')->check())->toBeFalse();
|
|
});
|
|
|
|
it('refuses a disabled operator', function () {
|
|
$operator = Operator::factory()->role('Owner')->create([
|
|
'password' => 'richtig-langes-pw', 'disabled_at' => now(),
|
|
]);
|
|
|
|
Livewire::test(OperatorLogin::class)
|
|
->set('email', $operator->email)
|
|
->set('password', 'richtig-langes-pw')
|
|
->call('authenticate')
|
|
->assertHasErrors('email');
|
|
|
|
expect(Auth::guard('operator')->check())->toBeFalse();
|
|
});
|
|
|
|
it('sends an operator with confirmed two-factor to the challenge instead of the console', function () {
|
|
$operator = Operator::factory()->role('Owner')->create([
|
|
'password' => 'richtig-langes-pw',
|
|
'two_factor_secret' => encrypt('JBSWY3DPEHPK3PXP'),
|
|
'two_factor_confirmed_at' => now(),
|
|
]);
|
|
|
|
Livewire::test(OperatorLogin::class)
|
|
->set('email', $operator->email)
|
|
->set('password', 'richtig-langes-pw')
|
|
->call('authenticate')
|
|
->assertRedirect(route('admin.two-factor'));
|
|
|
|
// Not signed in yet — the challenge is not decoration.
|
|
expect(Auth::guard('operator')->check())->toBeFalse();
|
|
});
|
|
|
|
it('throttles repeated failures', function () {
|
|
$operator = Operator::factory()->role('Owner')->create(['password' => 'richtig-langes-pw']);
|
|
|
|
$component = Livewire::test(OperatorLogin::class)->set('email', $operator->email)->set('password', 'falsch');
|
|
|
|
foreach (range(1, 5) as $ignored) {
|
|
$component->call('authenticate');
|
|
}
|
|
|
|
$component->call('authenticate')->assertHasErrors('email');
|
|
expect(session()->has('login.id'))->toBeFalse();
|
|
});
|
|
|
|
it('refuses to complete a login for an operator who no longer qualifies', function () {
|
|
// completeLogin() is the one place both the direct exit and the
|
|
// two-factor challenge complete a sign-in — re-checked here so a
|
|
// revocation in the window between the password step and a pending
|
|
// two-factor code cannot slip through the challenge, which only ever
|
|
// confirmed the row still existed.
|
|
$disabled = Operator::factory()->role('Owner')->create(['disabled_at' => now()]);
|
|
$roleless = Operator::factory()->create(); // no role synced at all
|
|
|
|
expect(OperatorLogin::completeLogin($disabled, false))->toBeFalse()
|
|
->and(Auth::guard('operator')->check())->toBeFalse();
|
|
|
|
expect(OperatorLogin::completeLogin($roleless, false))->toBeFalse()
|
|
->and(Auth::guard('operator')->check())->toBeFalse();
|
|
});
|
|
|
|
it('records when the operator last signed in', function () {
|
|
$operator = Operator::factory()->role('Owner')->create(['password' => 'richtig-langes-pw']);
|
|
|
|
Livewire::test(OperatorLogin::class)
|
|
->set('email', $operator->email)->set('password', 'richtig-langes-pw')
|
|
->call('authenticate');
|
|
|
|
expect($operator->fresh()->last_login_at)->not->toBeNull();
|
|
});
|