CluPilotCloud/tests/Feature/SessionListTest.php

142 lines
5.7 KiB
PHP

<?php
use App\Livewire\Sessions as PortalSessions;
use App\Models\LoginSession;
use App\Models\User;
use App\Models\UserDevice;
use App\Services\Devices\SessionRegistry;
use Illuminate\Support\Facades\DB;
use Livewire\Livewire;
/**
* Seeing where an account is signed in, and ending one of those places.
*
* The scoping tests are the point. Operators and customers are two guards and
* two tables (R21), so operator 1 and customer 1 share an integer and nothing
* else — an unscoped query here would let either of them sign the other out,
* and it would look exactly like a working feature while doing it.
*/
function makeSession(string $guard, int $id, string $sessionId, string $device = 'Chrome auf macOS', ?int $lastActivity = null): LoginSession
{
$deviceRow = UserDevice::create([
'guard' => $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);
});