first commit

This commit is contained in:
2026-10-09 08:21:06 +02:00
parent db7456df87
commit d259c62304
3761 changed files with 896046 additions and 0 deletions
+91
View File
@@ -0,0 +1,91 @@
<?php
use Carbon\Carbon;
use Facades\Tests\Setup\UserFactory;
use PragmaRX\Google2FA\Google2FA;
use Tests\UpdatesSettings;
uses(UpdatesSettings::class);
beforeEach(function () {
$this->artisan('db:seed');
});
it('sends an unverified user to the verification notice', function () {
$user = UserFactory::user()->unverified()->create(['password' => 'password']);
visit('/login')
->fill('#username', $user->email)
->fill('#password', 'password')
->click('Log In')
->assertSee('Verify Your Email Address')
->assertSee('Before proceeding, please check your email for a verification link.')
->assertPathIs('/email/verify');
});
it('resends the verification link', function () {
$user = UserFactory::user()->unverified()->create(['password' => 'password']);
visit('/login')
->fill('#username', $user->email)
->fill('#password', 'password')
->click('Log In')
->assertSee('Verify Your Email Address')
->click('click here to request another')
->assertSee('A fresh verification link has been sent to your email address.');
});
it('renders the approval notice', function () {
browserLogin();
visit('/approval-notice')
->assertNoJavaScriptErrors()
->assertSee('Waiting Approval')
->assertSee('Your account is waiting approval from administrators.');
});
it('asks for a two-factor token and then completes the login', function () {
$this->setSettings(['2fa.enabled' => true, 'throttle_enabled' => false]);
$google2fa = new Google2FA;
// two_factor_secret is not fillable, so it is assigned the way the server
// suite assigns it rather than through the factory.
$user = UserFactory::admin()->create(['password' => 'password']);
$user->two_factor_secret = encrypt($google2fa->generateSecretKey());
$user->two_factor_confirmed_at = Carbon::now();
$user->save();
visit('/login')
->fill('#username', $user->email)
->fill('#password', 'password')
->click('Log In')
->assertSee('Two-Factor Authentication')
->assertPathIs('/auth/two-factor-authentication')
->fill('#code', $google2fa->getCurrentOtp(decrypt($user->two_factor_secret)))
->click('Validate')
->assertSee('Dashboard')
->assertPathIs('/');
$this->assertAuthenticatedAs($user->fresh());
});
it('rejects an invalid two-factor token', function () {
$this->setSettings(['2fa.enabled' => true, 'throttle_enabled' => false]);
$user = UserFactory::admin()->create(['password' => 'password']);
$user->two_factor_secret = encrypt((new Google2FA)->generateSecretKey());
$user->two_factor_confirmed_at = Carbon::now();
$user->save();
visit('/login')
->fill('#username', $user->email)
->fill('#password', 'password')
->click('Log In')
->assertSee('Two-Factor Authentication')
->fill('#code', '000000')
->click('Validate')
->assertSee('Invalid 2FA token.');
$this->assertGuest();
});
+77
View File
@@ -0,0 +1,77 @@
<?php
use Facades\Tests\Setup\UserFactory;
use Tests\UpdatesSettings;
uses(UpdatesSettings::class);
beforeEach(function () {
$this->artisan('db:seed');
});
it('renders the login form', function () {
visit('/login')
->assertNoJavaScriptErrors()
->assertSee('Login')
->assertPresent('#username')
->assertPresent('#password');
});
it('signs a user in and lands on the dashboard', function () {
$user = UserFactory::admin()->create(['password' => 'password']);
visit('/login')
->fill('#username', $user->email)
->fill('#password', 'password')
->click('Log In')
->assertPathIs('/')
->assertSee('Dashboard');
$this->assertAuthenticatedAs($user);
});
it('keeps the user on the login page after wrong credentials', function () {
$user = UserFactory::admin()->create(['password' => 'password']);
visit('/login')
->fill('#username', $user->email)
->fill('#password', 'wrong-password')
->click('Log In')
->assertPathIs('/login')
->assertSee('These credentials do not match our records.');
$this->assertGuest();
});
it('sends an unauthenticated visitor to the login page', function () {
visit('/')->assertPathIs('/login')->assertSee('Login');
$this->assertGuest();
});
it('logs the user back out', function () {
browserLogin();
visit('/logout')->assertPathIs('/login')->assertSee('Login');
$this->assertGuest();
});
it('reveals the password when the toggle is pressed', function () {
visit('/login')
->assertAttribute('#password', 'type', 'password')
->click('[aria-label="Show password"]')
->assertAttribute('#password', 'type', 'text');
});
it('hides the password reset link when the setting is disabled', function () {
$this->setSettings(['forgot_password' => false]);
visit('/login')->assertSee('Login')->assertDontSee('I forgot my password');
});
it('shows the password reset link when the setting is enabled', function () {
$this->setSettings(['forgot_password' => true]);
visit('/login')->assertSee('I forgot my password');
});
+44
View File
@@ -0,0 +1,44 @@
<?php
use App\Models\User;
use Illuminate\Support\Facades\Password;
use Tests\UpdatesSettings;
uses(UpdatesSettings::class);
beforeEach(function () {
$this->artisan('db:seed');
$this->setSettings(['forgot_password' => true]);
});
it('requests a reset link', function () {
User::factory()->create(['email' => 'forgetful@vanguard.test']);
visit('/password/reset')
->assertNoJavaScriptErrors()
->assertSee('Forgot Your Password?')
->fill('#email', 'forgetful@vanguard.test')
->click('Reset Password')
->assertSee('you should receive a password reset link shortly');
});
it('resets the password with a valid token', function () {
$user = User::factory()->create(['email' => 'forgetful@vanguard.test']);
$token = Password::broker()->createToken($user);
visit("/password/reset/{$token}?email={$user->email}")
->assertSee('Reset Your Password')
->fill('#password', 'brand-new-password')
->fill('#password_confirmation', 'brand-new-password')
->click('Update Password')
->assertPathIs('/')
->assertSee('Dashboard');
$this->assertAuthenticatedAs($user->fresh());
});
it('returns a 404 when password reset is disabled', function () {
$this->setSettings(['forgot_password' => false]);
visit('/password/reset')->assertSee('404');
});
+62
View File
@@ -0,0 +1,62 @@
<?php
use App\Models\User;
use Tests\UpdatesSettings;
uses(UpdatesSettings::class);
/*
* reCAPTCHA is disabled the way the server suite disables it: the widget calls
* out to Google, which a test must never depend on.
*/
beforeEach(function () {
$this->artisan('db:seed');
$this->setSettings([
'reg_enabled' => true,
'registration.captcha.enabled' => false,
'tos' => false,
'approval.enabled' => false,
'reg_email_confirmation' => false,
]);
});
it('renders the registration form', function () {
visit('/register')
->assertNoJavaScriptErrors()
->assertSee('Register')
->assertPresent('#email')
->assertPresent('#username')
->assertPresent('#password');
});
it('registers a new user', function () {
visit('/register')
->fill('#email', 'newcomer@vanguard.test')
->fill('#username', 'newcomer')
->fill('#password', 'sup3r-secret')
->fill('#password_confirmation', 'sup3r-secret')
// The page heading is also "Register", and a text locator would match it
// instead of the button.
->click('button[type="submit"]')
// assertSee waits for the page to settle; assertPathIs does not, so a
// content assertion has to come first after any navigation.
->assertSee('Dashboard')
->assertPathIs('/');
expect(User::where('email', 'newcomer@vanguard.test')->exists())->toBeTrue();
});
it('opens the terms of service dialog', function () {
$this->setSettings(['tos' => true]);
visit('/register')
->assertSee('I accept')
->click('Terms of Service')
->assertSee('Close');
});
it('returns a 404 when registration is disabled', function () {
$this->setSettings(['reg_enabled' => false]);
visit('/register')->assertSee('404');
});
+28
View File
@@ -0,0 +1,28 @@
<?php
use Facades\Tests\Setup\UserFactory;
beforeEach(function () {
$this->artisan('db:seed');
});
it('shows the user management widgets to an admin', function () {
browserLogin();
visit('/')
->assertNoJavaScriptErrors()
->assertSee('Dashboard')
->assertSee('Total Users')
->assertSee('New Users')
->assertSee('Registration History')
->assertSee('Latest Registrations');
});
it('hides the user management widgets from a role without the permission', function () {
browserLogin(UserFactory::user()->create(['password' => 'password']));
visit('/')
->assertSee('Dashboard')
->assertDontSee('Total Users')
->assertDontSee('Registration History');
});
+32
View File
@@ -0,0 +1,32 @@
<?php
beforeEach(function () {
$this->artisan('db:seed');
});
it('renders the login page in a requested locale', function () {
// SetLocale reads ?lang= and stores the choice in the session, so the
// switch is a full page visit rather than an Inertia one.
visit('/login?lang=de')
->assertSee('Anmelden')
->assertAttribute('#username', 'placeholder', 'eMail oder Benutzername');
});
it('keeps the chosen locale for the next request', function () {
visit('/login?lang=de')->assertSee('Anmelden');
visit('/login')->assertSee('Anmelden');
});
it('translates the application chrome once signed in', function () {
browserLogin();
visit('/?lang=de')
->assertNoJavaScriptErrors()
->assertSee('Benutzer')
->assertSee('Gesamte Benutzeranzahl');
});
it('falls back to english for an unknown locale', function () {
visit('/login?lang=xx')->assertSee('Login');
});
+52
View File
@@ -0,0 +1,52 @@
<?php
beforeEach(function () {
$this->artisan('db:seed');
});
it('reaches each area from the sidebar', function () {
browserLogin();
visit('/')
->click('Users')
->assertSee('Registration Date')
->assertPathIs('/users');
visit('/roles')->assertSee('Add Role')->assertSee('Roles');
visit('/permissions')->assertSee('Add Permission');
visit('/settings')->assertSee('General Settings');
});
it('opens the profile from the navbar', function () {
browserLogin();
visit('/profile')
->assertNoJavaScriptErrors()
->assertSee('My Profile')
->assertSee('User Details')
->assertSee('Login Details');
});
it('slims the header down on a phone', function () {
browserLogin();
// The sidebar is off canvas below md, so the navbar drops the wordmark and
// the breadcrumbs to leave room for the heading and the controls. Both
// elements stay in the DOM and are hidden by CSS, so these assert on
// visibility rather than presence.
visit('/users')->on()->iPhone15()
->assertSee('Registration Date')
->assertVisible('img[src="/assets/img/logo-no-text.png"]')
->assertMissing('img[src="/assets/img/logo.png"]')
->assertMissing('[aria-label="Breadcrumb"]');
});
it('keeps the wordmark and breadcrumbs on a desktop', function () {
browserLogin();
visit('/users')->on()->desktop()
->assertSee('Registration Date')
->assertVisible('img[src="/assets/img/logo.png"]')
->assertMissing('img[src="/assets/img/logo-no-text.png"]')
->assertVisible('[aria-label="Breadcrumb"]');
});
+102
View File
@@ -0,0 +1,102 @@
<?php
use App\Models\Permission;
use App\Models\Role;
beforeEach(function () {
$this->artisan('db:seed');
});
it('lists the permission matrix', function () {
browserLogin();
visit('/permissions')
->assertNoJavaScriptErrors()
->assertSee('Permissions')
->assertSee('Add Permission')
->assertSee('Save Permissions')
// A column header per role, and a row per permission.
->assertSee('Admin')
->assertSee('Manage Users')
->assertSee('Manage Roles');
});
it('creates a permission', function () {
browserLogin();
visit('/permissions/create')
->assertSee('Create New Permission')
->assertSee('Permission Details')
->fill('#name', 'reports.view')
->fill('#display_name', 'View Reports')
->fill('#description', 'Grants access to the reports.')
->click('Create Permission')
->assertSee('Permission created successfully.');
expect(Permission::where('name', 'reports.view')->exists())->toBeTrue();
});
it('reflects the permissions a role already holds', function () {
browserLogin();
$role = Role::factory()->create(['display_name' => 'Auditor']);
$permission = Permission::factory()->create(['display_name' => 'Audit Everything']);
$role->permissions()->attach($permission->id);
// The checkbox is a Reka CheckboxRoot, which renders a button carrying
// aria-checked rather than a native input.
visit('/permissions')
->assertSee('Audit Everything')
->assertAttribute("#cb-{$role->id}-{$permission->id}", 'aria-checked', 'true');
});
it('centres each checkbox in its column', function () {
browserLogin();
$role = Role::factory()->create(['display_name' => 'Auditor']);
$permission = Permission::factory()->create(['display_name' => 'Audit Everything']);
// Checkbox renders a flex box, which the cell's text-center cannot reach,
// so the centring is measured rather than assumed.
$centred = <<<JS
function () {
const box = document.querySelector('#cb-{$role->id}-{$permission->id}');
const cell = box.closest('td').getBoundingClientRect();
const self = box.getBoundingClientRect();
return Math.abs((self.left + self.right) / 2 - (cell.left + cell.right) / 2) < 1;
}
JS;
visit('/permissions')
->assertSee('Audit Everything')
->assertScript($centred);
});
it('saves a change to the matrix', function () {
browserLogin();
$role = Role::factory()->create(['display_name' => 'Auditor']);
$permission = Permission::factory()->create(['display_name' => 'Audit Everything']);
visit('/permissions')
->assertAttribute("#cb-{$role->id}-{$permission->id}", 'aria-checked', 'false')
->click("#cb-{$role->id}-{$permission->id}")
->click('Save Permissions')
->assertSee('Permissions saved successfully.');
expect($role->permissions()->pluck('id')->all())->toContain($permission->id);
});
it('asks for confirmation before deleting a permission', function () {
browserLogin();
Permission::factory()->create(['display_name' => 'Throwaway Permission']);
// The seeded permissions are not removable, so only this one renders the
// delete control.
visit('/permissions')
->assertSee('Throwaway Permission')
->click('[aria-label="Delete Permission"]')
->assertSee('Are you sure that you want to delete this permission?');
});
+60
View File
@@ -0,0 +1,60 @@
<?php
use Vanguard\Announcements\Database\Seeders\AnnouncementsDatabaseSeeder;
/*
* The two official plugins ship their own compiled bundles to
* public/vendor/plugins and register their pages against window.Vanguard at
* runtime. A page that renders here proves the bundle loaded, resolved against
* the host's Vue instance and found the host UI kit on that runtime.
*/
beforeEach(function () {
$this->artisan('db:seed');
// announcements.manage lives in the plugin's own seeder, which the host
// seeder does not run. Without it the admin role is denied these routes.
$this->artisan('db:seed', ['--class' => AnnouncementsDatabaseSeeder::class]);
});
it('renders the announcements page from the plugin bundle', function () {
browserLogin();
visit('/announcements')
->assertNoJavaScriptErrors()
->assertSee('Announcements')
->assertSee('Create Announcement')
->assertSee('Creator')
->assertSee('Created At');
});
it('renders the announcement form from the plugin bundle', function () {
browserLogin();
visit('/announcements/create')
->assertNoJavaScriptErrors()
->assertSee('New Announcement')
->assertSee('Title')
->assertSee('Body')
->assertPresent('#title');
});
it('renders the activity log page from the plugin bundle', function () {
browserLogin();
visit('/activity')
->assertNoJavaScriptErrors()
->assertSee('Activity Log')
->assertSee('IP Address')
->assertSee('Log Time')
->assertSee('Search for Action');
});
it('renders the signed in users own activity log', function () {
browserLogin();
visit('/profile/activity')
->assertNoJavaScriptErrors()
->assertSee('Activity Log')
->assertSee('Message');
});
+72
View File
@@ -0,0 +1,72 @@
<?php
use Tests\UpdatesSettings;
uses(UpdatesSettings::class);
beforeEach(function () {
$this->artisan('db:seed');
});
it('renders the profile page with its cards', function () {
browserLogin();
visit('/profile')
->assertNoJavaScriptErrors()
->assertSee('My Profile')
->assertSee('User Details')
->assertSee('Login Details')
->assertSee('Change Photo');
});
it('updates the profile details', function () {
$user = browserLogin();
visit('/profile')
->fill('#first_name', 'Grace')
->fill('#last_name', 'Hopper')
->click('Update Details')
->assertSee('Profile updated successfully.');
expect($user->fresh()->first_name)->toBe('Grace');
expect($user->fresh()->last_name)->toBe('Hopper');
});
it('keeps role and status read only on the profile', function () {
browserLogin();
visit('/profile')
->assertSee('Role')
->assertAttribute('#role_id', 'disabled', '')
->assertAttribute('#status', 'disabled', '');
});
it('changes the password from the login details tab', function () {
$user = browserLogin();
// The inactive tab panel is unmounted, so the two "Update Details" buttons
// never coexist and the label stays unambiguous.
visit('/profile')
->click('Login Details')
->assertSee('New Password')
->fill('#password', 'a-brand-new-password')
->fill('#password_confirmation', 'a-brand-new-password')
->click('Update Details')
->assertSee('Login details updated successfully.');
expect(Hash::check('a-brand-new-password', $user->fresh()->password))->toBeTrue();
});
it('shows the two-factor tab only when the feature is enabled', function () {
$this->setSettings(['2fa.enabled' => false]);
browserLogin();
visit('/profile')
->assertSee('Login Details')
->assertDontSee('Two-Factor Authentication');
$this->setSettings(['2fa.enabled' => true]);
visit('/profile')->assertSee('Two-Factor Authentication');
});
+61
View File
@@ -0,0 +1,61 @@
<?php
use App\Models\Role;
beforeEach(function () {
$this->artisan('db:seed');
});
it('lists the seeded roles', function () {
browserLogin();
visit('/roles')
->assertNoJavaScriptErrors()
->assertSee('Roles')
->assertSee('Display Name')
->assertSee('# of users with this role')
->assertSee('Admin')
->assertSee('User');
});
it('creates a role', function () {
browserLogin();
visit('/roles/create')
->assertSee('Create New Role')
->assertSee('Role Details')
->fill('#name', 'Editor')
->fill('#display_name', 'Content Editor')
->fill('#description', 'Edits the content.')
->click('Create Role')
->assertSee('Role created successfully.');
expect(Role::where('name', 'Editor')->exists())->toBeTrue();
});
it('edits a role', function () {
browserLogin();
$role = Role::factory()->create(['name' => 'Reviewer', 'display_name' => 'Reviewer']);
visit("/roles/{$role->id}/edit")
->assertSee('Role Details')
->fill('#display_name', 'Peer Reviewer')
->click('Update Role')
->assertSee('Role updated successfully.');
expect($role->fresh()->display_name)->toBe('Peer Reviewer');
});
it('asks for confirmation before deleting a role', function () {
browserLogin();
Role::factory()->create(['name' => 'Temporary', 'display_name' => 'Temporary']);
// Only removable roles render the delete control, so the seeded roles do
// not add a second match for this locator.
visit('/roles')
->assertSee('Temporary')
->click('[aria-label="Delete Role"]')
->assertSee('Are you sure that you want to delete this role?');
});
+94
View File
@@ -0,0 +1,94 @@
<?php
use Tests\UpdatesSettings;
uses(UpdatesSettings::class);
beforeEach(function () {
$this->artisan('db:seed');
});
it('updates the application name', function () {
browserLogin();
visit('/settings')
->assertNoJavaScriptErrors()
->assertSee('General Settings')
->fill('#app_name', 'Vanguard Renamed')
->click('Update')
->assertSee('Settings updated successfully.');
expect(setting('app_name'))->toBe('Vanguard Renamed');
});
it('renders both authentication settings tabs', function () {
browserLogin();
visit('/settings/auth')
->assertNoJavaScriptErrors()
->assertSee('Authentication')
->assertSee('Authentication Throttling')
->assertSee('Two-Factor Authentication')
->assertSee('Force Password Change On Next Login')
->click('Registration')
->assertSee('Allow Registration')
->assertSee('Google reCAPTCHA');
});
it('saves the general authentication settings', function () {
browserLogin();
// Two cards on this tab each end in an Update button, so the submit is
// scoped to the form that owns the field being changed.
visit('/settings/auth')
->fill('#login_reset_token_lifetime', '45')
->fill('#max_active_sessions', '3')
->click('form:has(#login_reset_token_lifetime) button[type="submit"]')
->assertSee('Settings updated successfully.');
expect(setting('login_reset_token_lifetime'))->toBe('45');
expect(setting('max_active_sessions'))->toBe('3');
});
it('toggles a switch and saves the throttling settings', function () {
$this->setSettings(['throttle_enabled' => 0]);
browserLogin();
visit('/settings/auth')
->assertAttribute('#switch-throttle', 'aria-checked', 'false')
->click('#switch-throttle')
->click('form:has(#throttle_attempts) button[type="submit"]')
->assertSee('Settings updated successfully.');
expect((bool) setting('throttle_enabled'))->toBeTrue();
});
it('turns a feature toggle card on', function () {
$this->setSettings(['2fa.enabled' => 0]);
browserLogin();
visit('/settings/auth')
->assertSee('Two-Factor Authentication')
->click('div:has(> h5:text-is("Two-Factor Authentication")) button')
->assertSee('Two-Factor Authentication enabled successfully.');
expect((bool) setting('2fa.enabled'))->toBeTrue();
});
it('updates the notification settings', function () {
$this->setSettings(['notifications_signup_email' => 0]);
browserLogin();
visit('/settings/notifications')
->assertNoJavaScriptErrors()
->assertSee('Notification Settings')
->assertSee('Sign-Up Notification')
->click('#switch-signup-email')
->click('Update')
->assertSee('Settings updated successfully.');
expect((bool) setting('notifications_signup_email'))->toBeTrue();
});
+61
View File
@@ -0,0 +1,61 @@
<?php
use App\Models\User;
beforeEach(function () {
$this->artisan('db:seed');
});
it('creates a user', function () {
browserLogin();
visit('/users/create')
->assertNoJavaScriptErrors()
->assertSee('Create New User')
->assertSee('User Details')
->fill('#first_name', 'Ada')
->fill('#last_name', 'Lovelace')
->fill('#email', 'ada@vanguard.test')
->fill('#password', 'analytical-engine')
->fill('#password_confirmation', 'analytical-engine')
->click('button[type="submit"]')
->assertSee('ada@vanguard.test');
expect(User::where('email', 'ada@vanguard.test')->exists())->toBeTrue();
});
it('edits a user', function () {
browserLogin();
$user = User::factory()->create(['first_name' => 'Before']);
visit("/users/{$user->id}/edit")
->assertSee('User Details')
->fill('#first_name', 'After')
->click('button[type="submit"]')
->assertSee('User updated successfully.');
expect($user->fresh()->first_name)->toBe('After');
});
it('shows a user', function () {
browserLogin();
$user = User::factory()->create(['email' => 'shown@vanguard.test']);
visit("/users/{$user->id}")
->assertNoJavaScriptErrors()
->assertSee('Details')
->assertSee('shown@vanguard.test');
});
it('asks for confirmation before deleting', function () {
browserLogin();
$user = User::factory()->create(['username' => 'doomed']);
visit('/users')
->assertSee('doomed')
// Every row carries a Delete User button, so the click has to be scoped
// to this user's row or Playwright refuses the ambiguous locator.
->click('tr:has-text("doomed") [aria-label="Delete User"]')
->assertSee('Are you sure that you want to delete this user?')
->assertSee('Yes, delete him!');
});
+56
View File
@@ -0,0 +1,56 @@
<?php
use App\Models\User;
use Facades\Tests\Setup\UserFactory;
beforeEach(function () {
$this->artisan('db:seed');
});
it('lists users with their details', function () {
browserLogin();
User::factory()->create(['email' => 'listed@vanguard.test', 'username' => 'listeduser']);
visit('/users')
->assertNoJavaScriptErrors()
->assertSee('Users')
->assertSee('Registration Date')
->assertSee('listed@vanguard.test')
->assertSee('listeduser');
});
it('filters the list with the search box', function () {
browserLogin();
User::factory()->create(['username' => 'findme']);
User::factory()->create(['username' => 'hidden']);
visit('/users')
->assertSee('hidden')
->fill('#search', 'findme')
->assertSee('findme')
->assertDontSee('hidden');
});
it('filters the list by status', function () {
browserLogin();
UserFactory::banned()->create(['username' => 'bannedone']);
visit('/users')
->assertSee('bannedone')
->select('#status', 'Active')
->assertDontSee('bannedone');
});
it('renders the row actions', function () {
browserLogin();
// Each row carries the same controls, so these are asserted by presence
// rather than clicked: a text or aria locator would match every row.
visit('/users')
->assertPresent('[aria-label="Action"]')
->assertPresent('[aria-label="Edit User"]')
->assertPresent('[aria-label="Delete User"]');
});
+75
View File
@@ -0,0 +1,75 @@
<?php
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
/*
* Both session pages sit behind the session.database middleware, which 404s
* unless the database driver is in use. The suite otherwise runs on the array
* driver configured in phpunit.xml.
*/
beforeEach(function () {
config(['session.driver' => 'database']);
$this->artisan('db:seed');
});
it('lists another users sessions for an admin', function () {
browserLogin();
$user = User::factory()->create(['username' => 'watched']);
insertBrowserSession($user, '10.0.0.9');
visit("/users/{$user->id}/sessions")
->assertNoJavaScriptErrors()
->assertSee('Sessions')
->assertSee('IP Address')
->assertSee('10.0.0.9');
});
it('asks for confirmation before invalidating a session', function () {
browserLogin();
$user = User::factory()->create();
insertBrowserSession($user, '10.0.0.10');
visit("/users/{$user->id}/sessions")
->assertSee('10.0.0.10')
->click('[aria-label="Invalidate Session"]')
->assertSee('Are you sure that you want to invalidate this session?')
->assertSee('Yes, proceed!');
});
it('lists the signed in users own sessions', function () {
browserLogin();
visit('/profile/sessions')
->assertNoJavaScriptErrors()
->assertSee('Sessions')
->assertSee('Last Activity');
});
/**
* Insert a session row for a user, the way the server suite fakes one.
*
* Named apart from the Feature suite's insertUserSession: Pest loads every test
* file into one process, so a repeated global function name is a fatal
* redeclare even when the group is excluded from the run.
*/
function insertBrowserSession(User $user, string $ipAddress): string
{
$id = Str::random();
DB::table('sessions')->insert([
'id' => $id,
'user_id' => $user->id,
'ip_address' => $ipAddress,
'user_agent' => fake()->userAgent,
'payload' => 'foo',
'last_activity' => Carbon::now()->timestamp,
]);
return $id;
}