first commit
This commit is contained in:
@@ -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();
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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"]');
|
||||
});
|
||||
@@ -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?');
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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?');
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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!');
|
||||
});
|
||||
@@ -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"]');
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user