181 lines
6.7 KiB
PHP
181 lines
6.7 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Livewire\Modals\ConfirmAction;
|
|
use App\Livewire\Settings\Sessions;
|
|
use App\Models\AuditEvent;
|
|
use App\Models\User;
|
|
use App\Services\SessionService;
|
|
use App\Support\Confirm\ConfirmToken;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Session;
|
|
use Livewire\Livewire;
|
|
use Tests\TestCase;
|
|
|
|
class SessionManagementTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
// phpunit.xml pins SESSION_DRIVER=array for the suite; the session-management
|
|
// feature only exists with the database driver (rows in the `sessions` table).
|
|
config(['session.driver' => 'database']);
|
|
}
|
|
|
|
/** Insert a raw session row for $user; returns the generated id. */
|
|
private function seedSession(User $user, string $id, ?string $ip = '203.0.113.1', ?string $ua = 'Mozilla/5.0 Chrome', int $ago = 0): string
|
|
{
|
|
DB::table('sessions')->insert([
|
|
'id' => $id,
|
|
'user_id' => $user->id,
|
|
'ip_address' => $ip,
|
|
'user_agent' => $ua,
|
|
'payload' => base64_encode(serialize([])),
|
|
'last_activity' => now()->subSeconds($ago)->getTimestamp(),
|
|
]);
|
|
|
|
return $id;
|
|
}
|
|
|
|
public function test_for_user_returns_rows_with_current_flagged(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
$other = User::factory()->create();
|
|
|
|
$currentId = Session::getId();
|
|
$this->seedSession($user, $currentId, ago: 0);
|
|
$this->seedSession($user, 'sess-old', ago: 600);
|
|
$this->seedSession($other, 'sess-other'); // must NOT appear
|
|
|
|
$rows = app(SessionService::class)->forUser($user);
|
|
|
|
$this->assertCount(2, $rows);
|
|
// Newest activity first.
|
|
$this->assertSame($currentId, $rows[0]['id']);
|
|
$this->assertTrue($rows[0]['isCurrent']);
|
|
$this->assertSame('sess-old', $rows[1]['id']);
|
|
$this->assertFalse($rows[1]['isCurrent']);
|
|
$this->assertSame('203.0.113.1', $rows[0]['ip']);
|
|
$this->assertStringContainsString('Chrome', $rows[0]['userAgentShort']);
|
|
$this->assertTrue($rows[0]['lastActivity']->isToday());
|
|
}
|
|
|
|
public function test_logout_other_devices_deletes_others_keeps_current(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
$currentId = Session::getId();
|
|
$this->seedSession($user, $currentId);
|
|
$this->seedSession($user, 'sess-b');
|
|
$this->seedSession($user, 'sess-c');
|
|
|
|
app(SessionService::class)->logoutOtherDevices($user);
|
|
|
|
$this->assertDatabaseHas('sessions', ['id' => $currentId]);
|
|
$this->assertDatabaseMissing('sessions', ['id' => 'sess-b']);
|
|
$this->assertDatabaseMissing('sessions', ['id' => 'sess-c']);
|
|
}
|
|
|
|
public function test_logout_user_everywhere_deletes_all_and_rotates_token(): void
|
|
{
|
|
$user = User::factory()->create(['remember_token' => 'original-token']);
|
|
$this->seedSession($user, Session::getId());
|
|
$this->seedSession($user, 'sess-b');
|
|
|
|
app(SessionService::class)->logoutUserEverywhere($user);
|
|
|
|
$this->assertSame(0, DB::table('sessions')->where('user_id', $user->id)->count());
|
|
$this->assertNotSame('original-token', $user->fresh()->remember_token);
|
|
$this->assertNotNull($user->fresh()->remember_token);
|
|
}
|
|
|
|
public function test_logout_everyone_truncates_and_rotates_all_tokens(): void
|
|
{
|
|
$a = User::factory()->create(['remember_token' => 'token-a']);
|
|
$b = User::factory()->create(['remember_token' => 'token-b']);
|
|
$this->seedSession($a, 'sess-a');
|
|
$this->seedSession($b, 'sess-b');
|
|
|
|
app(SessionService::class)->logoutEveryone();
|
|
|
|
$this->assertSame(0, DB::table('sessions')->count());
|
|
$this->assertNotSame('token-a', $a->fresh()->remember_token);
|
|
$this->assertNotSame('token-b', $b->fresh()->remember_token);
|
|
}
|
|
|
|
public function test_component_lists_the_users_sessions(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
$this->seedSession($user, Session::getId());
|
|
|
|
Livewire::actingAs($user)->test(Sessions::class)
|
|
->assertOk()
|
|
->assertViewHas('sessions', fn ($sessions) => count($sessions) === 1);
|
|
}
|
|
|
|
public function test_logout_others_action_confirms_then_deletes(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
$current = Session::getId();
|
|
$this->seedSession($user, $current);
|
|
$this->seedSession($user, 'sess-other');
|
|
|
|
// The button opens the R5 confirm modal carrying the apply event + audit descriptor.
|
|
Livewire::actingAs($user)->test(Sessions::class)
|
|
->call('confirmLogoutOthers')
|
|
->assertDispatched('openModal');
|
|
|
|
// The confirm modal re-dispatches the apply event with the signed token; simulate that.
|
|
$this->actingAs($user);
|
|
$token = ConfirmToken::issue('sessionsLogoutOthers');
|
|
ConfirmToken::confirm($token);
|
|
Livewire::actingAs($user)->test(Sessions::class)
|
|
->call('logoutOthers', $token);
|
|
|
|
$this->assertDatabaseHas('sessions', ['id' => $current]);
|
|
$this->assertDatabaseMissing('sessions', ['id' => 'sess-other']);
|
|
}
|
|
|
|
public function test_logout_all_action_truncates_and_redirects_to_login(): void
|
|
{
|
|
$user = User::factory()->create(['remember_token' => 'tok']);
|
|
$this->seedSession($user, Session::getId());
|
|
|
|
Livewire::actingAs($user)->test(Sessions::class)
|
|
->call('confirmLogoutAll')
|
|
->assertDispatched('openModal');
|
|
|
|
$this->actingAs($user);
|
|
$token = ConfirmToken::issue('sessionsLogoutAll');
|
|
ConfirmToken::confirm($token);
|
|
Livewire::actingAs($user)->test(Sessions::class)
|
|
->call('logoutAll', $token)
|
|
->assertRedirect(route('login'));
|
|
|
|
$this->assertSame(0, DB::table('sessions')->count());
|
|
$this->assertNotSame('tok', $user->fresh()->remember_token);
|
|
}
|
|
|
|
public function test_confirm_modal_writes_audit_row_on_confirm(): void
|
|
{
|
|
// The generic confirm modal persists the audit row from the SEALED descriptor
|
|
// in the signed token (mirrors Settings\Security). Exercise it directly.
|
|
$user = User::factory()->create();
|
|
$this->actingAs($user);
|
|
|
|
$token = ConfirmToken::issue('sessionsLogoutOthers', [], 'session.logout_others', $user->email);
|
|
|
|
Livewire::actingAs($user)->test(ConfirmAction::class, ['token' => $token])->call('confirm');
|
|
|
|
$this->assertDatabaseHas('audit_events', [
|
|
'action' => 'session.logout_others',
|
|
'target' => $user->email,
|
|
]);
|
|
$this->assertSame(1, AuditEvent::where('action', 'session.logout_others')->count());
|
|
}
|
|
}
|