$guard, 'authenticatable_id' => $id, 'token_hash' => hash('sha256', Str::random(40)), 'name' => $device, 'last_seen_at' => now(), ]); DB::table('sessions')->insert([ 'id' => $sessionId, 'ip_address' => '203.0.113.10', 'user_agent' => 'test', 'payload' => '', 'last_activity' => $lastActivity ?? now()->getTimestamp(), ]); return LoginSession::create([ 'device_id' => $deviceRow->id, 'session_id' => $sessionId, 'ip_address' => '203.0.113.10', 'last_seen_at' => now(), ]); } it('lists the sessions of one identity and marks the current one', function () { $user = User::factory()->create(); $here = makeSession('web', $user->id, 'session-here', 'Chrome auf macOS'); makeSession('web', $user->id, 'session-there', 'Safari auf iPhone'); $rows = app(SessionRegistry::class)->for('web', $user->id, $here->uuid); expect($rows)->toHaveCount(2) ->and($rows->firstWhere('uuid', $here->uuid)->is_current)->toBeTrue() ->and($rows->firstWhere('device', 'Safari auf iPhone')->is_current)->toBeFalse(); }); it('does not show a customer the sessions of the operator with the same id', function () { $user = User::factory()->create(); makeSession('web', $user->id, 'session-customer'); makeSession('operator', $user->id, 'session-operator', 'Firefox auf Linux'); $rows = app(SessionRegistry::class)->for('web', $user->id); expect($rows)->toHaveCount(1) ->and($rows->first()->device)->not->toBe('Firefox auf Linux'); }); it('refuses to end a session that belongs to the other identity', function () { $user = User::factory()->create(); $operatorSession = makeSession('operator', $user->id, 'session-operator'); $ended = app(SessionRegistry::class)->end('web', $user->id, $operatorSession->uuid); expect($ended)->toBeFalse() ->and(DB::table('sessions')->where('id', 'session-operator')->exists())->toBeTrue(); }); it('ending a session removes the framework row, which is what signs the browser out', function () { $user = User::factory()->create(); $other = makeSession('web', $user->id, 'session-there'); expect(app(SessionRegistry::class)->end('web', $user->id, $other->uuid))->toBeTrue() ->and(DB::table('sessions')->where('id', 'session-there')->exists())->toBeFalse() ->and(LoginSession::query()->where('uuid', $other->uuid)->exists())->toBeFalse(); }); it('leaves the session doing the asking alone when ending the others', function () { // Signing somebody out of the page they are using to sign other people out // is not what the button says, and leaves them unable to see whether it // worked. $user = User::factory()->create(); $here = makeSession('web', $user->id, 'session-here'); makeSession('web', $user->id, 'session-b'); makeSession('web', $user->id, 'session-c'); $ended = app(SessionRegistry::class)->endOthers('web', $user->id, $here->uuid); expect($ended)->toBe(2) ->and(DB::table('sessions')->where('id', 'session-here')->exists())->toBeTrue() ->and(DB::table('sessions')->count())->toBe(1); }); it('hides a session whose framework row has expired but not yet been collected', function () { // Laravel collects expired sessions by lottery, so on a quiet installation // rows sit there for days. Listing them offers somebody a laptop they shut // last month as somewhere they are signed in right now. $user = User::factory()->create(); makeSession('web', $user->id, 'session-stale', 'Chrome auf Windows', lastActivity: now()->subMinutes((int) config('session.lifetime') + 5)->getTimestamp()); expect(app(SessionRegistry::class)->for('web', $user->id))->toHaveCount(0); }); it('lets a customer end another session from the settings page', function () { $user = User::factory()->create(); $other = makeSession('web', $user->id, 'session-there', 'Safari auf iPhone'); Livewire::actingAs($user) ->test(PortalSessions::class) ->assertSee('Safari auf iPhone') ->call('end', $other->uuid) ->assertDispatched('notify', message: __('sessions.ended')); expect(DB::table('sessions')->where('id', 'session-there')->exists())->toBeFalse(); }); it('ends the others only when the confirmation modal says so', function () { // R23: the destructive one goes through the product's own modal, which // mutates nothing and raises an event this component catches — so the // identity resolution stays at the one place that already had it. $user = User::factory()->create(); makeSession('web', $user->id, 'session-b'); makeSession('web', $user->id, 'session-c'); Livewire::actingAs($user) ->test(PortalSessions::class) ->dispatch('sessions-end-others-confirmed') ->assertDispatched('notify'); expect(DB::table('sessions')->count())->toBe(0); });