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;
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use App\Support\Enum\UserStatus;
|
||||
use Carbon\Carbon;
|
||||
|
||||
test('login', function () {
|
||||
$credentials = defaultCredentials();
|
||||
|
||||
$user = User::factory()->create($credentials);
|
||||
|
||||
$credentials['device_name'] = 'test';
|
||||
|
||||
$this->postJson('/api/login', $credentials)->assertOk();
|
||||
|
||||
$this->assertNotNull($user->tokens()->first());
|
||||
});
|
||||
|
||||
test('last login timestamp is updated after login', function () {
|
||||
$credentials = defaultCredentials();
|
||||
|
||||
$now = Carbon::now();
|
||||
|
||||
Carbon::setTestNow($now);
|
||||
|
||||
$user = User::factory()->create($credentials);
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'id' => $user->id,
|
||||
'last_login' => null,
|
||||
]);
|
||||
|
||||
$credentials['device_name'] = 'test';
|
||||
|
||||
$this->postJson('/api/login', $credentials)->assertOk();
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'id' => $user->id,
|
||||
'last_login' => $now,
|
||||
]);
|
||||
});
|
||||
|
||||
test('login with invalid credentials', function () {
|
||||
$credentials = defaultCredentials();
|
||||
|
||||
User::factory()->create($credentials);
|
||||
|
||||
$this->postJson('/api/login', [
|
||||
'username' => 'foo',
|
||||
'password' => 'invalid',
|
||||
'device_name' => 'test',
|
||||
])->assertJsonValidationErrors('username');
|
||||
});
|
||||
|
||||
test('login when credentials are not provided', function () {
|
||||
$this->postJson('/api/login')
|
||||
->assertStatus(422)
|
||||
->assertJsonFragment([
|
||||
'username' => [
|
||||
trans('validation.required', ['attribute' => 'username']),
|
||||
],
|
||||
'password' => [
|
||||
trans('validation.required', ['attribute' => 'password']),
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
test('banned user cannot log in', function () {
|
||||
$credentials = defaultCredentials();
|
||||
|
||||
$user = User::factory()->create(array_merge($credentials, [
|
||||
'status' => UserStatus::BANNED,
|
||||
]));
|
||||
|
||||
$credentials['device_name'] = 'test';
|
||||
|
||||
$this->postJson('/api/login', $credentials)
|
||||
->assertStatus(401)
|
||||
->assertJson([
|
||||
'message' => trans('auth.banned'),
|
||||
]);
|
||||
|
||||
$this->assertDatabaseMissing('personal_access_tokens', ['tokenable_id' => $user->id]);
|
||||
});
|
||||
|
||||
test('unconfirmed user can log in', function () {
|
||||
$credentials = defaultCredentials();
|
||||
|
||||
User::factory()->create(array_merge($credentials, [
|
||||
'status' => UserStatus::UNCONFIRMED,
|
||||
]));
|
||||
|
||||
$credentials['device_name'] = 'test';
|
||||
|
||||
$this->postJson('/api/login', $credentials)->assertOk();
|
||||
});
|
||||
|
||||
test('logout', function () {
|
||||
$credentials = defaultCredentials();
|
||||
|
||||
Carbon::setTestNow(Carbon::now());
|
||||
|
||||
$user = User::factory()->create($credentials);
|
||||
|
||||
$credentials['device_name'] = 'test';
|
||||
|
||||
$response = $this->postJson('/api/login', $credentials);
|
||||
|
||||
$token = $user->tokens()->first();
|
||||
|
||||
auth('sanctum')->user()->withAccessToken($token);
|
||||
|
||||
$this->postJson('/api/logout', [], [
|
||||
'Authorization' => "Bearer {$response->original['token']}",
|
||||
]);
|
||||
|
||||
$this->assertDatabaseMissing('personal_access_tokens', ['id' => $token->id]);
|
||||
});
|
||||
|
||||
test('logout if token is not provided', function () {
|
||||
$this->postJson('/api/logout')->assertStatus(401);
|
||||
});
|
||||
|
||||
function defaultCredentials(array $override = [])
|
||||
{
|
||||
return array_merge([
|
||||
'username' => 'foo',
|
||||
'password' => 'bar',
|
||||
], $override);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
use App\Mail\ResetPassword;
|
||||
use App\Models\User;
|
||||
|
||||
test('test send password reminder', function () {
|
||||
$this->setSettings(['forgot_password' => true]);
|
||||
|
||||
Mail::fake();
|
||||
|
||||
$user = User::factory()->create(['email' => 'test@test.com']);
|
||||
|
||||
$this->postJson('api/password/remind', ['email' => 'test@test.com'])
|
||||
->assertOk();
|
||||
|
||||
Mail::assertQueued(ResetPassword::class, function ($mail) use ($user) {
|
||||
return $mail->hasTo($user->email);
|
||||
});
|
||||
});
|
||||
|
||||
test('test password reminder with wrong email', function () {
|
||||
$this->setSettings(['forgot_password' => true]);
|
||||
|
||||
$this->postJson('api/password/remind', ['email' => 'test@test.com'])
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrors('email');
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
test('test password reset', function () {
|
||||
$this->setSettings(['forgot_password' => true]);
|
||||
|
||||
$user = User::factory()->create(['email' => 'test@test.com']);
|
||||
|
||||
$token = createNewToken();
|
||||
|
||||
DB::table('password_resets')->insert([
|
||||
'email' => $user->email,
|
||||
'token' => Hash::make($token),
|
||||
'created_at' => Carbon::now(),
|
||||
]);
|
||||
|
||||
resetPassword($token, $user->email)
|
||||
->assertOk();
|
||||
|
||||
$this->assertTrue(Hash::check('123123123', $user->fresh()->password));
|
||||
});
|
||||
|
||||
test('test password reset with expired token', function () {
|
||||
$this->setSettings(['forgot_password' => true]);
|
||||
|
||||
$user = User::factory()->create(['email' => 'test@test.com']);
|
||||
|
||||
$token = createNewToken();
|
||||
|
||||
DB::table('password_resets')->insert([
|
||||
'email' => $user->email,
|
||||
'token' => Hash::make($token),
|
||||
'created_at' => Carbon::now()->subHours(2),
|
||||
]);
|
||||
|
||||
resetPassword($token, $user->email)
|
||||
->assertJson([
|
||||
'message' => 'This password reset token is invalid.',
|
||||
]);
|
||||
});
|
||||
|
||||
test('test password reset with invalid email', function () {
|
||||
$this->setSettings(['forgot_password' => true]);
|
||||
|
||||
$user = User::factory()->create(['email' => 'test@test.com']);
|
||||
|
||||
$token = createNewToken();
|
||||
|
||||
DB::table('password_resets')->insert([
|
||||
'email' => $user->email,
|
||||
'token' => Hash::make($token),
|
||||
'created_at' => Carbon::now(),
|
||||
]);
|
||||
|
||||
resetPassword($token, 'foo@bar.com')
|
||||
->assertOk()
|
||||
->assertJson(['success' => true]);
|
||||
});
|
||||
|
||||
function resetPassword($token, $email)
|
||||
{
|
||||
return test()->postJson('api/password/reset', [
|
||||
'token' => $token,
|
||||
'email' => $email,
|
||||
'password' => '123123123',
|
||||
'password_confirmation' => '123123123',
|
||||
]);
|
||||
}
|
||||
|
||||
function createNewToken()
|
||||
{
|
||||
$key = config('app.key');
|
||||
|
||||
if (Str::startsWith($key, 'base64:')) {
|
||||
$key = base64_decode(substr($key, 7));
|
||||
}
|
||||
|
||||
return hash_hmac('sha256', Str::random(40), $key);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Notifications\VerifyEmail;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
|
||||
test('test register user when registration is disabled', function () {
|
||||
$this->setSettings(['reg_enabled' => false]);
|
||||
|
||||
$this->postJson('api/register')->assertStatus(404);
|
||||
});
|
||||
|
||||
test('test register user', function () {
|
||||
$this->setSettings([
|
||||
'reg_enabled' => true,
|
||||
'reg_email_confirmation' => false,
|
||||
'registration.captcha.enabled' => false,
|
||||
'tos' => false,
|
||||
]);
|
||||
|
||||
$data = [
|
||||
'email' => 'john.doe@test.com',
|
||||
'username' => 'john.doe',
|
||||
'password' => '123123123',
|
||||
'password_confirmation' => '123123123',
|
||||
];
|
||||
|
||||
$expected = Arr::except($data, ['password', 'password_confirmation']);
|
||||
|
||||
$this->postJson('/api/register', $data)
|
||||
->assertStatus(201)
|
||||
->assertJson([
|
||||
'requires_email_confirmation' => false,
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('users', $expected);
|
||||
});
|
||||
|
||||
test('test register user with email confirmation', function () {
|
||||
$this->setSettings([
|
||||
'reg_enabled' => true,
|
||||
'reg_email_confirmation' => true,
|
||||
'registration.captcha.enabled' => false,
|
||||
'tos' => false,
|
||||
]);
|
||||
|
||||
Notification::fake();
|
||||
|
||||
$data = [
|
||||
'email' => 'john.doe@test.com',
|
||||
'username' => 'john.doe',
|
||||
'password' => '123123123',
|
||||
'password_confirmation' => '123123123',
|
||||
];
|
||||
|
||||
$expected = Arr::except($data, ['password', 'password_confirmation']);
|
||||
|
||||
$this->postJson('/api/register', $data)
|
||||
->assertStatus(201)
|
||||
->assertJson(['requires_email_confirmation' => true]);
|
||||
|
||||
$this->assertDatabaseHas('users', $expected);
|
||||
|
||||
$user = User::where('email', $data['email'])->first();
|
||||
|
||||
Notification::assertSentTo($user, VerifyEmail::class);
|
||||
});
|
||||
|
||||
test('test register with tos', function () {
|
||||
$this->setSettings([
|
||||
'reg_enabled' => true,
|
||||
'reg_email_confirmation' => false,
|
||||
'registration.captcha.enabled' => false,
|
||||
'tos' => true,
|
||||
]);
|
||||
|
||||
$data = [
|
||||
'email' => 'john.doe@test.com',
|
||||
'username' => 'john.doe',
|
||||
'password' => '123123123',
|
||||
'password_confirmation' => '123123123',
|
||||
];
|
||||
|
||||
$this->postJson('/api/register', $data)
|
||||
->assertStatus(422)
|
||||
->assertJsonFragment([
|
||||
'tos' => ['You have to accept Terms of Service.'],
|
||||
]);
|
||||
});
|
||||
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use App\Repositories\User\UserRepository;
|
||||
use App\Support\Enum\UserStatus;
|
||||
use Carbon\Carbon;
|
||||
use Laravel\Socialite\Contracts\User as SocialUserContract;
|
||||
use Laravel\Socialite\Two\FacebookProvider;
|
||||
use Mockery as m;
|
||||
|
||||
test('social authentication for first time', function () {
|
||||
$this->setSettings(['reg_enabled' => true]);
|
||||
|
||||
$socialUser = new ApiStubSocialUser;
|
||||
|
||||
mockFacebookProvider($socialUser);
|
||||
|
||||
$now = Carbon::now()->addHours(2);
|
||||
Carbon::setTestNow($now);
|
||||
|
||||
$this->postJson('/api/login/social', defaultParams())
|
||||
->assertOk();
|
||||
|
||||
$user = User::whereEmail($socialUser->getEmail())->first();
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'first_name' => 'John',
|
||||
'last_name' => 'Doe',
|
||||
'email' => $socialUser->getEmail(),
|
||||
'status' => UserStatus::ACTIVE,
|
||||
'avatar' => $socialUser->getAvatar(),
|
||||
'last_login' => $now,
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('social_logins', [
|
||||
'user_id' => $user->id,
|
||||
'provider' => 'facebook',
|
||||
'provider_id' => $socialUser->getId(),
|
||||
'avatar' => $socialUser->getAvatar(),
|
||||
]);
|
||||
});
|
||||
|
||||
test('associate social account with existing user', function () {
|
||||
$this->setSettings(['reg_enabled' => true]);
|
||||
|
||||
$socialUser = new ApiStubSocialUser;
|
||||
|
||||
mockFacebookProvider($socialUser);
|
||||
|
||||
$user = User::factory()->create([
|
||||
'email' => $socialUser->getEmail(),
|
||||
]);
|
||||
|
||||
$this->postJson('/api/login/social', defaultParams())
|
||||
->assertOk();
|
||||
|
||||
$this->assertDatabaseHas('social_logins', [
|
||||
'user_id' => $user->id,
|
||||
'provider' => 'facebook',
|
||||
'provider_id' => $socialUser->getId(),
|
||||
'avatar' => $socialUser->getAvatar(),
|
||||
]);
|
||||
});
|
||||
|
||||
test('social login if registration is disabled', function () {
|
||||
$this->setSettings(['reg_enabled' => false]);
|
||||
|
||||
$socialUser = new ApiStubSocialUser;
|
||||
|
||||
mockFacebookProvider($socialUser);
|
||||
|
||||
$this->postJson('/api/login/social', defaultParams())
|
||||
->assertForbidden()
|
||||
->assertJson([
|
||||
'message' => 'Only users who already created an account can log in.',
|
||||
]);
|
||||
});
|
||||
|
||||
test('social login with invalid provider', function () {
|
||||
$this->postJson('/api/login/social', defaultParams(['network' => 'foo']))
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrors('network');
|
||||
});
|
||||
|
||||
test('social login for banned user', function () {
|
||||
$socialUser = new ApiStubSocialUser;
|
||||
|
||||
mockFacebookProvider($socialUser);
|
||||
|
||||
$user = User::factory()->create([
|
||||
'email' => $socialUser->getEmail(),
|
||||
'status' => UserStatus::BANNED,
|
||||
]);
|
||||
|
||||
app(UserRepository::class)->associateSocialAccountForUser($user->id, 'facebook', $socialUser);
|
||||
|
||||
$this->postJson('/api/login/social', defaultParams())
|
||||
->assertForbidden()
|
||||
->assertJson([
|
||||
'message' => 'Your account is banned by administrators.',
|
||||
]);
|
||||
});
|
||||
|
||||
function defaultParams(array $overrides = [])
|
||||
{
|
||||
return array_merge([
|
||||
'network' => 'facebook',
|
||||
'social_token' => 'foo',
|
||||
'device_name' => 'test',
|
||||
], $overrides);
|
||||
}
|
||||
|
||||
function mockFacebookProvider($socialUser)
|
||||
{
|
||||
$provider = m::mock(FacebookProvider::class);
|
||||
$provider->shouldReceive('userFromToken')->with('foo')->andReturn($socialUser);
|
||||
|
||||
Socialite::shouldReceive('driver')->with('facebook')->andReturn($provider);
|
||||
}
|
||||
|
||||
class ApiStubSocialUser implements SocialUserContract
|
||||
{
|
||||
public $avatar_original = 'http://www.gravatar.com/avatar';
|
||||
|
||||
public function getId()
|
||||
{
|
||||
return '123';
|
||||
}
|
||||
|
||||
public function getNickname()
|
||||
{
|
||||
return 'johndoe';
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return 'John Doe';
|
||||
}
|
||||
|
||||
public function getEmail()
|
||||
{
|
||||
return 'john@doe.com';
|
||||
}
|
||||
|
||||
public function getAvatar()
|
||||
{
|
||||
return 'http://www.gravatar.com/avatar';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\Verified;
|
||||
use Illuminate\Auth\Notifications\VerifyEmail;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Tests\Feature\ApiTestCase;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->setSettings(['reg_enabled' => true]);
|
||||
});
|
||||
|
||||
test('unauthenticated request cannot verify email', function () {
|
||||
$this->postJson('api/email/verify')->assertStatus(401);
|
||||
});
|
||||
|
||||
test('verify returns not found when email confirmation disabled', function () {
|
||||
$this->setSettings(['reg_email_confirmation' => false]);
|
||||
|
||||
$user = $this->login();
|
||||
|
||||
$payload = validSignedPayload($user);
|
||||
|
||||
$this->postJson('api/email/verify', $payload)
|
||||
->assertStatus(404);
|
||||
});
|
||||
|
||||
test('verify succeeds for unverified user with valid signature', function () {
|
||||
$this->setSettings(['reg_email_confirmation' => true]);
|
||||
|
||||
Event::fake([Verified::class]);
|
||||
|
||||
$user = User::factory()->create(['email_verified_at' => null]);
|
||||
$this->be($user, ApiTestCase::API_GUARD);
|
||||
|
||||
$payload = validSignedPayload($user);
|
||||
|
||||
$this->postJson('api/email/verify', $payload)
|
||||
->assertOk();
|
||||
|
||||
$this->assertNotNull($user->fresh()->email_verified_at);
|
||||
Event::assertDispatched(Verified::class);
|
||||
});
|
||||
|
||||
test('verify returns bad request when already verified', function () {
|
||||
$this->setSettings(['reg_email_confirmation' => true]);
|
||||
|
||||
$user = User::factory()->create(['email_verified_at' => Carbon::now()]);
|
||||
$this->be($user, ApiTestCase::API_GUARD);
|
||||
|
||||
$payload = validSignedPayload($user);
|
||||
|
||||
$this->postJson('api/email/verify', $payload)
|
||||
->assertStatus(400);
|
||||
});
|
||||
|
||||
test('verify fails with invalid signature', function () {
|
||||
$this->setSettings(['reg_email_confirmation' => true]);
|
||||
|
||||
$user = User::factory()->create(['email_verified_at' => null]);
|
||||
$this->be($user, ApiTestCase::API_GUARD);
|
||||
|
||||
$this->postJson('api/email/verify', [
|
||||
'id' => $user->id,
|
||||
'hash' => sha1($user->email),
|
||||
'expires' => Carbon::now()->addMinutes(60)->timestamp,
|
||||
'signature' => 'invalid-signature',
|
||||
])->assertStatus(403);
|
||||
|
||||
$this->assertNull($user->fresh()->email_verified_at);
|
||||
});
|
||||
|
||||
test('verify requires id hash expires signature', function () {
|
||||
$this->login();
|
||||
|
||||
$this->postJson('api/email/verify', [])
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrors(['id', 'hash', 'expires', 'signature']);
|
||||
});
|
||||
|
||||
test('resend sends verification notification', function () {
|
||||
Notification::fake();
|
||||
|
||||
$user = User::factory()->create(['email_verified_at' => null]);
|
||||
$this->be($user, ApiTestCase::API_GUARD);
|
||||
|
||||
$this->postJson('api/email/resend')->assertStatus(202);
|
||||
|
||||
Notification::assertSentTo($user, VerifyEmail::class);
|
||||
});
|
||||
|
||||
test('resend returns bad request for already verified user', function () {
|
||||
$user = User::factory()->create(['email_verified_at' => Carbon::now()]);
|
||||
$this->be($user, ApiTestCase::API_GUARD);
|
||||
|
||||
$this->postJson('api/email/resend')->assertStatus(400);
|
||||
});
|
||||
|
||||
function validSignedPayload(User $user): array
|
||||
{
|
||||
$expires = Carbon::now()->addMinutes(60)->timestamp;
|
||||
|
||||
$signedUrl = URL::temporarySignedRoute(
|
||||
'verification.verify',
|
||||
Carbon::createFromTimestamp($expires),
|
||||
['id' => $user->id, 'hash' => sha1($user->email)]
|
||||
);
|
||||
|
||||
parse_str(parse_url($signedUrl, PHP_URL_QUERY), $query);
|
||||
|
||||
return [
|
||||
'id' => $user->id,
|
||||
'hash' => sha1($user->email),
|
||||
'expires' => $query['expires'],
|
||||
'signature' => $query['signature'],
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Resources\PermissionResource;
|
||||
use App\Models\Permission;
|
||||
use App\Models\User;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Tests\Feature\ApiTestCase;
|
||||
|
||||
test('unauthenticated', function () {
|
||||
$this->getJson('/api/permissions')->assertStatus(401);
|
||||
});
|
||||
|
||||
test('get users without permission', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user, ApiTestCase::API_GUARD)
|
||||
->getJson('/api/permissions')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('get permissions', function () {
|
||||
Permission::factory()->times(3)->create();
|
||||
|
||||
$response = $this->actingAs(permissionUser(), ApiTestCase::API_GUARD)
|
||||
->getJson('/api/permissions')
|
||||
->assertOk();
|
||||
|
||||
// 7 default permissions + 3 newly created
|
||||
$this->assertCount(10, $response->original);
|
||||
});
|
||||
|
||||
test('get permission', function () {
|
||||
$permission = Permission::factory()->create();
|
||||
|
||||
$this->actingAs(permissionUser(), ApiTestCase::API_GUARD)
|
||||
->getJson("/api/permissions/{$permission->id}")
|
||||
->assertOk()
|
||||
->assertJson([
|
||||
'data' => (new PermissionResource($permission))->toArray(request()),
|
||||
]);
|
||||
});
|
||||
|
||||
test('create permission', function () {
|
||||
$data = [
|
||||
'name' => 'foo',
|
||||
'display_name' => 'Foo Permission',
|
||||
'description' => 'This is foo permission.',
|
||||
];
|
||||
|
||||
$this->actingAs(permissionUser(), ApiTestCase::API_GUARD)
|
||||
->postJson('/api/permissions', $data)
|
||||
->assertStatus(201)
|
||||
->assertJsonFragment($data);
|
||||
|
||||
$this->assertDatabaseHas('permissions', $data);
|
||||
});
|
||||
|
||||
test('create permission with invalid name', function () {
|
||||
$this->actingAs(permissionUser(), ApiTestCase::API_GUARD)
|
||||
->postJson('/api/permissions')
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrors('name');
|
||||
|
||||
$existingPermission = Permission::first();
|
||||
|
||||
$this->postJson('/api/permissions', ['name' => $existingPermission->name])
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrors('name');
|
||||
|
||||
$this->postJson('/api/permissions', ['name' => 'foo bar'])
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrors('name');
|
||||
});
|
||||
|
||||
test('partially update permission', function () {
|
||||
permissionUser();
|
||||
|
||||
$permission = Permission::factory()->create();
|
||||
|
||||
$data = ['name' => 'foo'];
|
||||
$expected = $data + ['id' => $permission->id];
|
||||
|
||||
$this->actingAs(permissionUser(), ApiTestCase::API_GUARD)
|
||||
->patchJson("/api/permissions/{$permission->id}", $data)
|
||||
->assertJsonFragment($expected);
|
||||
|
||||
$this->assertDatabaseHas('permissions', $expected);
|
||||
});
|
||||
|
||||
test('update permission', function () {
|
||||
$permission = Permission::factory()->create();
|
||||
|
||||
$data = [
|
||||
'name' => 'foo',
|
||||
'display_name' => 'Foo Role',
|
||||
'description' => 'This is foo role.',
|
||||
];
|
||||
$expected = $data + ['id' => $permission->id];
|
||||
|
||||
$this->actingAs(permissionUser(), ApiTestCase::API_GUARD)
|
||||
->patchJson("/api/permissions/{$permission->id}", $data)
|
||||
->assertJsonFragment($expected);
|
||||
|
||||
$this->assertDatabaseHas('permissions', $expected);
|
||||
});
|
||||
|
||||
test('remove permission', function () {
|
||||
$permission = Permission::factory()->create(['removable' => true]);
|
||||
|
||||
$this->actingAs(permissionUser(), ApiTestCase::API_GUARD)
|
||||
->deleteJson("/api/permissions/{$permission->id}")
|
||||
->assertOk()
|
||||
->assertJson(['success' => true]);
|
||||
|
||||
$this->assertDatabaseMissing('permissions', ['id' => $permission->id]);
|
||||
});
|
||||
|
||||
test('remove non removable permission', function () {
|
||||
$permission = Permission::factory()->create(['removable' => false]);
|
||||
|
||||
$this->actingAs(permissionUser(), ApiTestCase::API_GUARD)
|
||||
->deleteJson("/api/permissions/{$permission->id}")
|
||||
->assertStatus(403);
|
||||
});
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
function permissionUser()
|
||||
{
|
||||
return UserFactory::user()->withPermissions('permissions.manage')->create();
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Resources\PermissionResource;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Tests\Feature\ApiTestCase;
|
||||
|
||||
test('unauthenticated', function () {
|
||||
$role = Role::factory()->create();
|
||||
|
||||
$this->getJson("/api/roles/{$role->id}/permissions")
|
||||
->assertStatus(401);
|
||||
});
|
||||
|
||||
test('get settings without permission', function () {
|
||||
$role = Role::factory()->create();
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user, ApiTestCase::API_GUARD)
|
||||
->getJson("/api/roles/{$role->id}/permissions")
|
||||
->assertStatus(403);
|
||||
});
|
||||
|
||||
test('get role permissions', function () {
|
||||
$role = Role::factory()->create();
|
||||
$permission = Permission::factory()->create();
|
||||
|
||||
$role->attachPermission($permission);
|
||||
|
||||
$this->actingAs(rolePermissionUser(), ApiTestCase::API_GUARD)
|
||||
->getJson("/api/roles/{$role->id}/permissions")
|
||||
->assertOk()
|
||||
->assertJsonFragment(
|
||||
PermissionResource::collection([$permission])->toArray(request())
|
||||
);
|
||||
});
|
||||
|
||||
test('update role permissions', function () {
|
||||
$role = Role::factory()->create();
|
||||
$permissions1 = Permission::factory()->times(2)->create();
|
||||
$permissions2 = Permission::factory()->times(3)->create();
|
||||
|
||||
$role->attachPermissions($permissions1->toArray());
|
||||
|
||||
$this->actingAs(rolePermissionUser(), ApiTestCase::API_GUARD)
|
||||
->putJson("/api/roles/{$role->id}/permissions", [
|
||||
'permissions' => $permissions2->pluck('id')->toArray(),
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonFragment(
|
||||
(new PermissionResource($permissions2[0]))->toArray(
|
||||
$this->createTestRequest(Request::createFromGlobals())
|
||||
)
|
||||
)
|
||||
->assertJsonFragment(
|
||||
(new PermissionResource($permissions2[1]))->toArray(
|
||||
$this->createTestRequest(Request::createFromGlobals())
|
||||
)
|
||||
)
|
||||
->assertJsonFragment(
|
||||
(new PermissionResource($permissions2[2]))->toArray(
|
||||
$this->createTestRequest(Request::createFromGlobals())
|
||||
)
|
||||
);
|
||||
|
||||
foreach ($permissions2 as $perm) {
|
||||
$this->assertDatabaseHas('permission_role', [
|
||||
'permission_id' => $perm->id,
|
||||
'role_id' => $role->id,
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
function rolePermissionUser()
|
||||
{
|
||||
return UserFactory::user()->withPermissions('permissions.manage')->create();
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Resources\RoleResource;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use Facades\Tests\Setup\RoleFactory;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Tests\Feature\ApiTestCase;
|
||||
|
||||
test('unauthenticated', function () {
|
||||
$this->getJson('/api/roles')
|
||||
->assertStatus(401);
|
||||
});
|
||||
|
||||
test('get settings without permission', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user, ApiTestCase::API_GUARD)
|
||||
->getJson('/api/roles')
|
||||
->assertStatus(403);
|
||||
});
|
||||
|
||||
test('get roles', function () {
|
||||
Role::factory()->times(4)->create();
|
||||
|
||||
$response = $this->actingAs(roleUser(), ApiTestCase::API_GUARD)
|
||||
->getJson('/api/roles')
|
||||
->assertOk();
|
||||
|
||||
$this->assertCount(7, $response->original);
|
||||
});
|
||||
|
||||
test('get role', function () {
|
||||
$userRole = Role::whereName('User')->first();
|
||||
|
||||
$this->actingAs(roleUser(), ApiTestCase::API_GUARD)
|
||||
->getJson("/api/roles/{$userRole->id}")
|
||||
->assertOk()
|
||||
->assertJson([
|
||||
'data' => (new RoleResource($userRole))->resolve(),
|
||||
]);
|
||||
});
|
||||
|
||||
test('create role', function () {
|
||||
roleUser();
|
||||
|
||||
$data = [
|
||||
'name' => 'foo',
|
||||
'display_name' => 'Foo Role',
|
||||
'description' => 'This is foo role.',
|
||||
];
|
||||
|
||||
$this->actingAs(roleUser(), ApiTestCase::API_GUARD)
|
||||
->postJson('/api/roles', $data)
|
||||
->assertStatus(201)
|
||||
->assertJsonFragment($data);
|
||||
|
||||
$this->assertDatabaseHas('roles', $data);
|
||||
});
|
||||
|
||||
test('create role with invalid name', function () {
|
||||
$this->be(roleUser(), ApiTestCase::API_GUARD);
|
||||
|
||||
$this->postJson('/api/roles')
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrors('name');
|
||||
|
||||
$this->postJson('/api/roles', ['name' => 'User'])
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrors('name');
|
||||
|
||||
$this->postJson('/api/roles', ['name' => 'foo bar'])
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrors('name');
|
||||
});
|
||||
|
||||
test('update role', function () {
|
||||
$user = roleUser();
|
||||
|
||||
$data = ['name' => 'foo'];
|
||||
$expected = $data + ['id' => $user->role_id];
|
||||
|
||||
$this->actingAs($user, ApiTestCase::API_GUARD)
|
||||
->patchJson("/api/roles/{$user->role_id}", $data)
|
||||
->assertOk()
|
||||
->assertJsonFragment($expected);
|
||||
|
||||
$this->assertDatabaseHas('roles', $expected);
|
||||
});
|
||||
|
||||
test('partially update role', function () {
|
||||
$user = roleUser();
|
||||
|
||||
$data = [
|
||||
'name' => 'foo',
|
||||
'display_name' => 'Foo Role',
|
||||
'description' => 'This is foo role.',
|
||||
];
|
||||
$expected = $data + ['id' => $user->role_id];
|
||||
|
||||
$this->actingAs($user, ApiTestCase::API_GUARD)
|
||||
->patchJson("/api/roles/{$user->role_id}", $data)
|
||||
->assertOk()
|
||||
->assertJsonFragment($expected);
|
||||
|
||||
$this->assertDatabaseHas('roles', $expected);
|
||||
});
|
||||
|
||||
test('remove role', function () {
|
||||
$userRole = Role::whereName('User')->first();
|
||||
$role = RoleFactory::removable()->withPermissions('roles.manage')->create();
|
||||
$user = UserFactory::role($role)->create();
|
||||
|
||||
$this->actingAs($user, ApiTestCase::API_GUARD)
|
||||
->deleteJson("/api/roles/{$role->id}")
|
||||
->assertOk()
|
||||
->assertJson(['success' => true]);
|
||||
|
||||
$this->assertDatabaseMissing('roles', ['id' => $role->id]);
|
||||
$this->assertEquals($userRole->id, $user->fresh()->role_id);
|
||||
});
|
||||
|
||||
test('remove non removable role', function () {
|
||||
$role = RoleFactory::withPermissions('roles.manage')->create();
|
||||
|
||||
$this->actingAs(roleUser(), ApiTestCase::API_GUARD)
|
||||
->deleteJson("/api/roles/{$role->id}")
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
function roleUser()
|
||||
{
|
||||
return UserFactory::user()->withPermissions('roles.manage')->create();
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Resources\CountryResource;
|
||||
use App\Models\Country;
|
||||
|
||||
test('unauthenticated', function () {
|
||||
$this->getJson('/api/countries')->assertStatus(401);
|
||||
});
|
||||
|
||||
test('get all countries', function () {
|
||||
$this->login();
|
||||
|
||||
$this->getJson('/api/countries')
|
||||
->assertOk()
|
||||
->assertJson([
|
||||
'data' => CountryResource::collection(Country::all())->resolve(),
|
||||
]);
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Tests\Feature\ApiTestCase;
|
||||
|
||||
/**
|
||||
* The seeded account config/demo.php protects by default.
|
||||
*/
|
||||
function apiDemoAccount(): User
|
||||
{
|
||||
return User::where('username', 'admin')->firstOrFail();
|
||||
}
|
||||
|
||||
/**
|
||||
* An actor who would be allowed to manage users if demo mode were off, so a
|
||||
* refusal here is demo mode's doing and not the permission middleware's.
|
||||
*/
|
||||
function apiUserManager(): User
|
||||
{
|
||||
return UserFactory::withPermissions('users.manage')->create();
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
config(['demo.enabled' => true]);
|
||||
});
|
||||
|
||||
test('the API refuses a blocked write with a 403 and a message', function () {
|
||||
$this->actingAs(apiDemoAccount(), ApiTestCase::API_GUARD)
|
||||
->putJson('api/me/2fa')
|
||||
->assertForbidden()
|
||||
->assertJson(['message' => 'This action is disabled in the demo.']);
|
||||
});
|
||||
|
||||
test('the API will not let the demo account be deleted', function () {
|
||||
$admin = apiDemoAccount();
|
||||
|
||||
$this->actingAs(apiUserManager(), ApiTestCase::API_GUARD)
|
||||
->deleteJson('api/users/'.$admin->id)
|
||||
->assertForbidden()
|
||||
->assertJson(['message' => 'The demo account cannot be modified.']);
|
||||
|
||||
$this->assertDatabaseHas('users', ['username' => 'admin']);
|
||||
});
|
||||
|
||||
test('the API will not let the demo account change its own login details', function () {
|
||||
$this->actingAs(apiDemoAccount(), ApiTestCase::API_GUARD)
|
||||
->patchJson('api/me/details/auth', ['email' => 'hijacked@example.com'])
|
||||
->assertForbidden();
|
||||
|
||||
$this->assertEquals('admin@example.com', apiDemoAccount()->email);
|
||||
});
|
||||
|
||||
test('an ordinary API consumer is unaffected', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user, ApiTestCase::API_GUARD)
|
||||
->patchJson('api/me/details', ['first_name' => 'Jane'])
|
||||
->assertSuccessful();
|
||||
|
||||
$this->assertEquals('Jane', $user->fresh()->first_name);
|
||||
});
|
||||
|
||||
test('the API is untouched while demo mode is off', function () {
|
||||
config(['demo.enabled' => false]);
|
||||
|
||||
$admin = apiDemoAccount();
|
||||
|
||||
$this->actingAs(apiUserManager(), ApiTestCase::API_GUARD)
|
||||
->deleteJson('api/users/'.$admin->id)
|
||||
->assertSuccessful();
|
||||
|
||||
$this->assertDatabaseMissing('users', ['username' => 'admin']);
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
|
||||
test('test user can update his authentication details', function () {
|
||||
$user = $this->login();
|
||||
|
||||
$this->patch('/api/me/details/auth', [
|
||||
'email' => 'foo@example.com',
|
||||
'username' => 'john.doe',
|
||||
'password' => '12345678',
|
||||
'password_confirmation' => '12345678',
|
||||
])->assertOk()
|
||||
->assertJsonFragment(['email' => 'foo@example.com', 'username' => 'john.doe']);
|
||||
|
||||
$this->assertTrue(password_verify('12345678', $user->fresh()->password));
|
||||
});
|
||||
|
||||
test('test user can update only email and leave other fields unchanged', function () {
|
||||
$user = $this->login();
|
||||
|
||||
$this->patch('/api/me/details/auth', [
|
||||
'email' => 'foo@example.com',
|
||||
])->assertOk()
|
||||
->assertJsonFragment(['email' => 'foo@example.com']);
|
||||
|
||||
$this->assertEquals($user->username, $user->fresh()->username);
|
||||
$this->assertEquals($user->password, $user->fresh()->password);
|
||||
});
|
||||
|
||||
test('test email field is required', function () {
|
||||
$this->login();
|
||||
|
||||
$this->patch('/api/me/details/auth')
|
||||
->assertJsonValidationErrors('email');
|
||||
});
|
||||
|
||||
test('test email field must be valid email', function () {
|
||||
$this->login();
|
||||
|
||||
$this->patch('/api/me/details/auth', [
|
||||
'email' => 'invalid email',
|
||||
])->assertJsonValidationErrors('email');
|
||||
});
|
||||
|
||||
test('test email field must be unique', function () {
|
||||
$this->login();
|
||||
|
||||
UserFactory::email('john.doe@test.com')->create();
|
||||
|
||||
$this->patch('/api/me/details/auth', [
|
||||
'email' => 'john.doe@test.com',
|
||||
])->assertJsonValidationErrors('email');
|
||||
});
|
||||
|
||||
test('test username field must be unique', function () {
|
||||
$this->login();
|
||||
|
||||
UserFactory::withCredentials('john.doe', '123123')->create();
|
||||
|
||||
$this->patch('/api/me/details/auth', [
|
||||
'email' => 'john.doe@test.com',
|
||||
'username' => 'john.doe',
|
||||
])->assertJsonValidationErrors('username');
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Http\UploadedFile;
|
||||
|
||||
test('only authenticated user can update avatar', function () {
|
||||
$this->post('/api/me/avatar')->assertStatus(401);
|
||||
});
|
||||
|
||||
test('upload avatar image', function () {
|
||||
$this->login();
|
||||
|
||||
Storage::fake('public');
|
||||
|
||||
$file = UploadedFile::fake()->image('avatar.png', 500, 500);
|
||||
|
||||
$response = $this->post('api/me/avatar', [
|
||||
'file' => $file,
|
||||
]);
|
||||
|
||||
$this->assertNotNull($response->json('data.avatar'));
|
||||
|
||||
$uploadedFile = str_replace(url(''), '', $response->json('data.avatar'));
|
||||
$uploadedFile = ltrim($uploadedFile, '/storage/');
|
||||
|
||||
Storage::disk('public')->assertExists($uploadedFile);
|
||||
|
||||
[$width, $height] = getimagesizefromstring(
|
||||
Storage::disk('public')->get($uploadedFile)
|
||||
);
|
||||
|
||||
$this->assertEquals(160, $width);
|
||||
$this->assertEquals(160, $height);
|
||||
});
|
||||
|
||||
test('upload invalid image', function () {
|
||||
$this->login();
|
||||
|
||||
Storage::fake('public');
|
||||
|
||||
$file = UploadedFile::fake()->create('avatar.txt', 500);
|
||||
|
||||
$this->post('/api/me/avatar', ['file' => $file])
|
||||
->assertStatus(422)
|
||||
->assertJsonFragment([
|
||||
'file' => [
|
||||
trans('validation.image', ['attribute' => 'file']),
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
test('update avatar from external source', function () {
|
||||
$this->login();
|
||||
|
||||
$url = 'http://google.com';
|
||||
|
||||
$this->putJson('/api/me/avatar/external', ['url' => $url])
|
||||
->assertOk()
|
||||
->assertJsonFragment(['avatar' => $url]);
|
||||
});
|
||||
|
||||
test('update avatar with invalid external source', function () {
|
||||
$this->login();
|
||||
|
||||
$this->putJson('/api/me/avatar/external', ['url' => 'foo'])
|
||||
->assertStatus(422);
|
||||
});
|
||||
|
||||
test('delete avatar', function () {
|
||||
$user = $this->login();
|
||||
|
||||
$user->forceFill(['avatar' => 'http://google.com'])->save();
|
||||
|
||||
$this->deleteJson('api/me/avatar')
|
||||
->assertOk()
|
||||
->assertJsonFragment([
|
||||
'avatar' => url('assets/img/profile.png'), // default profile image
|
||||
]);
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Resources\UserResource;
|
||||
use Carbon\Carbon;
|
||||
|
||||
test('get user profile unauthenticated', function () {
|
||||
$this->getJson('/api/me')->assertStatus(401);
|
||||
});
|
||||
|
||||
test('get user profile', function () {
|
||||
$user = $this->login();
|
||||
|
||||
$this->getJson('/api/me')
|
||||
->assertOk()
|
||||
->assertJson(['data' => (new UserResource($user))->resolve()]);
|
||||
});
|
||||
|
||||
test('update user profile unauthenticated', function () {
|
||||
$this->patchJson('/api/me/details')->assertStatus(401);
|
||||
});
|
||||
|
||||
test('update user profile', function () {
|
||||
$user = $this->login();
|
||||
|
||||
$data = getData();
|
||||
|
||||
$response = $this->patchJson('/api/me/details', $data);
|
||||
|
||||
$transformed = (new UserResource($user->fresh()))->resolve();
|
||||
|
||||
$response->assertJsonFragment($transformed);
|
||||
|
||||
$this->assertDatabaseHas('users', array_merge($data, ['id' => $user->id]));
|
||||
});
|
||||
|
||||
test('partially update user details', function () {
|
||||
$user = $this->login();
|
||||
|
||||
$data = [
|
||||
'first_name' => 'John',
|
||||
'last_name' => 'Doe',
|
||||
];
|
||||
|
||||
$response = $this->patchJson('/api/me/details', $data);
|
||||
|
||||
$transformed = (new UserResource($user->fresh()))->resolve();
|
||||
|
||||
$response->assertJsonFragment($transformed);
|
||||
|
||||
$this->assertDatabaseHas('users', array_merge($data, [
|
||||
'id' => $user->id,
|
||||
'birthday' => $user->birthday->format('Y-m-d'),
|
||||
'phone' => $user->phone,
|
||||
'address' => $user->address,
|
||||
'country_id' => $user->country_id,
|
||||
]));
|
||||
});
|
||||
|
||||
test('update without country id', function () {
|
||||
$user = $this->login();
|
||||
|
||||
$data = getData();
|
||||
|
||||
unset($data['country_id']);
|
||||
|
||||
$response = $this->patchJson('/api/me/details', $data);
|
||||
|
||||
$transformed = (new UserResource($user->fresh()))->resolve();
|
||||
|
||||
$response->assertJsonFragment($transformed);
|
||||
|
||||
$this->assertDatabaseHas('users', array_merge($data, ['id' => $user->id]));
|
||||
});
|
||||
|
||||
test('update with invalid date format', function () {
|
||||
$this->login();
|
||||
|
||||
$this->patchJson('/api/me/details', ['birthday' => 'foo'])
|
||||
->assertStatus(422)
|
||||
->assertJsonFragment([
|
||||
'birthday' => [
|
||||
trans('validation.date', ['attribute' => 'birthday']),
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
function getData(array $attrs = []): array
|
||||
{
|
||||
return array_merge([
|
||||
'first_name' => 'John',
|
||||
'last_name' => 'Doe',
|
||||
'birthday' => Carbon::now()->subYears(25)->format('Y-m-d'),
|
||||
'phone' => '(123) 456 789',
|
||||
'address' => 'some address 1',
|
||||
'country_id' => 688,
|
||||
], $attrs);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Resources\SessionResource;
|
||||
use App\Models\User;
|
||||
use App\Repositories\Session\SessionRepository;
|
||||
use Carbon\Carbon;
|
||||
use Faker\Generator;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
test('get user sessions unauthenticated', function () {
|
||||
$this->getJson('/api/me/sessions')->assertStatus(401);
|
||||
});
|
||||
|
||||
test('get sessions if non database driver is used', function () {
|
||||
config(['session.driver' => 'array']);
|
||||
|
||||
$this->login();
|
||||
|
||||
$this->getJson('/api/me/sessions')->assertStatus(404);
|
||||
});
|
||||
|
||||
test('get user sessions', function () {
|
||||
config(['session.driver' => 'database']);
|
||||
|
||||
$user = $this->login();
|
||||
|
||||
$sessions = generateProfileApiSessions($user);
|
||||
|
||||
$this->getJson('/api/me/sessions')
|
||||
->assertOk()
|
||||
->assertJsonFragment([
|
||||
'data' => SessionResource::collection($sessions)->resolve(),
|
||||
]);
|
||||
});
|
||||
|
||||
function generateProfileApiSessions(User $user, $count = 5)
|
||||
{
|
||||
$sessions = [];
|
||||
$faker = app(Generator::class);
|
||||
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
array_push($sessions, [
|
||||
'id' => Str::random(40),
|
||||
'user_id' => $user->id,
|
||||
'ip_address' => $faker->ipv4,
|
||||
'user_agent' => $faker->userAgent,
|
||||
'payload' => Str::random(),
|
||||
'last_activity' => Carbon::now()->subMinute()->timestamp,
|
||||
]);
|
||||
}
|
||||
|
||||
DB::table('sessions')->insert($sessions);
|
||||
|
||||
return app(SessionRepository::class)->getUserSessions($user->id);
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
use App\Events\User\TwoFactorEnabled;
|
||||
use App\Http\Resources\UserResource;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use PragmaRX\Google2FA\Google2FA;
|
||||
use Tests\Feature\ApiTestCase;
|
||||
|
||||
test('update 2fa unathenticated', function () {
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
User::factory()->create();
|
||||
|
||||
$this->putJson('api/me/2fa')
|
||||
->assertStatus(401);
|
||||
});
|
||||
|
||||
test('enable two factor auth', function () {
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
$this->withoutExceptionHandling();
|
||||
|
||||
Event::fake([
|
||||
TwoFactorEnabled::class,
|
||||
]);
|
||||
|
||||
$user = $this->login();
|
||||
|
||||
$this->putJson('api/me/2fa')
|
||||
->assertOk()
|
||||
->assertJson(['message' => 'Verification token sent.'])
|
||||
->assertJsonStructure([
|
||||
'message',
|
||||
'qrcode',
|
||||
]);
|
||||
|
||||
$this->assertTrue(
|
||||
DB::table('users')
|
||||
->where('id', $user->id)
|
||||
->whereNotNull('two_factor_secret')
|
||||
->exists()
|
||||
);
|
||||
|
||||
Event::assertNotDispatched(TwoFactorEnabled::class);
|
||||
});
|
||||
|
||||
test('verify user auth app with correct code', function () {
|
||||
$google2fa = new Google2FA;
|
||||
$secret = encrypt($google2fa->generateSecretKey());
|
||||
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
Event::fake([
|
||||
TwoFactorEnabled::class,
|
||||
]);
|
||||
|
||||
$user = $this->login();
|
||||
$user->two_factor_secret = $secret;
|
||||
$user->save();
|
||||
|
||||
$validCode = $google2fa->getCurrentOtp(decrypt($user->two_factor_secret));
|
||||
|
||||
$response = $this->postJson('api/me/2fa/verify', ['code' => $validCode]);
|
||||
|
||||
$updatedUser = (new UserResource($user->fresh()))->resolve();
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonFragment($updatedUser);
|
||||
|
||||
$this->assertTrue(
|
||||
DB::table('users')
|
||||
->where('id', $user->id)
|
||||
->whereNotNull('two_factor_confirmed_at')
|
||||
->exists()
|
||||
);
|
||||
|
||||
Event::assertDispatched(TwoFactorEnabled::class);
|
||||
});
|
||||
|
||||
test('verify user app with invalid token', function () {
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
$user = $this->login();
|
||||
|
||||
$this->postJson('api/me/2fa/verify', ['code' => '123123'])
|
||||
->assertStatus(422)
|
||||
->assertJson(['message' => 'Invalid 2FA token.']);
|
||||
|
||||
$this->assertTrue(
|
||||
DB::table('users')
|
||||
->where('id', $user->id)
|
||||
->whereNull('two_factor_confirmed_at')
|
||||
->exists()
|
||||
);
|
||||
});
|
||||
|
||||
test('enable two factor auth when it is already enabled', function () {
|
||||
$google2fa = new Google2FA;
|
||||
$secret = encrypt($google2fa->generateSecretKey());
|
||||
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
$user = $this->login();
|
||||
$user->two_factor_secret = $secret;
|
||||
$user->two_factor_confirmed_at = Carbon::now();
|
||||
$user->save();
|
||||
|
||||
$this->putJson('api/me/2fa')
|
||||
->assertStatus(422)
|
||||
->assertJson([
|
||||
'message' => '2FA is already enabled for this user.',
|
||||
]);
|
||||
});
|
||||
|
||||
test('disable two factor auth', function () {
|
||||
$google2fa = new Google2FA;
|
||||
$secret = encrypt($google2fa->generateSecretKey());
|
||||
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
$user = User::factory()->create([
|
||||
'two_factor_secret' => $secret,
|
||||
'two_factor_confirmed_at' => Carbon::now(),
|
||||
]);
|
||||
|
||||
$this->be($user, ApiTestCase::API_GUARD);
|
||||
|
||||
$response = $this->deleteJson('api/me/2fa');
|
||||
|
||||
$user = (new UserResource($user->fresh()))->resolve();
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonFragment($user);
|
||||
});
|
||||
|
||||
test('disable 2fa when it is already disabled', function () {
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
$this->login();
|
||||
|
||||
$this->deleteJson('api/me/2fa')
|
||||
->assertStatus(422)
|
||||
->assertJson([
|
||||
'message' => '2FA is not enabled for this user.',
|
||||
]);
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Resources\SessionResource;
|
||||
use App\Models\User;
|
||||
use App\Repositories\Session\SessionRepository;
|
||||
use Carbon\Carbon;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\Feature\ApiTestCase;
|
||||
|
||||
beforeEach(function () {
|
||||
config(['session.driver' => 'database']);
|
||||
});
|
||||
|
||||
test('unauthenticated', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$session = createSession($user);
|
||||
|
||||
$this->getJson("/api/sessions/{$session->id}")
|
||||
->assertStatus(401);
|
||||
});
|
||||
|
||||
test('get session which belongs to other user', function () {
|
||||
$this->login();
|
||||
|
||||
$user2 = User::factory()->create();
|
||||
|
||||
$session = createSession($user2);
|
||||
|
||||
$this->getJson("/api/sessions/{$session->id}")
|
||||
->assertStatus(403);
|
||||
});
|
||||
|
||||
test('get session', function () {
|
||||
$user = $this->login();
|
||||
|
||||
$session = createSession($user);
|
||||
|
||||
$this->getJson("/api/sessions/{$session->id}")
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'data' => (new SessionResource($session))->resolve(),
|
||||
]);
|
||||
});
|
||||
|
||||
test('invalidate his own session', function () {
|
||||
$user = $this->login();
|
||||
|
||||
$session = createSession($user);
|
||||
|
||||
$this->deleteJson("/api/sessions/{$session->id}")
|
||||
->assertStatus(200)
|
||||
->assertJson(['success' => true]);
|
||||
});
|
||||
|
||||
test('invalidate session for other user', function () {
|
||||
$user = UserFactory::withPermissions('users.manage')->create();
|
||||
$user2 = User::factory()->create();
|
||||
$session = createSession($user2);
|
||||
|
||||
$this->actingAs($user, ApiTestCase::API_GUARD)
|
||||
->deleteJson("/api/sessions/{$session->id}")
|
||||
->assertOk()
|
||||
->assertJson(['success' => true]);
|
||||
});
|
||||
|
||||
test('invalidate session for other user without permission', function () {
|
||||
$this->login();
|
||||
$user2 = User::factory()->create();
|
||||
$session = createSession($user2);
|
||||
|
||||
$this->deleteJson("/api/sessions/{$session->id}")
|
||||
->assertStatus(403);
|
||||
});
|
||||
|
||||
function createSession(User $user)
|
||||
{
|
||||
$sessionId = Str::random(40);
|
||||
|
||||
$data = [
|
||||
'id' => $sessionId,
|
||||
'user_id' => $user->id,
|
||||
'ip_address' => '127.0.0.1',
|
||||
'user_agent' => 'foo',
|
||||
'payload' => Str::random(),
|
||||
'last_activity' => Carbon::now()->timestamp,
|
||||
];
|
||||
|
||||
DB::table('sessions')->insert($data);
|
||||
|
||||
return app(SessionRepository::class)->find($sessionId);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Tests\Feature\ApiTestCase;
|
||||
|
||||
test('only authenticated users can view app settings', function () {
|
||||
$this->getJson('/api/settings')->assertStatus(401);
|
||||
});
|
||||
|
||||
test('get settings without permission', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user, ApiTestCase::API_GUARD)
|
||||
->getJson('/api/settings')
|
||||
->assertStatus(403);
|
||||
});
|
||||
|
||||
test('get settings', function () {
|
||||
$user = UserFactory::withPermissions('settings.general')->create();
|
||||
|
||||
$this->actingAs($user, ApiTestCase::API_GUARD)
|
||||
->getJson('/api/settings')
|
||||
->assertOk()
|
||||
->assertJson(Setting::all());
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Resources\UserResource;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use App\Repositories\User\UserRepository;
|
||||
use App\Support\Enum\UserStatus;
|
||||
use Carbon\Carbon;
|
||||
use Tests\Feature\ApiTestCase;
|
||||
|
||||
test('unauthenticated', function () {
|
||||
$this->getJson('/api/stats')->assertStatus(401);
|
||||
});
|
||||
|
||||
test('get stats as admin', function () {
|
||||
DB::table('users')->delete();
|
||||
|
||||
$adminRole = Role::whereName('Admin')->first();
|
||||
|
||||
$user = User::factory()->create(['role_id' => $adminRole->id]);
|
||||
|
||||
$this->be($user, ApiTestCase::API_GUARD);
|
||||
|
||||
Carbon::setTestNow(Carbon::now()->startOfYear());
|
||||
|
||||
User::factory()->times(4)->create(['status' => UserStatus::ACTIVE]);
|
||||
|
||||
Carbon::setTestNow(null);
|
||||
|
||||
User::factory()->times(2)->create(['status' => UserStatus::BANNED]);
|
||||
|
||||
User::factory()->times(7)->create(['status' => UserStatus::UNCONFIRMED]);
|
||||
|
||||
$users = app(UserRepository::class);
|
||||
|
||||
$response = $this->getJson('/api/stats');
|
||||
|
||||
$usersPerMonth = $users->countOfNewUsersPerMonthPerRole(
|
||||
now()->subYear()->startOfMonth(),
|
||||
now()->endOfMonth()
|
||||
);
|
||||
|
||||
$latestRegistrations = $users->latest(7);
|
||||
|
||||
$response->assertOk()
|
||||
->assertJson([
|
||||
'users_per_month' => $usersPerMonth,
|
||||
'users_per_status' => [
|
||||
'total' => 14,
|
||||
'new' => $users->newUsersCount(),
|
||||
'banned' => 2,
|
||||
'unconfirmed' => 7,
|
||||
],
|
||||
'latest_registrations' => UserResource::collection($latestRegistrations)->resolve(),
|
||||
]);
|
||||
});
|
||||
|
||||
test('non admin users cannot get user stats', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user, ApiTestCase::API_GUARD)->getJson('/api/stats')->assertForbidden();
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
use App\Events\User\UpdatedByAdmin;
|
||||
use App\Models\User;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Tests\Feature\ApiTestCase;
|
||||
|
||||
test('upload user avatar unauthenticated', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->post("/api/users/{$user->id}/avatar")
|
||||
->assertStatus(401);
|
||||
});
|
||||
|
||||
test('upload avatar without permission', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user, ApiTestCase::API_GUARD)
|
||||
->post("/api/users/{$user->id}/avatar")
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('upload avatar image', function () {
|
||||
Event::fake([UpdatedByAdmin::class]);
|
||||
|
||||
Storage::fake('public');
|
||||
|
||||
$user = UserFactory::withPermissions('users.manage')->create();
|
||||
|
||||
$file = UploadedFile::fake()->image('avatar.png', 500, 500);
|
||||
|
||||
$response = $this->actingAs($user, ApiTestCase::API_GUARD)
|
||||
->post("/api/users/{$user->id}/avatar", ['file' => $file])
|
||||
->assertOk();
|
||||
|
||||
$avatar = $response->json('data.avatar');
|
||||
$this->assertNotNull($avatar);
|
||||
|
||||
$uploadedFile = str_replace(url(''), '', $avatar);
|
||||
$uploadedFile = ltrim($uploadedFile, '/storage/');
|
||||
|
||||
Storage::disk('public')->assertExists($uploadedFile);
|
||||
|
||||
[$width, $height] = getimagesizefromstring(
|
||||
Storage::disk('public')->get($uploadedFile)
|
||||
);
|
||||
|
||||
$this->assertEquals(160, $width);
|
||||
$this->assertEquals(160, $height);
|
||||
|
||||
Event::assertDispatched(UpdatedByAdmin::class);
|
||||
});
|
||||
|
||||
test('upload invalid image', function () {
|
||||
$user = UserFactory::withPermissions('users.manage')->create();
|
||||
|
||||
Storage::fake('public');
|
||||
|
||||
$file = UploadedFile::fake()->create('avatar.txt', 500);
|
||||
|
||||
$this->actingAs($user, ApiTestCase::API_GUARD)
|
||||
->post("/api/users/{$user->id}/avatar", ['file' => $file])
|
||||
->assertStatus(422)
|
||||
->assertJsonFragment([
|
||||
'file' => [
|
||||
trans('validation.image', ['attribute' => 'file']),
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
test('update avatar from external source', function () {
|
||||
Event::fake([UpdatedByAdmin::class]);
|
||||
|
||||
$user = UserFactory::withPermissions('users.manage')->create();
|
||||
|
||||
$url = 'http://google.com';
|
||||
|
||||
$this->actingAs($user, ApiTestCase::API_GUARD)
|
||||
->putJson("/api/users/{$user->id}/avatar/external", ['url' => $url])
|
||||
->assertOk()
|
||||
->assertJsonFragment(['avatar' => $url]);
|
||||
|
||||
Event::assertDispatched(UpdatedByAdmin::class);
|
||||
});
|
||||
|
||||
test('update avatar with invalid external source', function () {
|
||||
$user = UserFactory::withPermissions('users.manage')->create();
|
||||
|
||||
$this->actingAs($user, ApiTestCase::API_GUARD)
|
||||
->putJson("/api/users/{$user->id}/avatar/external", ['url' => 'foo'])
|
||||
->assertStatus(422);
|
||||
});
|
||||
|
||||
test('delete user avatar', function () {
|
||||
Event::fake([UpdatedByAdmin::class]);
|
||||
|
||||
$user = UserFactory::withPermissions('users.manage')->create();
|
||||
|
||||
$user->forceFill(['avatar' => 'http://google.com'])->save();
|
||||
|
||||
$this->actingAs($user, ApiTestCase::API_GUARD)
|
||||
->deleteJson("api/users/{$user->id}/avatar")
|
||||
->assertOk()
|
||||
->assertJsonFragment([
|
||||
'avatar' => url('assets/img/profile.png'), // default profile image
|
||||
]);
|
||||
|
||||
Event::assertDispatched(UpdatedByAdmin::class);
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Resources\SessionResource;
|
||||
use App\Models\User;
|
||||
use App\Repositories\Session\SessionRepository;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Faker\Generator;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\Feature\ApiTestCase;
|
||||
|
||||
test('get sessions unauthenticated', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->getJson("/api/users/{$user->id}/sessions")->assertStatus(401);
|
||||
});
|
||||
|
||||
test('get user sessions without permission', function () {
|
||||
$user = UserFactory::create();
|
||||
|
||||
$this->actingAs($user, ApiTestCase::API_GUARD)
|
||||
->getJson("/api/users/{$user->id}/sessions")
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('get user sessions', function () {
|
||||
config(['session.driver' => 'database']);
|
||||
|
||||
$user = UserFactory::withPermissions('users.manage')->create();
|
||||
|
||||
$sessions = generateUserApiSessions($user);
|
||||
|
||||
$this->actingAs($user, ApiTestCase::API_GUARD)
|
||||
->getJson("/api/users/{$user->id}/sessions")
|
||||
->assertOk()
|
||||
->assertJson([
|
||||
'data' => SessionResource::collection($sessions)->resolve(),
|
||||
]);
|
||||
});
|
||||
|
||||
function generateUserApiSessions(User $user, $count = 5)
|
||||
{
|
||||
$sessions = [];
|
||||
$faker = app(Generator::class);
|
||||
$lifetime = config('session.lifetime') - 1;
|
||||
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
array_push($sessions, [
|
||||
'id' => Str::random(40),
|
||||
'user_id' => $user->id,
|
||||
'ip_address' => $faker->ipv4,
|
||||
'user_agent' => $faker->userAgent,
|
||||
'payload' => Str::random(),
|
||||
'last_activity' => $faker->dateTimeBetween("-{$lifetime} minutes")->getTimestamp(),
|
||||
]);
|
||||
}
|
||||
|
||||
DB::table('sessions')->insert($sessions);
|
||||
|
||||
return app(SessionRepository::class)->getUserSessions($user->id);
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
use App\Events\User\TwoFactorDisabledByAdmin;
|
||||
use App\Events\User\TwoFactorEnabledByAdmin;
|
||||
use App\Http\Resources\UserResource;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use PragmaRX\Google2FA\Google2FA;
|
||||
use Tests\Feature\ApiTestCase;
|
||||
|
||||
test('update 2fa unathenticated', function () {
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->putJson("api/users/{$user->id}/2fa")
|
||||
->assertStatus(401);
|
||||
});
|
||||
|
||||
test('update 2fa without permission', function () {
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user, ApiTestCase::API_GUARD)
|
||||
->putJson("api/users/{$user->id}/2fa")
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('enable two factor auth for user', function () {
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
Event::fake([TwoFactorEnabledByAdmin::class]);
|
||||
|
||||
$user = UserFactory::withPermissions('users.manage')->create();
|
||||
|
||||
$this->actingAs($user, ApiTestCase::API_GUARD)->putJson("api/users/{$user->id}/2fa")
|
||||
->assertOk()
|
||||
->assertJson(['message' => 'Verification token sent.'])
|
||||
->assertJsonStructure([
|
||||
'message',
|
||||
'qrcode',
|
||||
]);
|
||||
|
||||
$this->assertTrue(
|
||||
DB::table('users')
|
||||
->where('id', $user->id)
|
||||
->whereNotNull('two_factor_secret')
|
||||
->exists()
|
||||
);
|
||||
|
||||
Event::assertNotDispatched(TwoFactorEnabledByAdmin::class);
|
||||
});
|
||||
|
||||
test('verify user app with correct code', function () {
|
||||
$google2fa = new Google2FA;
|
||||
$secret = encrypt($google2fa->generateSecretKey());
|
||||
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
Event::fake([TwoFactorEnabledByAdmin::class]);
|
||||
|
||||
$user = UserFactory::withPermissions('users.manage')->create();
|
||||
$user->two_factor_secret = $secret;
|
||||
$user->save();
|
||||
|
||||
$validCode = $google2fa->getCurrentOtp(decrypt($user->two_factor_secret));
|
||||
|
||||
$response = $this->actingAs($user, ApiTestCase::API_GUARD)
|
||||
->postJson("api/users/{$user->id}/2fa/verify", ['code' => $validCode]);
|
||||
|
||||
$updatedUser = (new UserResource($user->fresh()))->resolve();
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonFragment($updatedUser);
|
||||
|
||||
$this->assertTrue($user->fresh()->twoFactorEnabled());
|
||||
|
||||
Event::assertDispatched(TwoFactorEnabledByAdmin::class);
|
||||
});
|
||||
|
||||
test('verify user app with invalid token', function () {
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
$user = UserFactory::withPermissions('users.manage')->create();
|
||||
|
||||
$this->actingAs($user, ApiTestCase::API_GUARD)
|
||||
->postJson("api/users/{$user->id}/2fa/verify", ['code' => '123123'])
|
||||
->assertStatus(422)
|
||||
->assertJson([
|
||||
'message' => 'Invalid 2FA token.',
|
||||
]);
|
||||
|
||||
$this->assertTrue(
|
||||
DB::table('users')
|
||||
->where('id', $user->id)
|
||||
->whereNull('two_factor_confirmed_at')
|
||||
->exists()
|
||||
);
|
||||
});
|
||||
|
||||
test('enable two factor auth for user when it is already enabled', function () {
|
||||
$google2fa = new Google2FA;
|
||||
$secret = encrypt($google2fa->generateSecretKey());
|
||||
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
$user = UserFactory::withPermissions('users.manage')->create();
|
||||
$user->two_factor_secret = $secret;
|
||||
$user->two_factor_confirmed_at = Carbon::now();
|
||||
$user->save();
|
||||
|
||||
$this->actingAs($user, ApiTestCase::API_GUARD)
|
||||
->putJson("api/users/{$user->id}/2fa")
|
||||
->assertStatus(422)
|
||||
->assertJson([
|
||||
'message' => '2FA is already enabled for this user.',
|
||||
]);
|
||||
});
|
||||
|
||||
test('disable two factor auth for user', function () {
|
||||
$google2fa = new Google2FA;
|
||||
$secret = encrypt($google2fa->generateSecretKey());
|
||||
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
Event::fake([TwoFactorDisabledByAdmin::class]);
|
||||
|
||||
$user = UserFactory::withPermissions('users.manage')->create();
|
||||
$user->two_factor_secret = $secret;
|
||||
$user->two_factor_confirmed_at = Carbon::now();
|
||||
$user->save();
|
||||
|
||||
$this->be($user, ApiTestCase::API_GUARD);
|
||||
|
||||
$response = $this->deleteJson("api/users/{$user->id}/2fa");
|
||||
|
||||
$user = (new UserResource($user->fresh()))->resolve();
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonFragment($user);
|
||||
|
||||
Event::assertDispatched(TwoFactorDisabledByAdmin::class);
|
||||
});
|
||||
|
||||
test('disable 2fa for user when it is already disabled', function () {
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
$user = UserFactory::withPermissions('users.manage')->create();
|
||||
|
||||
$this->actingAs($user, ApiTestCase::API_GUARD)
|
||||
->deleteJson("api/users/{$user->id}/2fa")
|
||||
->assertStatus(422)
|
||||
->assertJson([
|
||||
'message' => '2FA is not enabled for this user.',
|
||||
]);
|
||||
});
|
||||
@@ -0,0 +1,228 @@
|
||||
<?php
|
||||
|
||||
use App\Events\User\Deleted;
|
||||
use App\Events\User\UpdatedByAdmin;
|
||||
use App\Http\Resources\UserResource;
|
||||
use App\Models\Country;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use App\Support\Enum\UserStatus;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Support\Arr;
|
||||
use Tests\Feature\ApiTestCase;
|
||||
|
||||
test('test only authenticated users can list all users', function () {
|
||||
$this->getJson('/api/users')->assertStatus(401);
|
||||
});
|
||||
|
||||
test('test get users without permission', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user, ApiTestCase::API_GUARD)
|
||||
->getJson('/api/users')
|
||||
->assertStatus(403);
|
||||
});
|
||||
|
||||
test('test paginate all users', function () {
|
||||
DB::table('users')->delete();
|
||||
|
||||
$user = login();
|
||||
|
||||
$users = User::factory()->times(20)->create();
|
||||
$users->push($user);
|
||||
|
||||
$response = $this->getJson('/api/users');
|
||||
|
||||
$transformed = UserResource::collection($users->sortBy('id')->take(20))->resolve();
|
||||
|
||||
$this->assertEquals($response->json('data'), $transformed);
|
||||
});
|
||||
|
||||
test('test paginate users with country included', function () {
|
||||
login();
|
||||
|
||||
$country = Country::factory()->create();
|
||||
|
||||
User::factory()->create(['country_id' => null]);
|
||||
User::factory()->create(['country_id' => $country->id]);
|
||||
|
||||
$response = $this->getJson('/api/users?include=country')
|
||||
->assertOk()
|
||||
->json();
|
||||
|
||||
$this->assertNull($response['data'][0]['country']);
|
||||
$this->assertNotNull($response['data'][1]['country_id']);
|
||||
});
|
||||
|
||||
test('test paginate users by status', function () {
|
||||
login();
|
||||
|
||||
User::factory()->times(2)->create(['status' => UserStatus::ACTIVE]);
|
||||
User::factory()->times(5)->create(['status' => UserStatus::BANNED]);
|
||||
|
||||
$response = $this->getJson('/api/users?filter[status]='.UserStatus::BANNED->value);
|
||||
|
||||
$this->assertCount(5, $response->json('data'));
|
||||
});
|
||||
|
||||
test('test paginate users on search', function () {
|
||||
$user = login();
|
||||
|
||||
$user1 = User::factory()->create([
|
||||
'first_name' => 'John',
|
||||
'last_name' => 'Doe',
|
||||
'email' => 'john.doe@vanguardapp.io',
|
||||
]);
|
||||
|
||||
$user2 = User::factory()->create([
|
||||
'first_name' => 'Jane',
|
||||
'last_name' => 'Doe',
|
||||
'email' => 'jane.doe@vanguardapp.io',
|
||||
]);
|
||||
|
||||
$user3 = User::factory()->create([
|
||||
'first_name' => 'Brad',
|
||||
'last_name' => 'Pitt',
|
||||
'email' => 'b.pitt@vanguardapp.io',
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/users?filter[search]=doe');
|
||||
|
||||
$this->assertCount(2, $response->json('data'));
|
||||
});
|
||||
|
||||
test('test create user', function () {
|
||||
login();
|
||||
|
||||
$newUser = User::factory()->make();
|
||||
|
||||
$data = array_merge($newUser->toArray(), [
|
||||
'birthday' => $newUser->birthday->format('Y-m-d'),
|
||||
'role' => $newUser->role_id,
|
||||
'password' => '123123123',
|
||||
'password_confirmation' => '123123123',
|
||||
]);
|
||||
|
||||
$response = $this->postJson('api/users', $data);
|
||||
|
||||
$expected = [
|
||||
'first_name' => $newUser->first_name,
|
||||
'last_name' => $newUser->last_name,
|
||||
'email' => $newUser->email,
|
||||
'username' => $newUser->username,
|
||||
'country_id' => $newUser->country_id,
|
||||
'birthday' => $newUser->birthday->format('Y-m-d'),
|
||||
'phone' => $newUser->phone,
|
||||
'address' => $newUser->address,
|
||||
'status' => UserStatus::ACTIVE,
|
||||
'role_id' => $newUser->role_id,
|
||||
];
|
||||
|
||||
$response->assertStatus(201)
|
||||
->assertJsonFragment($expected);
|
||||
|
||||
$this->assertDatabaseHas('users', $expected);
|
||||
});
|
||||
|
||||
test('test get user', function () {
|
||||
$user = login();
|
||||
|
||||
$this->getJson("api/users/{$user->id}")
|
||||
->assertOk()
|
||||
->assertJson([
|
||||
'data' => (new UserResource($user->fresh()))->resolve(),
|
||||
]);
|
||||
});
|
||||
|
||||
test('test get user which does not exist', function () {
|
||||
login();
|
||||
|
||||
$this->getJson('api/users/222')->assertStatus(404);
|
||||
});
|
||||
|
||||
test('test update user', function () {
|
||||
Event::fake([UpdatedByAdmin::class]);
|
||||
|
||||
$user = login();
|
||||
|
||||
$data = [
|
||||
'email' => 'john.doe@test.com',
|
||||
'username' => 'john.doe',
|
||||
'password' => '123123123',
|
||||
'password_confirmation' => '123123123',
|
||||
'first_name' => 'John',
|
||||
'last_name' => 'Doe',
|
||||
'phone' => '+38123456789',
|
||||
'address' => 'Some random address',
|
||||
'country_id' => Country::first()->id,
|
||||
'birthday' => '1990-10-18',
|
||||
'status' => UserStatus::BANNED,
|
||||
'role_id' => Role::whereName('User')->first()->id,
|
||||
];
|
||||
|
||||
$expected = Arr::except($data, ['password', 'password_confirmation']);
|
||||
$expected += ['id' => $user->id];
|
||||
|
||||
$this->patchJson("api/users/{$user->id}", $data)
|
||||
->assertOk()
|
||||
->assertJsonFragment($expected);
|
||||
|
||||
$this->assertDatabaseHas('users', $expected);
|
||||
|
||||
Event::assertDispatched(UpdatedByAdmin::class);
|
||||
});
|
||||
|
||||
test('test update only specific field', function () {
|
||||
Event::fake([UpdatedByAdmin::class]);
|
||||
|
||||
$user = login();
|
||||
|
||||
$data = ['email' => 'john.doe@test.com'];
|
||||
|
||||
$expected = array_merge(
|
||||
$user->toArray(),
|
||||
$data,
|
||||
['birthday' => $user->birthday->format('Y-m-d')]
|
||||
);
|
||||
|
||||
$expected = Arr::except($expected, ['created_at', 'updated_at', 'avatar', 'role']);
|
||||
|
||||
$this->patchJson("api/users/{$user->id}", $data)
|
||||
->assertOk()
|
||||
->assertJsonFragment($expected);
|
||||
|
||||
$this->assertDatabaseHas('users', $expected);
|
||||
|
||||
Event::assertDispatched(UpdatedByAdmin::class);
|
||||
});
|
||||
|
||||
test('test delete user', function () {
|
||||
Event::fake([Deleted::class]);
|
||||
|
||||
$user = login();
|
||||
|
||||
$user2 = User::factory()->create();
|
||||
|
||||
$this->deleteJson("api/users/{$user2->id}")
|
||||
->assertOk()
|
||||
->assertJson(['success' => true]);
|
||||
|
||||
Event::assertDispatched(Deleted::class);
|
||||
});
|
||||
|
||||
test('test delete yourself', function () {
|
||||
$user = login();
|
||||
|
||||
$this->deleteJson("api/users/{$user->id}")
|
||||
->assertStatus(403)
|
||||
->assertJson(['message' => 'You cannot delete yourself.']);
|
||||
});
|
||||
|
||||
function login()
|
||||
{
|
||||
$user = UserFactory::withPermissions('users.manage')->create();
|
||||
|
||||
test()->be($user, ApiTestCase::API_GUARD);
|
||||
|
||||
return $user;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Collection;
|
||||
use Tests\TestCase;
|
||||
use Tests\UpdatesSettings;
|
||||
|
||||
class ApiTestCase extends TestCase
|
||||
{
|
||||
use RefreshDatabase, UpdatesSettings;
|
||||
|
||||
const API_GUARD = 'sanctum';
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->artisan('db:seed');
|
||||
|
||||
$this->setSettings(['auth.expose_api' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
protected function login()
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->be($user, self::API_GUARD);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform provided collection of items.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function transformCollection(Collection $collection, $transformer)
|
||||
{
|
||||
$transformed = [];
|
||||
|
||||
foreach ($collection as $item) {
|
||||
$transformed[] = $transformer->transform($item);
|
||||
}
|
||||
|
||||
return $transformed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @return array<string, array{string}>
|
||||
*/
|
||||
dataset('templates', function () {
|
||||
return [
|
||||
'.env.example' => [__DIR__.'/../../.env.example'],
|
||||
'installer' => [__DIR__.'/../../app/Http/Controllers/Web/InstallController.php'],
|
||||
];
|
||||
});
|
||||
|
||||
test('template names a configured mailer', function (string $path) {
|
||||
$mailer = valueOf('MAIL_MAILER', $path);
|
||||
|
||||
$this->assertArrayHasKey(
|
||||
$mailer,
|
||||
config('mail.mailers'),
|
||||
"MAIL_MAILER=$mailer is not defined in config/mail.php."
|
||||
);
|
||||
})->with('templates');
|
||||
|
||||
test('template ships with demo mode off', function (string $path) {
|
||||
$this->assertSame('false', valueOf('DEMO_MODE', $path));
|
||||
})->with('templates');
|
||||
|
||||
test('template names a configured database connection', function (string $path) {
|
||||
$connection = valueOf('DB_CONNECTION', $path);
|
||||
|
||||
$this->assertArrayHasKey(
|
||||
$connection,
|
||||
config('database.connections'),
|
||||
"DB_CONNECTION=$connection is not defined in config/database.php."
|
||||
);
|
||||
})->with('templates');
|
||||
|
||||
/**
|
||||
* Read a single environment value out of a template.
|
||||
*/
|
||||
function valueOf(string $key, string $path): string
|
||||
{
|
||||
test()->assertFileExists($path);
|
||||
|
||||
$matched = preg_match('/^\s*'.preg_quote($key, '/').'=(.*)$/m', file_get_contents($path), $matches);
|
||||
|
||||
test()->assertSame(1, $matched, "$key is not present in ".basename($path).'.');
|
||||
|
||||
return trim($matches[1], " \t\"'");
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Testing\TestResponse;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('dashboard renders for authenticated admin', function () {
|
||||
$this->actingAsAdmin()
|
||||
->get('/')
|
||||
->assertOk()
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->component('Dashboard')
|
||||
->has('widgets')
|
||||
);
|
||||
});
|
||||
|
||||
test('dashboard hides user management widgets from roles without the permission', function () {
|
||||
$this->be(UserFactory::user()->create());
|
||||
|
||||
$components = widgetComponents($this->get('/')->assertOk());
|
||||
|
||||
$this->assertNotContains('StatWidget', $components);
|
||||
$this->assertNotContains('RegistrationHistoryWidget', $components);
|
||||
$this->assertNotContains('LatestRegistrationsWidget', $components);
|
||||
$this->assertNotContains('ApprovalNoticeWidget', $components);
|
||||
$this->assertContains('UserActionsWidget', $components);
|
||||
});
|
||||
|
||||
test('dashboard shows user management widgets to roles holding the permission', function () {
|
||||
$this->be(UserFactory::withPermissions('users.manage')->create());
|
||||
|
||||
$components = widgetComponents($this->get('/')->assertOk());
|
||||
|
||||
$this->assertContains('StatWidget', $components);
|
||||
$this->assertContains('RegistrationHistoryWidget', $components);
|
||||
$this->assertContains('LatestRegistrationsWidget', $components);
|
||||
});
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
function widgetComponents(TestResponse $response): array
|
||||
{
|
||||
return array_column($response->viewData('page')['props']['widgets'], 'component');
|
||||
}
|
||||
|
||||
test('dashboard redirects guest to login', function () {
|
||||
$this->get('/')->assertRedirect('login');
|
||||
});
|
||||
|
||||
test('dashboard flashes success when email verified session is set', function () {
|
||||
$this->actingAsAdmin()
|
||||
->withSession(['verified' => true])
|
||||
->get('/')
|
||||
->assertOk()
|
||||
->assertSessionHas('success', __('E-Mail verified successfully.'));
|
||||
});
|
||||
@@ -0,0 +1,375 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use App\Support\Demo;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
/**
|
||||
* The account UserSeeder creates, which config/demo.php protects by default.
|
||||
*/
|
||||
function demoAccount(): User
|
||||
{
|
||||
return User::where('username', 'admin')->firstOrFail();
|
||||
}
|
||||
|
||||
function enableDemoMode(): void
|
||||
{
|
||||
config(['demo.enabled' => true]);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Off by default
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
test('demo mode is off unless the deployment turns it on', function () {
|
||||
// phpunit.xml pins DEMO_MODE off, because the demo deployment's own .env
|
||||
// turns it on and the suite must not change meaning there. That the
|
||||
// shipped templates default to false is EnvironmentTemplateTest's job.
|
||||
$this->assertFalse(Demo::enabled());
|
||||
});
|
||||
|
||||
test('every write is reachable while demo mode is off', function () {
|
||||
$this->actingAsAdmin()
|
||||
->from('/settings/general')
|
||||
->post('/settings/general', ['app_name' => 'Renamed'])
|
||||
->assertRedirect('/settings/general');
|
||||
|
||||
$this->assertEquals('Renamed', Setting::get('app_name'));
|
||||
|
||||
$this->from('/users')
|
||||
->delete('users/'.demoAccount()->id)
|
||||
->assertRedirect('users');
|
||||
|
||||
$this->assertDatabaseMissing('users', ['username' => 'admin']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Blocked routes
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
test('blocked routes are refused with a flashed error', function (string $url, array $data) {
|
||||
enableDemoMode();
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/')
|
||||
->post($url, $data)
|
||||
->assertRedirect('/');
|
||||
|
||||
$this->assertSessionHasFlashError('This action is disabled in the demo.');
|
||||
})->with([
|
||||
'general settings' => ['/settings/general', ['app_name' => 'Renamed']],
|
||||
'auth settings' => ['/settings/auth', ['reg_enabled' => false]],
|
||||
'two factor toggle' => ['/settings/auth/2fa/enable', []],
|
||||
'permission matrix' => ['/permissions/save', ['roles' => []]],
|
||||
]);
|
||||
|
||||
test('a blocked settings write leaves the setting alone', function () {
|
||||
Setting::set('app_name', 'Vanguard');
|
||||
Setting::save();
|
||||
|
||||
enableDemoMode();
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/settings/general')
|
||||
->post('/settings/general', ['app_name' => 'Renamed']);
|
||||
|
||||
$this->assertEquals('Vanguard', Setting::get('app_name'));
|
||||
});
|
||||
|
||||
test('a blocked permission matrix write leaves the role permissions alone', function () {
|
||||
$role = Role::where('name', 'User')->firstOrFail();
|
||||
$before = $role->permissions()->pluck('id')->sort()->values()->all();
|
||||
|
||||
enableDemoMode();
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/permissions')
|
||||
->post('/permissions/save', ['roles' => [$role->id => []]]);
|
||||
|
||||
$this->assertSessionHasFlashError('This action is disabled in the demo.');
|
||||
$this->assertEquals($before, $role->fresh()->permissions()->pluck('id')->sort()->values()->all());
|
||||
});
|
||||
|
||||
test('a refused Inertia delete answers 303 so the browser does not repeat the verb', function () {
|
||||
enableDemoMode();
|
||||
|
||||
$this->actingAs(UserFactory::admin()->create())
|
||||
->from('/users')
|
||||
->withHeader('X-Inertia', 'true')
|
||||
->delete('users/'.demoAccount()->id)
|
||||
->assertStatus(303)
|
||||
->assertRedirect('/users');
|
||||
|
||||
$this->assertSessionHasFlashError('The demo account cannot be modified.');
|
||||
});
|
||||
|
||||
test('the refusal reaches the shared flash prop the layouts turn into a toast', function () {
|
||||
enableDemoMode();
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/settings/general')
|
||||
->post('/settings/general', ['app_name' => 'Renamed']);
|
||||
|
||||
// The flash is read by HandleInertiaRequests on the request the redirect
|
||||
// lands on, which is where the layout's watcher fires.
|
||||
$this->get('/settings')
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->where('flash.error', 'This action is disabled in the demo.')
|
||||
);
|
||||
});
|
||||
|
||||
test('reading a page is never blocked', function () {
|
||||
enableDemoMode();
|
||||
|
||||
$this->actingAsAdmin()->get('/settings')->assertOk();
|
||||
$this->get('/users/'.demoAccount()->id)->assertOk();
|
||||
$this->get('/permissions')->assertOk();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| The demo stays interactive
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
test('a visitor can still create and delete an ordinary user', function () {
|
||||
enableDemoMode();
|
||||
|
||||
$this->actingAs(demoAccount())
|
||||
->from('/users/create')
|
||||
->post('users', [
|
||||
'email' => 'jane@example.com',
|
||||
'password' => 'secret123',
|
||||
'password_confirmation' => 'secret123',
|
||||
'role_id' => Role::where('name', 'User')->value('id'),
|
||||
])
|
||||
->assertRedirect('users');
|
||||
|
||||
$this->assertSessionHasSuccess('User created successfully.');
|
||||
|
||||
$created = User::where('email', 'jane@example.com')->firstOrFail();
|
||||
|
||||
$this->from('/users')
|
||||
->delete('users/'.$created->id)
|
||||
->assertRedirect('users');
|
||||
|
||||
$this->assertSessionHasSuccess('User deleted successfully.');
|
||||
$this->assertDatabaseMissing('users', ['email' => 'jane@example.com']);
|
||||
});
|
||||
|
||||
test('the demo account can still create a role', function () {
|
||||
enableDemoMode();
|
||||
|
||||
$this->actingAs(demoAccount())
|
||||
->from('/roles/create')
|
||||
->post('roles', Role::factory()->raw(['name' => 'Editor']))
|
||||
->assertRedirect('roles');
|
||||
|
||||
$this->assertSessionHasSuccess('Role created successfully.');
|
||||
$this->assertDatabaseHas('roles', ['name' => 'Editor']);
|
||||
});
|
||||
|
||||
test('a visitor can still impersonate another user and stop again', function () {
|
||||
enableDemoMode();
|
||||
|
||||
$target = UserFactory::user()->create();
|
||||
|
||||
$this->actingAs(demoAccount())
|
||||
->get(route('impersonate', $target->id))
|
||||
->assertRedirect('/');
|
||||
|
||||
$this->assertTrue(auth()->user()->is($target));
|
||||
|
||||
$this->get(route('impersonate.leave'));
|
||||
|
||||
$this->assertTrue(auth()->user()->is(demoAccount()));
|
||||
});
|
||||
|
||||
test('impersonating does not lift the demo restrictions', function () {
|
||||
enableDemoMode();
|
||||
|
||||
$target = UserFactory::withPermissions('settings.general')->create();
|
||||
|
||||
$this->actingAs(demoAccount())->get(route('impersonate', $target->id));
|
||||
|
||||
$this->from('/settings/general')
|
||||
->post('/settings/general', ['app_name' => 'Renamed']);
|
||||
|
||||
$this->assertSessionHasFlashError('This action is disabled in the demo.');
|
||||
});
|
||||
|
||||
test('impersonating the demo account does not hand over its login details', function () {
|
||||
enableDemoMode();
|
||||
|
||||
// Taking over a protected account is allowed; changing it is not, because
|
||||
// the self service rules read the impersonated user rather than the one
|
||||
// who signed in.
|
||||
$this->actingAs(UserFactory::admin()->create())
|
||||
->get(route('impersonate', demoAccount()->id));
|
||||
|
||||
$this->assertTrue(auth()->user()->is(demoAccount()));
|
||||
|
||||
$this->from('/profile')
|
||||
->put('profile/login-details', [
|
||||
'email' => 'hijacked@example.com',
|
||||
'password' => 'hijacked123',
|
||||
'password_confirmation' => 'hijacked123',
|
||||
]);
|
||||
|
||||
$this->assertSessionHasFlashError('The demo account cannot be modified.');
|
||||
$this->assertEquals('admin@example.com', demoAccount()->email);
|
||||
});
|
||||
|
||||
test('logging in still works', function () {
|
||||
enableDemoMode();
|
||||
|
||||
$user = UserFactory::withCredentials('jane', 'secret123')->user()->create();
|
||||
|
||||
$this->post('login', ['username' => 'jane', 'password' => 'secret123'])
|
||||
->assertRedirect('/');
|
||||
|
||||
$this->assertAuthenticatedAs($user);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| The protected account
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
test('the demo account cannot be deleted', function () {
|
||||
enableDemoMode();
|
||||
|
||||
$this->actingAs(UserFactory::admin()->create())
|
||||
->from('/users')
|
||||
->delete('users/'.demoAccount()->id)
|
||||
->assertRedirect('/users');
|
||||
|
||||
$this->assertSessionHasFlashError('The demo account cannot be modified.');
|
||||
$this->assertDatabaseHas('users', ['username' => 'admin']);
|
||||
});
|
||||
|
||||
test('the demo account cannot have its login details changed by an administrator', function () {
|
||||
enableDemoMode();
|
||||
|
||||
$this->actingAs(UserFactory::admin()->create())
|
||||
->from('/users')
|
||||
->put('users/'.demoAccount()->id.'/update/login-details', [
|
||||
'email' => 'hijacked@example.com',
|
||||
'password' => 'hijacked123',
|
||||
'password_confirmation' => 'hijacked123',
|
||||
]);
|
||||
|
||||
$this->assertSessionHasFlashError('The demo account cannot be modified.');
|
||||
$this->assertEquals('admin@example.com', demoAccount()->email);
|
||||
});
|
||||
|
||||
test('the demo account cannot change its own login details', function () {
|
||||
enableDemoMode();
|
||||
|
||||
$this->actingAs(demoAccount())
|
||||
->from('/profile')
|
||||
->put('profile/login-details', [
|
||||
'email' => 'hijacked@example.com',
|
||||
'password' => 'hijacked123',
|
||||
'password_confirmation' => 'hijacked123',
|
||||
])
|
||||
->assertRedirect('/profile');
|
||||
|
||||
$this->assertSessionHasFlashError('The demo account cannot be modified.');
|
||||
$this->assertEquals('admin@example.com', demoAccount()->email);
|
||||
});
|
||||
|
||||
test('an ordinary visitor can still change their own login details', function () {
|
||||
enableDemoMode();
|
||||
|
||||
$user = UserFactory::user()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->from('/profile')
|
||||
->put('profile/login-details', [
|
||||
'email' => 'jane@example.com',
|
||||
'password' => 'secret123',
|
||||
'password_confirmation' => 'secret123',
|
||||
])
|
||||
->assertRedirect('/profile');
|
||||
|
||||
$this->assertSessionHasSuccess('Login details updated successfully.');
|
||||
$this->assertEquals('jane@example.com', $user->fresh()->email);
|
||||
});
|
||||
|
||||
test('protected accounts can be named by email instead of username', function () {
|
||||
enableDemoMode();
|
||||
config(['demo.protected_users' => ['admin@example.com']]);
|
||||
|
||||
$this->actingAs(UserFactory::admin()->create())
|
||||
->from('/users')
|
||||
->delete('users/'.demoAccount()->id);
|
||||
|
||||
$this->assertSessionHasFlashError('The demo account cannot be modified.');
|
||||
$this->assertDatabaseHas('users', ['username' => 'admin']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Seeded roles and permissions
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
test('a seeded role cannot be renamed', function () {
|
||||
enableDemoMode();
|
||||
|
||||
$role = Role::where('name', 'Admin')->firstOrFail();
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/roles')
|
||||
->put('roles/'.$role->id, Role::factory()->raw(['name' => 'Renamed']))
|
||||
->assertRedirect('/roles');
|
||||
|
||||
$this->assertSessionHasFlashError('Default roles and permissions cannot be modified in the demo.');
|
||||
$this->assertEquals('Admin', $role->fresh()->name);
|
||||
});
|
||||
|
||||
test('a role created during the demo can still be renamed', function () {
|
||||
enableDemoMode();
|
||||
|
||||
$role = Role::factory()->create(['name' => 'Editor']);
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/roles')
|
||||
->put('roles/'.$role->id, Role::factory()->raw(['name' => 'Renamed']))
|
||||
->assertRedirect('roles');
|
||||
|
||||
$this->assertSessionHasSuccess('Role updated successfully.');
|
||||
$this->assertEquals('Renamed', $role->fresh()->name);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
test('the configured message replaces every reason', function () {
|
||||
enableDemoMode();
|
||||
config(['demo.message' => 'Grab your own copy to try this.']);
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/settings/general')
|
||||
->post('/settings/general', ['app_name' => 'Renamed']);
|
||||
|
||||
$this->assertSessionHasFlashError('Grab your own copy to try this.');
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed --class=RolesSeeder');
|
||||
$this->artisan('db:seed --class=PermissionsSeeder');
|
||||
});
|
||||
|
||||
test('unverified users cannot impersonate other users', function () {
|
||||
$user = UserFactory::withPermissions('users.manage')->unverified()->create();
|
||||
|
||||
User::factory()->create();
|
||||
|
||||
$this->actingAs($user)->get('/users')->assertRedirect('/email/verify');
|
||||
});
|
||||
|
||||
test('a user with appropriate permission can impersonate other users from a user list page', function () {
|
||||
$user = UserFactory::withPermissions('users.manage')->create();
|
||||
|
||||
User::factory()->create();
|
||||
|
||||
$this->actingAs($user)->get('/users')->assertSee('Impersonate');
|
||||
});
|
||||
|
||||
test('a user dont see impersonate button next to his name in the user list', function () {
|
||||
$user = UserFactory::withPermissions('users.manage')->create();
|
||||
|
||||
$this->actingAs($user)->get('/user')->assertDontSee('Impersonate');
|
||||
});
|
||||
|
||||
test('clicking on impersonate button will impersonate the user', function () {
|
||||
$userA = UserFactory::withPermissions('users.manage')->create();
|
||||
$userB = UserFactory::user()->create();
|
||||
|
||||
$this->actingAs($userA)
|
||||
->get(route('impersonate', $userB))
|
||||
->assertRedirect('/');
|
||||
|
||||
$this->assertTrue(auth()->user()->is($userB));
|
||||
});
|
||||
|
||||
test('while impersonating user can stop impersonating by clicking on the header button', function () {
|
||||
$userA = UserFactory::withPermissions('users.manage')->create();
|
||||
$userB = UserFactory::user()->create();
|
||||
|
||||
$this->actingAs($userA)->get(route('impersonate', $userB));
|
||||
|
||||
$this->assertTrue(auth()->user()->is($userB));
|
||||
|
||||
$this->get('/')->assertInertia(fn (Assert $page) => $page
|
||||
->where('auth.impersonating', true)
|
||||
);
|
||||
|
||||
$this->get(route('impersonate.leave'));
|
||||
|
||||
$this->assertTrue(auth()->user()->is($userA));
|
||||
});
|
||||
|
||||
test('while impersonating user cannot impersonate other user even if he has a permission', function () {
|
||||
$userA = UserFactory::withPermissions('users.manage')->create();
|
||||
$userB = UserFactory::withPermissions('users.manage')->create();
|
||||
|
||||
$this->actingAs($userA)->get(route('impersonate', $userB));
|
||||
|
||||
$this->get('/user')
|
||||
->assertDontSee('Impersonate');
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
test('install start page is displayed', function () {
|
||||
$this->get('install')
|
||||
->assertOk()
|
||||
->assertViewIs('install.start');
|
||||
});
|
||||
|
||||
test('requirements page is displayed', function () {
|
||||
$this->get('install/requirements')
|
||||
->assertOk()
|
||||
->assertViewIs('install.requirements')
|
||||
->assertViewHas('requirements')
|
||||
->assertViewHas('allLoaded');
|
||||
});
|
||||
|
||||
test('permissions page redirects when requirements not met', function () {
|
||||
// Hard to fake unmet requirements, so just verify the route is reachable.
|
||||
$response = $this->get('install/permissions');
|
||||
|
||||
$this->assertContains(
|
||||
$response->getStatusCode(),
|
||||
[200, 302],
|
||||
'install/permissions should return 200 or redirect to requirements.'
|
||||
);
|
||||
});
|
||||
|
||||
test('database page redirects when prerequisites not met', function () {
|
||||
$response = $this->get('install/database');
|
||||
|
||||
$this->assertContains(
|
||||
$response->getStatusCode(),
|
||||
[200, 302],
|
||||
'install/database should return 200 or redirect.'
|
||||
);
|
||||
});
|
||||
|
||||
test('installation redirects with bad db credentials', function () {
|
||||
$this->post('install/start-installation', [
|
||||
'host' => '127.0.0.1',
|
||||
'username' => 'nope',
|
||||
'password' => 'nope',
|
||||
'database' => 'nope_no_database',
|
||||
'prefix' => '',
|
||||
])->assertRedirect();
|
||||
});
|
||||
|
||||
test('complete page is displayed', function () {
|
||||
$this->get('install/complete')
|
||||
->assertOk()
|
||||
->assertViewIs('install.complete');
|
||||
});
|
||||
|
||||
test('error page is displayed', function () {
|
||||
$this->get('install/error')
|
||||
->assertOk()
|
||||
->assertViewIs('install.error');
|
||||
});
|
||||
@@ -0,0 +1,286 @@
|
||||
<?php
|
||||
|
||||
use App\Events\User\LoggedIn;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Cache\RateLimiter;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Testing\TestResponse;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
use PragmaRX\Google2FA\Google2FA;
|
||||
use Tests\UpdatesSettings;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
uses(UpdatesSettings::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->setSettings([
|
||||
'throttle_enabled' => false,
|
||||
]);
|
||||
});
|
||||
|
||||
test('successful login', function () {
|
||||
$user = UserFactory::withCredentials('foo', 'bar')->create();
|
||||
|
||||
loginUser('foo', 'bar')
|
||||
->assertRedirect('/');
|
||||
|
||||
$this->assertTrue(auth()->check());
|
||||
$this->assertTrue($user->is(auth()->user()));
|
||||
});
|
||||
|
||||
test('last login timestamp is updated after successful login', function () {
|
||||
$testDate = Carbon::now();
|
||||
|
||||
Carbon::setTestNow($testDate);
|
||||
|
||||
$user = UserFactory::withCredentials('foo', 'bar')->create();
|
||||
|
||||
$this->assertNull($user->last_login);
|
||||
|
||||
loginUser('foo', 'bar');
|
||||
|
||||
$this->assertEquals($testDate->timestamp, $user->fresh()->last_login->timestamp);
|
||||
});
|
||||
|
||||
test('login with wrong credentials will fail', function () {
|
||||
loginUser('foo', 'bar')
|
||||
->assertRedirect('/login');
|
||||
|
||||
$this->assertFalse(auth()->check());
|
||||
});
|
||||
|
||||
test('country id remains the same after login', function () {
|
||||
$user = User::factory()->create([
|
||||
'username' => 'foo',
|
||||
'password' => 'bar',
|
||||
'country_id' => 688,
|
||||
]);
|
||||
|
||||
loginUser('foo', 'bar')
|
||||
->assertRedirect('/');
|
||||
|
||||
$this->assertEquals(688, $user->fresh()->country_id);
|
||||
});
|
||||
|
||||
test('throttling', function () {
|
||||
$this->setSettings([
|
||||
'throttle_enabled' => true,
|
||||
'throttle_attempts' => 3,
|
||||
'throttle_lockout_time' => 2, // 2 minutes
|
||||
]);
|
||||
|
||||
for ($i = 0; $i < 3; $i++) {
|
||||
loginUser('foo', 'bar');
|
||||
}
|
||||
|
||||
loginUser('foo', 'bar')
|
||||
->assertRedirect('login')
|
||||
->assertSessionHasErrors('username');
|
||||
|
||||
$this->assertTrue(app(RateLimiter::class)->tooManyAttempts('foo|127.0.0.1', 3));
|
||||
});
|
||||
|
||||
test('login with remember', function () {
|
||||
$user = UserFactory::withCredentials('foo', 'bar')->create();
|
||||
|
||||
Setting::set('remember_me', false);
|
||||
|
||||
$this->get('login')
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->component('auth/Login')
|
||||
->where('settings.remember_me', false)
|
||||
);
|
||||
|
||||
Setting::set('remember_me', true);
|
||||
|
||||
$this->get('login')
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->component('auth/Login')
|
||||
->where('settings.remember_me', true)
|
||||
);
|
||||
|
||||
loginUser('foo', 'bar', true)
|
||||
->assertRedirect('/');
|
||||
|
||||
$this->assertNotNull($user->fresh()->remember_token);
|
||||
$this->assertNotNull($user->fresh()->last_login);
|
||||
});
|
||||
|
||||
test('login max number of sessions reached', function () {
|
||||
$user = UserFactory::withCredentials('foo', 'bar')->create();
|
||||
|
||||
Setting::set('max_active_sessions', 1);
|
||||
|
||||
DB::table('sessions')->insert([
|
||||
'id' => Str::random(),
|
||||
'user_id' => $user->id,
|
||||
'ip_address' => fake()->ipv4,
|
||||
'user_agent' => fake()->userAgent,
|
||||
'payload' => 'test',
|
||||
'last_activity' => Carbon::now()->timestamp,
|
||||
]);
|
||||
|
||||
loginUser('foo', 'bar')
|
||||
->assertRedirect('/login');
|
||||
|
||||
$this->assertSessionHasError(trans('auth.max_sessions_reached'));
|
||||
|
||||
$this->assertFalse(Auth::check());
|
||||
});
|
||||
|
||||
test('banned user cannot log in', function () {
|
||||
UserFactory::withCredentials('foo', 'bar')->banned()->create();
|
||||
|
||||
loginUser('foo', 'bar')
|
||||
->assertRedirect('/login');
|
||||
|
||||
$this->assertSessionHasError('Your account is banned by administrator.');
|
||||
});
|
||||
|
||||
test('login with 2fa enabled', function () {
|
||||
$google2fa = new Google2FA;
|
||||
$secret = encrypt($google2fa->generateSecretKey());
|
||||
|
||||
$this->withoutExceptionHandling();
|
||||
$this->setSettings(['2fa.enabled' => true, 'throttle_enabled' => false]);
|
||||
|
||||
Event::fake([LoggedIn::class]);
|
||||
|
||||
$user = UserFactory::withCredentials('foo', 'bar')->create();
|
||||
$user->two_factor_secret = $secret;
|
||||
$user->two_factor_confirmed_at = Carbon::now();
|
||||
$user->save();
|
||||
|
||||
$validCode = $google2fa->getCurrentOtp(decrypt($user->two_factor_secret));
|
||||
|
||||
loginUser('foo', 'bar')
|
||||
->assertRedirect('auth/two-factor-authentication')
|
||||
->assertSessionHas('auth.2fa.id', $user->id);
|
||||
|
||||
$this->post('auth/two-factor-authentication', ['code' => $validCode])
|
||||
->assertRedirect('/');
|
||||
|
||||
$this->assertTrue(auth()->check());
|
||||
|
||||
Event::assertDispatched(LoggedIn::class);
|
||||
});
|
||||
|
||||
test('login with 2fa enabled redirect to custom page', function () {
|
||||
$google2fa = new Google2FA;
|
||||
$secret = encrypt($google2fa->generateSecretKey());
|
||||
|
||||
$this->withoutExceptionHandling();
|
||||
$this->setSettings(['2fa.enabled' => true, 'throttle_enabled' => false]);
|
||||
|
||||
Event::fake([LoggedIn::class]);
|
||||
|
||||
$user = UserFactory::withCredentials('foo', 'bar')->create();
|
||||
$user->two_factor_secret = $secret;
|
||||
$user->two_factor_confirmed_at = Carbon::now();
|
||||
$user->save();
|
||||
|
||||
$validCode = $google2fa->getCurrentOtp(decrypt($user->two_factor_secret));
|
||||
|
||||
loginUser('foo', 'bar', redirectTo: 'https://google.com')
|
||||
->assertRedirect('auth/two-factor-authentication')
|
||||
->assertSessionHas('auth.2fa.id', $user->id);
|
||||
|
||||
$this->post('auth/two-factor-authentication', ['code' => $validCode])
|
||||
->assertRedirect('https://google.com');
|
||||
|
||||
$this->assertTrue(auth()->check());
|
||||
|
||||
Event::assertDispatched(LoggedIn::class);
|
||||
});
|
||||
|
||||
test('login with wrong 2fa token', function () {
|
||||
$google2fa = new Google2FA;
|
||||
$secret = encrypt($google2fa->generateSecretKey());
|
||||
|
||||
$this->setSettings(['2fa.enabled' => true, 'throttle_enabled' => false]);
|
||||
|
||||
$user = UserFactory::withCredentials('foo', 'bar')->create();
|
||||
$user->two_factor_secret = $secret;
|
||||
$user->two_factor_confirmed_at = Carbon::now();
|
||||
$user->save();
|
||||
|
||||
loginUser('foo', 'bar')
|
||||
->assertRedirect('auth/two-factor-authentication')
|
||||
->assertSessionHas('auth.2fa.id', $user->id);
|
||||
|
||||
$this->post('auth/two-factor-authentication', ['code' => '123123'])->assertRedirect('login');
|
||||
|
||||
$this->assertSessionHasError('Invalid 2FA token.');
|
||||
});
|
||||
|
||||
test('login with wrong 2fa token when custom redirect page is provided', function () {
|
||||
$google2fa = new Google2FA;
|
||||
$secret = encrypt($google2fa->generateSecretKey());
|
||||
|
||||
$this->setSettings(['2fa.enabled' => true, 'throttle_enabled' => false]);
|
||||
|
||||
$user = UserFactory::withCredentials('foo', 'bar')->create();
|
||||
$user->two_factor_secret = $secret;
|
||||
$user->two_factor_confirmed_at = Carbon::now();
|
||||
$user->save();
|
||||
|
||||
loginUser('foo', 'bar', redirectTo: 'https://google.com')
|
||||
->assertRedirect('auth/two-factor-authentication')
|
||||
->assertSessionHas('auth.2fa.id', $user->id);
|
||||
|
||||
$this->post('auth/two-factor-authentication', ['code' => '123'])
|
||||
->assertRedirect('login?to=https://google.com');
|
||||
|
||||
$this->assertSessionHasError('Invalid 2FA token.');
|
||||
});
|
||||
|
||||
test('force password change on login', function () {
|
||||
$this->setSettings(['password-change.enabled' => true]);
|
||||
|
||||
$user = User::factory()->create([
|
||||
'username' => 'foo',
|
||||
'password' => 'bar',
|
||||
'force_password_change' => true,
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'id' => $user->id,
|
||||
'force_password_change' => true,
|
||||
]);
|
||||
|
||||
loginUser('foo', 'bar')
|
||||
->assertRedirect('/');
|
||||
|
||||
$this->get('/')->assertRedirect('/profile');
|
||||
|
||||
$this->from('/profile')
|
||||
->put('/profile/login-details', [
|
||||
'email' => 'john@doe.com',
|
||||
'username' => 'milos',
|
||||
'password' => '123123123',
|
||||
'password_confirmation' => '123123123',
|
||||
])
|
||||
->assertRedirect('/profile');
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'id' => $user->id,
|
||||
'force_password_change' => false,
|
||||
]);
|
||||
});
|
||||
|
||||
function loginUser($username, $password, $remember = false, $redirectTo = null): TestResponse
|
||||
{
|
||||
$url = 'login';
|
||||
|
||||
if ($redirectTo) {
|
||||
$url .= '?to='.urlencode($redirectTo);
|
||||
}
|
||||
|
||||
return test()->post($url, [
|
||||
'username' => $username,
|
||||
'password' => $password,
|
||||
'remember' => $remember,
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
use App\Mail\ResetPassword;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\PasswordReset;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
use Tests\UpdatesSettings;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
uses(UpdatesSettings::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->setSettings(['forgot_password' => true]);
|
||||
});
|
||||
|
||||
test('password request form is displayed', function () {
|
||||
$this->get('password/reset')
|
||||
->assertOk()
|
||||
->assertInertia(fn (Assert $page) => $page->component('auth/ForgotPassword'));
|
||||
});
|
||||
|
||||
test('reset link is sent for existing email', function () {
|
||||
Mail::fake();
|
||||
|
||||
$user = User::factory()->create(['email' => 'foo@bar.com']);
|
||||
|
||||
$this->from('password/reset')
|
||||
->post('password/email', ['email' => $user->email])
|
||||
->assertRedirect('password/reset');
|
||||
|
||||
Mail::assertQueued(ResetPassword::class, fn ($mail) => $mail->hasTo($user->email));
|
||||
});
|
||||
|
||||
test('reset link request with unknown email does not leak existence', function () {
|
||||
Mail::fake();
|
||||
|
||||
$this->from('password/reset')
|
||||
->post('password/email', ['email' => 'nobody@example.com'])
|
||||
->assertRedirect('password/reset')
|
||||
->assertSessionHasNoErrors()
|
||||
->assertSessionHas('success', trans('passwords.sent'));
|
||||
|
||||
Mail::assertNothingQueued();
|
||||
});
|
||||
|
||||
test('unknown email flashes a string the toast can render', function () {
|
||||
Mail::fake();
|
||||
|
||||
$this->from('password/reset')
|
||||
->post('password/email', ['email' => 'nobody@example.com'])
|
||||
->assertSessionHas('success', fn ($success) => is_string($success));
|
||||
|
||||
$this->get('password/reset')
|
||||
->assertInertia(fn (Assert $page) => $page->where('flash.success', trans('passwords.sent')));
|
||||
});
|
||||
|
||||
test('a throttled reset link request reports the failure against the email field', function () {
|
||||
Mail::fake();
|
||||
|
||||
$user = User::factory()->create(['email' => 'foo@bar.com']);
|
||||
|
||||
$this->post('password/email', ['email' => $user->email]);
|
||||
|
||||
$this->from('password/reset')
|
||||
->post('password/email', ['email' => $user->email])
|
||||
->assertRedirect('password/reset')
|
||||
->assertSessionHasErrors(['email' => trans('passwords.throttled')]);
|
||||
});
|
||||
|
||||
test('reset form is displayed with token', function () {
|
||||
$this->get('password/reset/sometoken?email=foo@bar.com')
|
||||
->assertOk()
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->component('auth/ResetPassword')
|
||||
->where('token', 'sometoken')
|
||||
->where('email', 'foo@bar.com')
|
||||
);
|
||||
});
|
||||
|
||||
test('password can be reset with valid token', function () {
|
||||
Event::fake([PasswordReset::class]);
|
||||
|
||||
$user = User::factory()->create(['email' => 'foo@bar.com']);
|
||||
$token = Password::broker()->createToken($user);
|
||||
|
||||
$this->post('password/reset', [
|
||||
'email' => $user->email,
|
||||
'token' => $token,
|
||||
'password' => 'new-password',
|
||||
'password_confirmation' => 'new-password',
|
||||
])->assertRedirect('/');
|
||||
|
||||
$this->assertTrue(Hash::check('new-password', $user->fresh()->password));
|
||||
$this->assertTrue(auth()->check());
|
||||
$this->assertTrue($user->is(auth()->user()));
|
||||
|
||||
Event::assertDispatched(PasswordReset::class);
|
||||
});
|
||||
|
||||
test('password cannot be reset with invalid token', function () {
|
||||
$user = User::factory()->create(['email' => 'foo@bar.com']);
|
||||
$originalHash = $user->password;
|
||||
|
||||
$this->from('password/reset/invalid-token')
|
||||
->post('password/reset', [
|
||||
'email' => $user->email,
|
||||
'token' => 'invalid-token',
|
||||
'password' => 'new-password',
|
||||
'password_confirmation' => 'new-password',
|
||||
])
|
||||
->assertRedirect('password/reset/invalid-token');
|
||||
|
||||
$this->assertSame($originalHash, $user->fresh()->password);
|
||||
$this->assertFalse(auth()->check());
|
||||
});
|
||||
|
||||
test('password reset requires password confirmation', function () {
|
||||
$user = User::factory()->create();
|
||||
$token = Password::broker()->createToken($user);
|
||||
|
||||
$this->from('password/reset/'.$token)
|
||||
->post('password/reset', [
|
||||
'email' => $user->email,
|
||||
'token' => $token,
|
||||
'password' => 'new-password',
|
||||
'password_confirmation' => 'different',
|
||||
])
|
||||
->assertSessionHasErrors('password');
|
||||
});
|
||||
|
||||
test('password reset routes unavailable when disabled', function () {
|
||||
$this->setSettings(['forgot_password' => false]);
|
||||
|
||||
$this->get('password/reset')->assertStatus(404);
|
||||
$this->post('password/email', ['email' => 'foo@bar.com'])->assertStatus(404);
|
||||
});
|
||||
@@ -0,0 +1,340 @@
|
||||
<?php
|
||||
|
||||
use App\Events\Permission\Created;
|
||||
use App\Events\Permission\Deleted;
|
||||
use App\Events\Permission\Updated;
|
||||
use App\Events\Role\PermissionsUpdated;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Role;
|
||||
use Facades\Tests\Setup\RoleFactory;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
|
||||
$this->be(UserFactory::admin()->create());
|
||||
});
|
||||
|
||||
test('permissions list', function () {
|
||||
$permission = Permission::factory()->create();
|
||||
|
||||
$this->get('permissions')
|
||||
->assertSee($permission->display_name);
|
||||
});
|
||||
|
||||
test('only users with appropriate permissions can access the permissions list page', function () {
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('permissions.manage')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$this->actingAs($userA)->get('/permissions')->assertStatus(403);
|
||||
$this->actingAs($userB)->get('/permissions')->assertOk();
|
||||
});
|
||||
|
||||
test('permission list with roles', function () {
|
||||
$permission = Permission::factory()->create();
|
||||
$role = Role::first();
|
||||
|
||||
$role->permissions()->attach($permission->id);
|
||||
|
||||
$this->get('permissions')
|
||||
->assertInertia(function (Assert $page) use ($permission, $role) {
|
||||
$page->component('permissions/Index');
|
||||
|
||||
$props = $page->toArray()['props'];
|
||||
|
||||
$this->assertContains(
|
||||
$permission->display_name,
|
||||
collect($props['permissions'])->pluck('display_name')->all()
|
||||
);
|
||||
|
||||
// The granted matrix replaces the roles[{id}][] checkbox names.
|
||||
$this->assertContains($permission->id, $props['granted'][$role->id]);
|
||||
});
|
||||
});
|
||||
|
||||
test('save role permissions', function () {
|
||||
$this->withoutExceptionHandling();
|
||||
|
||||
Event::fake([
|
||||
PermissionsUpdated::class,
|
||||
]);
|
||||
|
||||
$permission = Permission::factory()->create();
|
||||
$role = Role::factory()->create();
|
||||
|
||||
$this->post('permissions/save', [
|
||||
'roles' => [
|
||||
$role->id => [$permission->id],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertSessionHasSuccess('Permissions saved successfully.');
|
||||
|
||||
$this->assertDatabaseHas('permission_role', [
|
||||
'role_id' => $role->id,
|
||||
'permission_id' => $permission->id,
|
||||
]);
|
||||
|
||||
Event::assertDispatched(PermissionsUpdated::class);
|
||||
});
|
||||
|
||||
test('only users with appropriate permissions can save role permissions', function () {
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('permissions.manage')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$permission = Permission::factory()->create();
|
||||
$role = Role::factory()->create();
|
||||
|
||||
$data = [$role->id => [$permission->id]];
|
||||
|
||||
$this->actingAs($userA)
|
||||
->post('permissions/save', ['roles' => $data])
|
||||
->assertStatus(403);
|
||||
|
||||
$this->assertDatabaseMissing('permission_role', [
|
||||
'role_id' => $role->id,
|
||||
'permission_id' => $permission->id,
|
||||
]);
|
||||
|
||||
$this->actingAs($userB)
|
||||
->post('permissions/save', ['roles' => $data]);
|
||||
|
||||
$this->assertDatabaseHas('permission_role', [
|
||||
'role_id' => $role->id,
|
||||
'permission_id' => $permission->id,
|
||||
]);
|
||||
});
|
||||
|
||||
test('save role permissions if no permission is selected for specific role', function () {
|
||||
Event::fake([
|
||||
PermissionsUpdated::class,
|
||||
]);
|
||||
|
||||
$role1 = Role::factory()->create();
|
||||
$permission1 = Permission::factory()->create();
|
||||
$role1->permissions()->attach($permission1->id);
|
||||
|
||||
$role2 = Role::factory()->create();
|
||||
$permission2 = Permission::factory()->create();
|
||||
$role2->permissions()->attach($permission2->id);
|
||||
|
||||
$this->post('/permissions/save', [
|
||||
'roles' => [
|
||||
$role1->id => [$permission1->id],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertSessionHasSuccess('Permissions saved successfully.');
|
||||
|
||||
$this->assertDatabaseHas('permission_role', [
|
||||
'role_id' => $role1->id,
|
||||
'permission_id' => $permission1->id,
|
||||
]);
|
||||
|
||||
$this->assertDatabaseMissing('permission_role', [
|
||||
'role_id' => $role2->id,
|
||||
'permission_id' => $permission2->id,
|
||||
]);
|
||||
|
||||
Event::assertDispatched(PermissionsUpdated::class);
|
||||
});
|
||||
|
||||
test('create permission', function () {
|
||||
$this->app->instance('middleware.disable', false);
|
||||
|
||||
Event::fake([
|
||||
Created::class,
|
||||
]);
|
||||
|
||||
$data = validPermissionParams();
|
||||
|
||||
$this->post('permissions', $data)
|
||||
->assertRedirect('/permissions');
|
||||
|
||||
$this->assertSessionHasSuccess('Permission created successfully.');
|
||||
$this->assertDatabaseHas('permissions', $data);
|
||||
|
||||
Event::assertDispatched(Created::class);
|
||||
});
|
||||
|
||||
test('only users with appropriate permission can create new permissions', function () {
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('permissions.manage')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$data = validPermissionParams();
|
||||
|
||||
$this->actingAs($userA)
|
||||
->post('permissions', $data)
|
||||
->assertStatus(403);
|
||||
|
||||
$this->assertDatabaseMissing('permissions', $data);
|
||||
|
||||
$this->actingAs($userB)
|
||||
->post('permissions', $data)
|
||||
->assertRedirect('/permissions');
|
||||
|
||||
$this->assertDatabaseHas('permissions', $data);
|
||||
});
|
||||
|
||||
test('permission name must have valid format', function () {
|
||||
$this->app->instance('middleware.disable', false);
|
||||
|
||||
$data = validPermissionParams();
|
||||
|
||||
$response = $this->post('permissions', validPermissionParams(['name' => 'invalid name']));
|
||||
$response->assertSessionHasErrors('name');
|
||||
$this->assertDatabaseMissing('permissions', $data);
|
||||
|
||||
$response = $this->post('permissions', validPermissionParams(['name' => 'invalid*name']));
|
||||
$response->assertSessionHasErrors('name');
|
||||
$this->assertDatabaseMissing('permissions', $data);
|
||||
});
|
||||
|
||||
test('update permission', function () {
|
||||
$this->withoutExceptionHandling();
|
||||
|
||||
Event::fake([
|
||||
Updated::class,
|
||||
]);
|
||||
|
||||
$permission = Permission::factory()->create();
|
||||
|
||||
$this->get("permissions/{$permission->id}/edit")
|
||||
->assertOk()
|
||||
->assertSee($permission->id)
|
||||
->assertSee($permission->name)
|
||||
->assertSee($permission->display_name);
|
||||
|
||||
$data = validPermissionParams();
|
||||
|
||||
$this->put("permissions/{$permission->id}", $data);
|
||||
|
||||
$this->assertSessionHasSuccess('Permission updated successfully.');
|
||||
$this->assertDatabaseHas('permissions', $data + ['id' => $permission->id]);
|
||||
|
||||
Event::assertDispatched(Updated::class);
|
||||
});
|
||||
|
||||
test('only users with appropriate permission can update existing permissions', function () {
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('permissions.manage')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$permission = Permission::factory()->create();
|
||||
|
||||
$data = validPermissionParams();
|
||||
|
||||
$this->actingAs($userA)
|
||||
->put("permissions/{$permission->id}", $data)
|
||||
->assertStatus(403);
|
||||
|
||||
$this->assertEquals($permission->toArray(), $permission->fresh()->toArray());
|
||||
|
||||
$this->actingAs($userB)
|
||||
->put("permissions/{$permission->id}", $data)
|
||||
->assertRedirect('/permissions');
|
||||
|
||||
$permission->refresh();
|
||||
|
||||
$this->assertEquals($permission->name, $data['name']);
|
||||
$this->assertEquals($permission->display_name, $data['display_name']);
|
||||
$this->assertEquals($permission->description, $data['description']);
|
||||
});
|
||||
|
||||
test('removable attribute cannot be changed on update', function () {
|
||||
$permission = Permission::factory()->create(['removable' => false]);
|
||||
|
||||
$data = validPermissionParams(['removable' => true]);
|
||||
|
||||
$this->put("permissions/{$permission->id}", $data);
|
||||
|
||||
$permission->refresh();
|
||||
|
||||
$this->assertEquals($permission->name, $data['name']);
|
||||
$this->assertEquals($permission->display_name, $data['display_name']);
|
||||
$this->assertEquals($permission->description, $data['description']);
|
||||
$this->assertFalse($permission->removable);
|
||||
});
|
||||
|
||||
test('permission name must have valid format while updating the permission', function () {
|
||||
$permission = Permission::factory()->create(['name' => 'foo']);
|
||||
|
||||
$response = $this->put('permissions/'.$permission->id, validPermissionParams(['name' => 'invalid name']));
|
||||
$response->assertSessionHasErrors('name');
|
||||
$this->assertEquals('foo', $permission->fresh()->name);
|
||||
|
||||
$response = $this->put('permissions/'.$permission->id, validPermissionParams(['name' => 'invalid*name']));
|
||||
$response->assertSessionHasErrors('name');
|
||||
$this->assertEquals('foo', $permission->fresh()->name);
|
||||
});
|
||||
|
||||
test('delete permission', function () {
|
||||
Event::fake([
|
||||
Deleted::class,
|
||||
]);
|
||||
|
||||
$permission = Permission::factory()->create();
|
||||
|
||||
$this->delete(route('permissions.destroy', $permission->id))
|
||||
->assertRedirect('/permissions');
|
||||
|
||||
$this->assertSessionHasSuccess('Permission deleted successfully.');
|
||||
$this->assertDatabaseMissing('permissions', ['id' => $permission->id]);
|
||||
|
||||
Event::assertDispatched(Deleted::class);
|
||||
});
|
||||
|
||||
test('only users with appropriate permission can delete permissions', function () {
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('permissions.manage')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$permission = Permission::factory()->create();
|
||||
|
||||
$this->actingAs($userA)
|
||||
->delete("permissions/{$permission->id}")
|
||||
->assertStatus(403);
|
||||
|
||||
$this->assertNotNull($permission->fresh());
|
||||
|
||||
$this->actingAs($userB)
|
||||
->delete("permissions/{$permission->id}")
|
||||
->assertRedirect('/permissions');
|
||||
|
||||
$this->assertNull($permission->fresh());
|
||||
});
|
||||
|
||||
test('non removable permissions cannot be removed', function () {
|
||||
$permission = Permission::factory()->create(['removable' => false]);
|
||||
|
||||
$this->delete(route('permissions.destroy', $permission->id))
|
||||
->assertStatus(404);
|
||||
|
||||
$this->assertNotNull($permission->fresh());
|
||||
});
|
||||
|
||||
function validPermissionParams(array $override = []): array
|
||||
{
|
||||
return array_merge([
|
||||
'name' => 'foo_permission',
|
||||
'display_name' => 'Foo Permission',
|
||||
'description' => 'the description',
|
||||
], $override);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
config(['session.driver' => 'database']);
|
||||
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('user can see their active sessions', function () {
|
||||
$user = User::factory()->create();
|
||||
$sessionId = insertProfileSession($user);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get('profile/sessions')
|
||||
->assertOk()
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->component('users/Sessions')
|
||||
->where('adminView', false)
|
||||
->where('user.id', $user->id)
|
||||
->has('sessions', 1)
|
||||
->where('sessions.0.id', $sessionId)
|
||||
);
|
||||
});
|
||||
|
||||
test('user can invalidate their session', function () {
|
||||
$user = User::factory()->create();
|
||||
$sessionId = insertProfileSession($user);
|
||||
|
||||
$this->actingAs($user)
|
||||
->delete("profile/sessions/{$sessionId}/invalidate")
|
||||
->assertRedirect(route('profile.sessions'));
|
||||
|
||||
$this->assertDatabaseMissing('sessions', ['id' => $sessionId]);
|
||||
});
|
||||
|
||||
test('guest cannot access profile sessions', function () {
|
||||
$this->get('profile/sessions')->assertRedirect('login');
|
||||
});
|
||||
|
||||
function insertProfileSession(User $user): string
|
||||
{
|
||||
$id = Str::random();
|
||||
|
||||
DB::table('sessions')->insert([
|
||||
'id' => $id,
|
||||
'user_id' => $user->id,
|
||||
'ip_address' => fake()->ipv4,
|
||||
'user_agent' => fake()->userAgent,
|
||||
'payload' => 'foo',
|
||||
'last_activity' => Carbon::now()->timestamp,
|
||||
]);
|
||||
|
||||
return $id;
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
|
||||
use App\Events\User\Approved;
|
||||
use App\Mail\UserRegistered;
|
||||
use App\Models\User;
|
||||
use App\Support\Enum\UserStatus;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Auth\Notifications\VerifyEmail;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
use Tests\UpdatesSettings;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
uses(UpdatesSettings::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('registration form is displayed when registration is enabled', function () {
|
||||
$this->setSettings(['reg_enabled' => true]);
|
||||
|
||||
$this->get('register')
|
||||
->assertOk()
|
||||
->assertInertia(fn (Assert $page) => $page->component('auth/Register'));
|
||||
});
|
||||
|
||||
test('registration form exposes the captcha sitekey only when captcha is enabled', function () {
|
||||
$this->setSettings(['reg_enabled' => true, 'registration.captcha.enabled' => false]);
|
||||
|
||||
$this->get('register')
|
||||
->assertInertia(fn (Assert $page) => $page->where('captchaSitekey', null));
|
||||
|
||||
$this->setSettings(['registration.captcha.enabled' => true]);
|
||||
config(['captcha.sitekey' => 'test-sitekey']);
|
||||
|
||||
$this->get('register')
|
||||
->assertInertia(fn (Assert $page) => $page->where('captchaSitekey', 'test-sitekey'));
|
||||
});
|
||||
|
||||
test('when registration is disabled a visitor cannot see the registration form', function () {
|
||||
$this->setSettings(['reg_enabled' => false]);
|
||||
|
||||
$this->get('register')->assertStatus(404);
|
||||
|
||||
$this->get('login')->assertDontSee('You don\'t have an account?');
|
||||
});
|
||||
|
||||
test('registration with approval enabled', function () {
|
||||
$this->setSettings([
|
||||
'reg_enabled' => true,
|
||||
'approval.enabled' => true,
|
||||
'reg_email_confirmation' => true,
|
||||
'registration.captcha.enabled' => false,
|
||||
'tos' => true,
|
||||
]);
|
||||
|
||||
Notification::fake();
|
||||
Event::fake([
|
||||
Approved::class,
|
||||
]);
|
||||
|
||||
$data = getRegistrationFormStubData();
|
||||
|
||||
$this->post('/register', $data)->assertRedirect('/login');
|
||||
|
||||
$expected = Arr::except($data, ['password', 'password_confirmation', 'tos']);
|
||||
$expected += ['status' => UserStatus::UNCONFIRMED];
|
||||
|
||||
$this->assertDatabaseHas('users', $expected);
|
||||
|
||||
$user = User::where('email', $data['email'])->first();
|
||||
$user->email_verified_at = now();
|
||||
$user->status = UserStatus::WAITING_APPROVAL;
|
||||
$user->save();
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from("/users/{$user->id}/edit")
|
||||
->put("/users/{$user->id}/update/approve")
|
||||
->assertRedirect("users/{$user->id}/edit");
|
||||
|
||||
$expected = array_merge($expected, ['status' => UserStatus::ACTIVE]);
|
||||
$this->assertDatabaseHas('users', $expected);
|
||||
|
||||
Event::assertDispatched(Approved::class);
|
||||
Notification::assertSentTo($user, VerifyEmail::class);
|
||||
});
|
||||
|
||||
test('registration with email confirmation', function () {
|
||||
$this->setSettings([
|
||||
'reg_enabled' => true,
|
||||
'approval.enabled' => false,
|
||||
'reg_email_confirmation' => true,
|
||||
'registration.captcha.enabled' => false,
|
||||
'tos' => true,
|
||||
]);
|
||||
|
||||
Notification::fake();
|
||||
|
||||
$data = getRegistrationFormStubData();
|
||||
|
||||
$this->post('/register', $data)->assertRedirect('/');
|
||||
|
||||
$expected = Arr::except($data, ['password', 'password_confirmation', 'tos']);
|
||||
$expected += ['status' => UserStatus::UNCONFIRMED];
|
||||
|
||||
$this->assertDatabaseHas('users', $expected);
|
||||
|
||||
$user = User::where('email', $data['email'])->first();
|
||||
|
||||
Notification::assertSentTo($user, VerifyEmail::class);
|
||||
});
|
||||
|
||||
test('registration without email confirmation', function () {
|
||||
$this->setSettings([
|
||||
'reg_enabled' => true,
|
||||
'reg_email_confirmation' => false,
|
||||
'notifications_signup_email' => false,
|
||||
'registration.captcha.enabled' => false,
|
||||
'tos' => true,
|
||||
]);
|
||||
|
||||
Notification::fake();
|
||||
|
||||
$data = getRegistrationFormStubData();
|
||||
|
||||
$this->post('/register', $data)->assertRedirect('/');
|
||||
|
||||
$expected = Arr::except($data, ['password', 'password_confirmation', 'tos']);
|
||||
$expected += ['status' => UserStatus::ACTIVE];
|
||||
|
||||
$this->assertDatabaseHas('users', $expected);
|
||||
|
||||
Notification::assertNotSentTo(
|
||||
User::where('email', $data['email'])->first(),
|
||||
VerifyEmail::class
|
||||
);
|
||||
});
|
||||
|
||||
test('email notification is being sent when new user registers', function () {
|
||||
$this->setSettings([
|
||||
'app_name' => 'foo',
|
||||
'reg_enabled' => true,
|
||||
'reg_email_confirmation' => false,
|
||||
'notifications_signup_email' => true,
|
||||
'registration.captcha.enabled' => false,
|
||||
'tos' => true,
|
||||
]);
|
||||
|
||||
Mail::fake();
|
||||
|
||||
$admin = UserFactory::admin()->email('john.doe@test.com')->create();
|
||||
$user1 = UserFactory::user()->email('jane.doe@test.com')->create();
|
||||
$user2 = UserFactory::user()->email('josh.doe@test.com')->create();
|
||||
|
||||
$this->post('/register', getRegistrationFormStubData());
|
||||
|
||||
Mail::assertQueued(UserRegistered::class, 2);
|
||||
});
|
||||
|
||||
test('redirect to custom page after login', function () {
|
||||
UserFactory::withCredentials('foo', 'bar')->create();
|
||||
|
||||
$this->post('/login', [
|
||||
'username' => 'foo',
|
||||
'password' => 'bar',
|
||||
'to' => 'http://www.google.com',
|
||||
])->assertRedirect('http://www.google.com');
|
||||
});
|
||||
|
||||
test('custom redirect page is available after failed login attempt', function () {
|
||||
$to = 'http://www.google.com';
|
||||
|
||||
$this->post('/login', [
|
||||
'username' => 'foo',
|
||||
'password' => 'bar',
|
||||
'to' => 'http://www.google.com',
|
||||
])->assertRedirect('login?to='.$to);
|
||||
});
|
||||
|
||||
test('access to auth pages is not allowed for authenticated users', function () {
|
||||
$this->setSettings([
|
||||
'reg_enabled' => true,
|
||||
'2fa.enabled' => true,
|
||||
'forgot_password' => true,
|
||||
]);
|
||||
|
||||
$user = UserFactory::withCredentials('foo', 'bar')->create();
|
||||
$this->be($user);
|
||||
|
||||
$forbiddenGetRoutes = [
|
||||
'login', 'register', 'password/reset', 'password/reset/123',
|
||||
'auth/facebook/login', 'auth/facebook/callback',
|
||||
];
|
||||
|
||||
foreach ($forbiddenGetRoutes as $route) {
|
||||
$this->get($route)->assertRedirect('/');
|
||||
}
|
||||
|
||||
$this->get('auth/two-factor-authentication')->assertRedirect('/login');
|
||||
});
|
||||
|
||||
function getRegistrationFormStubData()
|
||||
{
|
||||
return [
|
||||
'email' => 'test@test.com',
|
||||
'username' => 'johndoe',
|
||||
'password' => '123123123',
|
||||
'password_confirmation' => '123123123',
|
||||
'tos' => 1,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Role;
|
||||
use Facades\Tests\Setup\RoleFactory;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('guests cannot view role list', function () {
|
||||
$this->get('/roles')->assertRedirect('/login');
|
||||
});
|
||||
|
||||
test('users without appropriate permission cannot view role list', function () {
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('roles.manage')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$this->actingAs($userA)->get('/roles')->assertStatus(403);
|
||||
$this->actingAs($userB)->get('/roles')->assertOk();
|
||||
});
|
||||
|
||||
test('roles list is displayed properly', function () {
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::create();
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->get('/roles')
|
||||
->assertInertia(function (Assert $page) use ($roleA, $roleB) {
|
||||
// 2 default roles are created when db is seeded
|
||||
$page->component('roles/Index')->has('roles', 4);
|
||||
|
||||
$names = collect($page->toArray()['props']['roles'])->pluck('name');
|
||||
|
||||
$this->assertTrue($names->contains($roleA->name));
|
||||
$this->assertTrue($names->contains($roleB->name));
|
||||
});
|
||||
});
|
||||
|
||||
test('create role', function () {
|
||||
$data = Role::factory()->raw();
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->post('/roles', $data)
|
||||
->assertRedirect('/roles');
|
||||
|
||||
$this->assertSessionHasSuccess('Role created successfully.');
|
||||
$this->assertDatabaseHas('roles', $data);
|
||||
});
|
||||
|
||||
test('users without appropriate permission cannot create new roles', function () {
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('roles.manage')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$data = Role::factory()->raw();
|
||||
|
||||
$this->actingAs($userA)
|
||||
->post('/roles', $data)
|
||||
->assertStatus(403);
|
||||
|
||||
$this->assertDatabaseMissing('roles', $data);
|
||||
|
||||
$this->actingAs($userB)
|
||||
->post('/roles', $data)
|
||||
->assertRedirect('roles');
|
||||
|
||||
$this->assertDatabaseHas('roles', $data);
|
||||
});
|
||||
|
||||
test('update role', function () {
|
||||
$role = Role::factory()->create(['name' => 'foo']);
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->get("roles/{$role->id}/edit")
|
||||
->assertOk()
|
||||
->assertSee($role->name)
|
||||
->assertSee($role->display_name)
|
||||
->assertSee($role->description);
|
||||
|
||||
$data = Role::factory()->raw();
|
||||
|
||||
$this->put("/roles/{$role->id}", $data);
|
||||
|
||||
$this->assertSessionHasSuccess('Role updated successfully.');
|
||||
$this->assertDatabaseHas('roles', $data + ['id' => $role->id]);
|
||||
});
|
||||
|
||||
test('users without appropriate permission cannot update role', function () {
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('roles.manage')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$role = Role::factory()->create(['name' => 'foo']);
|
||||
|
||||
$data = Role::factory()->raw();
|
||||
|
||||
$this->actingAs($userA)
|
||||
->put("/roles/{$role->id}", $data)
|
||||
->assertStatus(403);
|
||||
|
||||
$this->assertEquals($role->toArray(), $role->fresh()->toArray());
|
||||
|
||||
$this->actingAs($userB)
|
||||
->put("/roles/{$role->id}", $data)
|
||||
->assertRedirect('roles');
|
||||
|
||||
$role->refresh();
|
||||
|
||||
$this->assertEquals($role->name, $data['name']);
|
||||
$this->assertEquals($role->display_name, $data['display_name']);
|
||||
$this->assertEquals($role->description, $data['description']);
|
||||
});
|
||||
|
||||
test('removable attribute cannot be changed on update', function () {
|
||||
$role = RoleFactory::unremovable()->create();
|
||||
|
||||
$data = Role::factory()->raw(['removable' => true]);
|
||||
|
||||
$this->actingAsAdmin()->put("/roles/{$role->id}", $data);
|
||||
|
||||
$role->refresh();
|
||||
|
||||
$this->assertEquals($role->name, $data['name']);
|
||||
$this->assertEquals($role->display_name, $data['display_name']);
|
||||
$this->assertEquals($role->description, $data['description']);
|
||||
$this->assertFalse($role->removable);
|
||||
});
|
||||
|
||||
test('delete role', function () {
|
||||
$role = RoleFactory::removable()->create();
|
||||
|
||||
$this->actingAsAdmin()->delete(route('roles.destroy', $role));
|
||||
|
||||
$this->assertNull($role->fresh());
|
||||
});
|
||||
|
||||
test('users without appropriate permission cannot delete role', function () {
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('roles.manage')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$role = Role::factory()->create(['name' => 'foo']);
|
||||
|
||||
$this->actingAs($userA)
|
||||
->delete(route('roles.destroy', $role->id))
|
||||
->assertStatus(403);
|
||||
|
||||
$this->assertNotNull($role->fresh());
|
||||
|
||||
$this->actingAs($userB)
|
||||
->delete(route('roles.destroy', $role->id))
|
||||
->assertRedirect('roles');
|
||||
|
||||
$this->assertNull($role->fresh());
|
||||
});
|
||||
|
||||
test('users receive default role after their role is deleted', function () {
|
||||
$user = UserFactory::create();
|
||||
$role = Role::factory()->create(['removable' => true]);
|
||||
$userRole = Role::where('name', 'User')->first();
|
||||
|
||||
$user->setRole($role);
|
||||
|
||||
$this->assertTrue($user->fresh()->hasRole($role->name));
|
||||
|
||||
$this->actingAsAdmin()->delete(route('roles.destroy', $role));
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'role_id' => $userRole->id,
|
||||
'id' => $user->id,
|
||||
]);
|
||||
|
||||
$user = $user->fresh();
|
||||
|
||||
$this->assertFalse($user->hasRole($role->name));
|
||||
$this->assertTrue($user->hasRole($userRole->name));
|
||||
});
|
||||
|
||||
test('only removable roles can be deleted', function () {
|
||||
$removableRole = Role::factory()->create(['removable' => true]);
|
||||
$nonRemovableRole = Role::factory()->create(['removable' => false]);
|
||||
|
||||
$this->beAdmin();
|
||||
|
||||
$this->delete(route('roles.destroy', $removableRole->id));
|
||||
|
||||
$this->assertNull($removableRole->fresh());
|
||||
|
||||
$this->delete(route('roles.destroy', $nonRemovableRole->id))
|
||||
->assertStatus(404);
|
||||
|
||||
$this->assertNotNull($nonRemovableRole->fresh());
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('enable two factor', function () {
|
||||
Setting::set('approval.enabled', false);
|
||||
|
||||
$this->assertFalse(Setting::get('approval.enabled'));
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/settings/auth')
|
||||
->post('/settings/auth/approval/enable')
|
||||
->assertRedirect('/settings/auth');
|
||||
|
||||
$this->assertTrue(Setting::get('approval.enabled'));
|
||||
});
|
||||
|
||||
test('disable two factor', function () {
|
||||
Setting::set('approval.enabled', true);
|
||||
|
||||
$this->assertTrue(Setting::get('approval.enabled'));
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/settings/auth')
|
||||
->post('/settings/auth/approval/disable')
|
||||
->assertRedirect('/settings/auth');
|
||||
|
||||
$this->assertFalse(Setting::get('approval.enabled'));
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
use Facades\Tests\Setup\RoleFactory;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('auth settings page can be rendered', function () {
|
||||
$this->actingAsAdmin()
|
||||
->get('/settings/auth')
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
test('update auth settings', function () {
|
||||
Setting::set('app_name', 'bar');
|
||||
|
||||
$data = authSettingsData();
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/settings/auth')
|
||||
->post('/settings/auth', $data)
|
||||
->assertRedirect('/settings/auth');
|
||||
|
||||
assertAuthSettingsWereUpdated($data);
|
||||
});
|
||||
|
||||
test('only users with appropriate permission can update auth settings', function () {
|
||||
Setting::set('app_name', 'foo');
|
||||
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('settings.auth')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$data = authSettingsData();
|
||||
|
||||
$data['app_name'] = 'bar';
|
||||
|
||||
$this->actingAs($userA)
|
||||
->from('/settings/auth')
|
||||
->post('/settings/auth', $data)
|
||||
->assertStatus(403);
|
||||
|
||||
$this->assertNotEquals('bar', (string) Setting::get('app_name'));
|
||||
|
||||
$this->actingAs($userB)
|
||||
->from('/settings/auth')
|
||||
->post('/settings/auth', $data)
|
||||
->assertRedirect('/settings/auth');
|
||||
|
||||
$this->assertEquals('bar', (string) Setting::get('app_name'));
|
||||
});
|
||||
|
||||
function assertAuthSettingsWereUpdated(array $data)
|
||||
{
|
||||
test()->assertEquals((string) $data['remember_me'], (string) Setting::get('remember_me'));
|
||||
test()->assertEquals((string) $data['forgot_password'], (string) Setting::get('forgot_password'));
|
||||
test()->assertEquals((string) $data['login_reset_token_lifetime'], (string) Setting::get('login_reset_token_lifetime'));
|
||||
test()->assertEquals((string) $data['throttle_enabled'], (string) Setting::get('throttle_enabled'));
|
||||
test()->assertEquals((string) $data['throttle_attempts'], (string) Setting::get('throttle_attempts'));
|
||||
test()->assertEquals((string) $data['throttle_lockout_time'], (string) Setting::get('throttle_lockout_time'));
|
||||
test()->assertEquals((string) $data['reg_enabled'], (string) Setting::get('reg_enabled'));
|
||||
test()->assertEquals((string) $data['tos'], (string) Setting::get('tos'));
|
||||
test()->assertEquals((string) $data['reg_email_confirmation'], (string) Setting::get('reg_email_confirmation'));
|
||||
}
|
||||
|
||||
function authSettingsData(): array
|
||||
{
|
||||
return [
|
||||
'remember_me' => 1,
|
||||
'forgot_password' => 1,
|
||||
'login_reset_token_lifetime' => 123,
|
||||
'throttle_enabled' => 1,
|
||||
'throttle_attempts' => 10,
|
||||
'throttle_lockout_time' => 2,
|
||||
'reg_enabled' => 1,
|
||||
'tos' => 1,
|
||||
'reg_email_confirmation' => 1,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('enable captcha', function () {
|
||||
Setting::set('registration.captcha.enabled', false);
|
||||
|
||||
$this->assertFalse(Setting::get('registration.captcha.enabled'));
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/settings/auth')
|
||||
->post('/settings/auth/registration/captcha/enable')
|
||||
->assertRedirect('/settings/auth');
|
||||
|
||||
$this->assertTrue(Setting::get('registration.captcha.enabled'));
|
||||
});
|
||||
|
||||
test('disable two factor', function () {
|
||||
Setting::set('registration.captcha.enabled', true);
|
||||
|
||||
$this->assertTrue(Setting::get('registration.captcha.enabled'));
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/settings/auth')
|
||||
->post('/settings/auth/registration/captcha/disable')
|
||||
->assertRedirect('/settings/auth');
|
||||
|
||||
$this->assertFalse(Setting::get('registration.captcha.enabled'));
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
use Facades\Tests\Setup\RoleFactory;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('update general settings', function () {
|
||||
Setting::set('app_name', 'bar');
|
||||
|
||||
$this->assertEquals('bar', Setting::get('app_name'));
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/settings/general')
|
||||
->post('/settings/general', ['app_name' => 'foo'])
|
||||
->assertRedirect('/settings/general');
|
||||
|
||||
$this->assertEquals('foo', Setting::get('app_name'));
|
||||
});
|
||||
|
||||
test('only users with appropriate permission can update general settings', function () {
|
||||
Setting::set('app_name', 'bar');
|
||||
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('settings.general')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$this->actingAs($userA)
|
||||
->from('/settings/general')
|
||||
->post('/settings/general', ['app_name' => 'foo'])
|
||||
->assertStatus(403);
|
||||
|
||||
$this->assertEquals('bar', Setting::get('app_name'));
|
||||
|
||||
$this->actingAs($userB)
|
||||
->from('/settings/general')
|
||||
->post('/settings/general', ['app_name' => 'foo'])
|
||||
->assertRedirect('/settings/general');
|
||||
|
||||
$this->assertEquals('foo', Setting::get('app_name'));
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
use App\Events\Settings\Updated as SettingsUpdated;
|
||||
use Facades\Tests\Setup\RoleFactory;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('notifications settings page is displayed', function () {
|
||||
Setting::set('notifications_signup_email', 1);
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->get('/settings/notifications')
|
||||
->assertOk()
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->component('settings/Notifications')
|
||||
->where('values.notifications_signup_email', 1)
|
||||
);
|
||||
});
|
||||
|
||||
test('update notifications settings', function () {
|
||||
Setting::set('notifications_signup_email', 0);
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/settings/notifications')
|
||||
->post('/settings/notifications', ['notifications_signup_email' => 1])
|
||||
->assertRedirect('/settings/notifications');
|
||||
|
||||
$this->assertEquals(1, Setting::get('notifications_signup_email'));
|
||||
});
|
||||
|
||||
test('updating notifications settings dispatches the settings updated event', function () {
|
||||
Event::fake([SettingsUpdated::class]);
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/settings/notifications')
|
||||
->post('/settings/notifications', ['notifications_signup_email' => 1])
|
||||
->assertRedirect('/settings/notifications');
|
||||
|
||||
Event::assertDispatched(SettingsUpdated::class);
|
||||
});
|
||||
|
||||
test('only users with appropriate permission can view notifications settings', function () {
|
||||
$userA = UserFactory::role(RoleFactory::create())->create();
|
||||
$userB = UserFactory::role(RoleFactory::withPermissions('settings.notifications')->create())->create();
|
||||
|
||||
$this->actingAs($userA)->get('/settings/notifications')->assertStatus(403);
|
||||
|
||||
$this->actingAs($userB)->get('/settings/notifications')->assertOk();
|
||||
});
|
||||
|
||||
test('only users with appropriate permission can update notifications settings', function () {
|
||||
Setting::set('notifications_signup_email', 0);
|
||||
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('settings.notifications')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$this->actingAs($userA)
|
||||
->from('/settings/notifications')
|
||||
->post('/settings/notifications', ['notifications_signup_email' => 1])
|
||||
->assertStatus(403);
|
||||
|
||||
$this->assertEquals(0, Setting::get('notifications_signup_email'));
|
||||
|
||||
$this->actingAs($userB)
|
||||
->from('/settings/notifications')
|
||||
->post('/settings/notifications', ['notifications_signup_email' => 1])
|
||||
->assertRedirect('/settings/notifications');
|
||||
|
||||
$this->assertEquals(1, Setting::get('notifications_signup_email'));
|
||||
});
|
||||
|
||||
test('notifications settings are unavailable to guests', function () {
|
||||
$this->get('/settings/notifications')->assertRedirect('login');
|
||||
$this->post('/settings/notifications', ['notifications_signup_email' => 1])->assertRedirect('login');
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('enable two factor', function () {
|
||||
Setting::set('2fa.enabled', false);
|
||||
|
||||
$this->assertFalse(Setting::get('2fa.enabled'));
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/settings/auth')
|
||||
->post('/settings/auth/2fa/enable')
|
||||
->assertRedirect('/settings/auth');
|
||||
|
||||
$this->assertTrue(Setting::get('2fa.enabled'));
|
||||
});
|
||||
|
||||
test('disable two factor', function () {
|
||||
Setting::set('2fa.enabled', true);
|
||||
|
||||
$this->assertTrue(Setting::get('2fa.enabled'));
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/settings/auth')
|
||||
->post('/settings/auth/2fa/disable')
|
||||
->assertRedirect('/settings/auth');
|
||||
|
||||
$this->assertFalse(Setting::get('2fa.enabled'));
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
use Facades\Tests\Setup\RoleFactory;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('update general settings', function () {
|
||||
Setting::set('app_name', 'bar');
|
||||
|
||||
$this->assertEquals('bar', Setting::get('app_name'));
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/settings/general')
|
||||
->post('/settings/general', ['app_name' => 'foo'])
|
||||
->assertRedirect('/settings/general');
|
||||
|
||||
$this->assertEquals('foo', Setting::get('app_name'));
|
||||
});
|
||||
|
||||
test('only users with appropriate permission can update general settings', function () {
|
||||
Setting::set('app_name', 'bar');
|
||||
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('settings.general')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$this->actingAs($userA)
|
||||
->from('/settings/general')
|
||||
->post('/settings/general', ['app_name' => 'foo'])
|
||||
->assertStatus(403);
|
||||
|
||||
$this->assertEquals('bar', Setting::get('app_name'));
|
||||
|
||||
$this->actingAs($userB)
|
||||
->from('/settings/general')
|
||||
->post('/settings/general', ['app_name' => 'foo'])
|
||||
->assertRedirect('/settings/general');
|
||||
|
||||
$this->assertEquals('foo', Setting::get('app_name'));
|
||||
});
|
||||
|
||||
test('update auth settings', function () {
|
||||
Setting::set('app_name', 'bar');
|
||||
|
||||
$data = settingsPageAuthData();
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/settings/auth')
|
||||
->post('/settings/auth', $data)
|
||||
->assertRedirect('/settings/auth');
|
||||
|
||||
assertSettingsPageAuthUpdated($data);
|
||||
});
|
||||
|
||||
test('only users with appropriate permission can update auth settings', function () {
|
||||
Setting::set('app_name', 'foo');
|
||||
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('settings.auth')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$data = ['app_name' => 'bar'];
|
||||
|
||||
$this->actingAs($userA)
|
||||
->from('/settings/auth')
|
||||
->post('/settings/auth', $data)
|
||||
->assertStatus(403);
|
||||
|
||||
$this->assertEquals('foo', Setting::get('app_name'));
|
||||
|
||||
$this->actingAs($userB)
|
||||
->from('/settings/auth')
|
||||
->post('/settings/auth', $data)
|
||||
->assertRedirect('/settings/auth');
|
||||
|
||||
$this->assertEquals('bar', Setting::get('app_name'));
|
||||
});
|
||||
|
||||
function assertSettingsPageAuthUpdated(array $data)
|
||||
{
|
||||
test()->assertEquals($data['remember_me'], Setting::get('remember_me'));
|
||||
test()->assertEquals($data['forgot_password'], Setting::get('forgot_password'));
|
||||
test()->assertEquals($data['login_reset_token_lifetime'], Setting::get('login_reset_token_lifetime'));
|
||||
test()->assertEquals($data['throttle_enabled'], Setting::get('throttle_enabled'));
|
||||
test()->assertEquals($data['throttle_attempts'], Setting::get('throttle_attempts'));
|
||||
test()->assertEquals($data['throttle_lockout_time'], Setting::get('throttle_lockout_time'));
|
||||
}
|
||||
|
||||
function settingsPageAuthData(): array
|
||||
{
|
||||
return [
|
||||
'remember_me' => 1,
|
||||
'forgot_password' => 1,
|
||||
'login_reset_token_lifetime' => 123,
|
||||
'throttle_enabled' => 1,
|
||||
'throttle_attempts' => 10,
|
||||
'throttle_lockout_time' => 2,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use App\Support\Enum\UserStatus;
|
||||
use Carbon\Carbon;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Laravel\Socialite\Contracts\Provider as SocialiteProvider;
|
||||
use Laravel\Socialite\Contracts\User as SocialUserContract;
|
||||
use Mockery as m;
|
||||
use Tests\UpdatesSettings;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
uses(UpdatesSettings::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('social login for new user', function () {
|
||||
$this->setSettings(['reg_enabled' => true]);
|
||||
|
||||
$socialUser = new StubSocialUser;
|
||||
|
||||
$driver = m::mock(SocialiteProvider::class);
|
||||
$driver->shouldReceive('user')->andReturn($socialUser);
|
||||
|
||||
Socialite::shouldReceive('driver')->with('foo')->andReturn($driver);
|
||||
|
||||
$this->get('auth/foo/callback');
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'username' => null,
|
||||
'email' => $socialUser->getEmail(),
|
||||
'first_name' => 'John',
|
||||
'last_name' => 'Doe',
|
||||
'status' => UserStatus::ACTIVE,
|
||||
'email_verified_at' => now(),
|
||||
]);
|
||||
|
||||
$user = User::where('email', $socialUser->getEmail())->first();
|
||||
|
||||
$this->assertDatabaseHas('social_logins', [
|
||||
'user_id' => $user->id,
|
||||
'provider' => 'foo',
|
||||
'provider_id' => $socialUser->getId(),
|
||||
'avatar' => $socialUser->getAvatar(),
|
||||
]);
|
||||
|
||||
$this->assertEquals($user->id, Auth::id());
|
||||
});
|
||||
|
||||
test('social login for new user if registration is disabled', function () {
|
||||
$this->setSettings(['reg_enabled' => false]);
|
||||
|
||||
$socialUser = new StubSocialUser;
|
||||
|
||||
$driver = m::mock(SocialiteProvider::class);
|
||||
$driver->shouldReceive('user')->andReturn($socialUser);
|
||||
|
||||
Socialite::shouldReceive('driver')->with('foo')->andReturn($driver);
|
||||
|
||||
$this->get('auth/foo/callback')
|
||||
->assertRedirect('/login');
|
||||
|
||||
$this->assertSessionHasError('Only users who already created an account can log in.');
|
||||
});
|
||||
|
||||
test('social login for banned user', function () {
|
||||
$user = UserFactory::banned()->create();
|
||||
$socialUser = new StubSocialUser;
|
||||
|
||||
$driver = m::mock(SocialiteProvider::class);
|
||||
$driver->shouldReceive('user')->andReturn($socialUser);
|
||||
|
||||
Socialite::shouldReceive('driver')->with('foo')->andReturn($driver);
|
||||
|
||||
DB::table('social_logins')->insert([
|
||||
'user_id' => $user->id,
|
||||
'provider' => 'foo',
|
||||
'provider_id' => $socialUser->getId(),
|
||||
'avatar' => $socialUser->getAvatar(),
|
||||
'created_at' => Carbon::now(),
|
||||
]);
|
||||
|
||||
$this->get('auth/foo/callback')
|
||||
->assertRedirect('/login');
|
||||
|
||||
$this->assertSessionHasError('Your account is banned by administrator.');
|
||||
});
|
||||
|
||||
test('social login for existing user', function () {
|
||||
$user = User::factory()->create();
|
||||
$socialUser = new StubSocialUser;
|
||||
|
||||
$driver = m::mock(SocialiteProvider::class);
|
||||
$driver->shouldReceive('user')->andReturn($socialUser);
|
||||
|
||||
Socialite::shouldReceive('driver')->with('foo')->andReturn($driver);
|
||||
|
||||
DB::table('social_logins')->insert([
|
||||
'user_id' => $user->id,
|
||||
'provider' => 'foo',
|
||||
'provider_id' => $socialUser->getId(),
|
||||
'avatar' => $socialUser->getAvatar(),
|
||||
'created_at' => Carbon::now(),
|
||||
]);
|
||||
|
||||
$this->get('auth/foo/callback')
|
||||
->assertRedirect('/');
|
||||
|
||||
$this->assertEquals($user->id, Auth::id());
|
||||
});
|
||||
|
||||
test('missing email login', function () {
|
||||
$this->setSettings(['reg_enabled' => true]);
|
||||
|
||||
$socialUser = new StubSocialUserWithoutEmail;
|
||||
$driver = m::mock(SocialiteProvider::class);
|
||||
$driver->shouldReceive('user')->andReturn($socialUser);
|
||||
Socialite::shouldReceive('driver')->with('foo')->andReturn($driver);
|
||||
|
||||
$this->get('auth/foo/callback')
|
||||
->assertRedirect('login');
|
||||
|
||||
$this->assertSessionHasError('You have to provide your email address.');
|
||||
});
|
||||
|
||||
test('social login for user with one word name', function () {
|
||||
$this->setSettings(['reg_enabled' => true]);
|
||||
|
||||
$socialUser = new StubSocialUserWithOneWordName;
|
||||
|
||||
$driver = m::mock(SocialiteProvider::class);
|
||||
$driver->shouldReceive('user')->andReturn($socialUser);
|
||||
|
||||
Socialite::shouldReceive('driver')->with('foo')->andReturn($driver);
|
||||
|
||||
$this->get('auth/foo/callback')
|
||||
->assertRedirect('/');
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'username' => null,
|
||||
'email' => $socialUser->getEmail(),
|
||||
'first_name' => 'John',
|
||||
'last_name' => '',
|
||||
'status' => UserStatus::ACTIVE,
|
||||
]);
|
||||
|
||||
$user = User::where('email', $socialUser->getEmail())->first();
|
||||
|
||||
$this->assertDatabaseHas('social_logins', [
|
||||
'user_id' => $user->id,
|
||||
'provider' => 'foo',
|
||||
'provider_id' => $socialUser->getId(),
|
||||
'avatar' => $socialUser->getAvatar(),
|
||||
]);
|
||||
|
||||
$this->assertEquals($user->id, Auth::id());
|
||||
});
|
||||
|
||||
class StubSocialUser implements SocialUserContract
|
||||
{
|
||||
public function getId()
|
||||
{
|
||||
return '123';
|
||||
}
|
||||
|
||||
public function getNickname()
|
||||
{
|
||||
return 'johndoe';
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return 'John Doe';
|
||||
}
|
||||
|
||||
public function getEmail()
|
||||
{
|
||||
return 'john@doe.com';
|
||||
}
|
||||
|
||||
public function getAvatar()
|
||||
{
|
||||
return 'http://www.gravatar.com/avatar';
|
||||
}
|
||||
}
|
||||
|
||||
class StubSocialUserWithoutEmail extends StubSocialUser
|
||||
{
|
||||
public $email = null;
|
||||
|
||||
public function getEmail()
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
}
|
||||
|
||||
class StubSocialUserWithOneWordName extends StubSocialUser
|
||||
{
|
||||
public function getName()
|
||||
{
|
||||
return 'John';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
<?php
|
||||
|
||||
use App\Events\User\TwoFactorDisabled;
|
||||
use App\Events\User\TwoFactorEnabled;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use PragmaRX\Google2FA\Google2FA;
|
||||
use Tests\UpdatesSettings;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
uses(UpdatesSettings::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('the 2fa form is visible on profile page if 2fa is enabled', function () {
|
||||
config(['services.authy.key' => 'test']);
|
||||
|
||||
$this->setSettings(['2fa.enabled' => false]);
|
||||
|
||||
// The tab is rendered client side from the shared setting, so that flag
|
||||
// is what decides whether it appears rather than the markup.
|
||||
$this->assertFalse(twoFactorAuthSetting('profile'));
|
||||
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
$this->assertTrue(twoFactorAuthSetting('profile'));
|
||||
});
|
||||
|
||||
test('the 2fa form is visible on edit user page if 2fa is enabled', function () {
|
||||
config(['services.authy.key' => 'test']);
|
||||
|
||||
$this->setSettings(['2fa.enabled' => false]);
|
||||
|
||||
$user = UserFactory::create();
|
||||
|
||||
$this->assertFalse(twoFactorAuthSetting("/users/{$user->id}/edit"));
|
||||
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
$this->assertTrue(twoFactorAuthSetting("/users/{$user->id}/edit"));
|
||||
});
|
||||
|
||||
/**
|
||||
* The `2fa.enabled` shared prop, which drives the Two-Factor tab.
|
||||
*/
|
||||
function twoFactorAuthSetting(string $url): bool
|
||||
{
|
||||
return (bool) test()->actingAsAdmin()
|
||||
->get($url)
|
||||
->viewData('page')['props']['settings']['2fa.enabled'];
|
||||
}
|
||||
|
||||
test('enable 2fa from profile page', function () {
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
$user = UserFactory::user()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->post('/two-factor/enable')
|
||||
->assertRedirect('/')
|
||||
->assertSessionHas('tab', '2fa');
|
||||
|
||||
$this->assertTrue(
|
||||
DB::table('users')
|
||||
->where('id', $user->id)
|
||||
->whereNotNull('two_factor_secret')
|
||||
->exists()
|
||||
);
|
||||
});
|
||||
|
||||
test('enable 2fa from edit user page', function () {
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
$user = UserFactory::user()->create();
|
||||
|
||||
$formData = ['user' => $user->id];
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->post("users/{$user->id}/two-factor/enable", $formData)
|
||||
->assertRedirect('/')
|
||||
->assertSessionHas('tab', '2fa');
|
||||
|
||||
$this->assertTrue(
|
||||
DB::table('users')
|
||||
->where('id', $user->id)
|
||||
->whereNotNull('two_factor_secret')
|
||||
->exists()
|
||||
);
|
||||
});
|
||||
|
||||
test('users without appropriate permissions cannot enable 2fa for other users', function () {
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
$this->be(UserFactory::user()->create());
|
||||
|
||||
$user = UserFactory::user()->create();
|
||||
|
||||
$this->post('two-factor/enable', [
|
||||
'user' => $user->id,
|
||||
])->assertStatus(403);
|
||||
|
||||
$this->assertTrue(
|
||||
DB::table('users')
|
||||
->where('id', $user->id)
|
||||
->whereNull('two_factor_secret')
|
||||
->exists()
|
||||
);
|
||||
});
|
||||
|
||||
test('code field is required during 2fa verification', function () {
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
$user = UserFactory::user()->create();
|
||||
$user->two_factor_secret = '123123';
|
||||
$user->save();
|
||||
|
||||
$this->actingAs($user)
|
||||
->post('two-factor/verify')
|
||||
->assertSessionHasErrors('code');
|
||||
});
|
||||
|
||||
test('the 2fa verification with wrong token will fail', function () {
|
||||
$google2fa = new Google2FA;
|
||||
$secret = encrypt($google2fa->generateSecretKey());
|
||||
|
||||
$this->withoutExceptionHandling();
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
$user = UserFactory::user()->create();
|
||||
$user->two_factor_secret = $secret;
|
||||
$user->save();
|
||||
|
||||
$this->actingAs($user)
|
||||
->post('two-factor/verify', ['code' => '123123']);
|
||||
|
||||
$this->assertSessionHasError('Invalid 2FA token.');
|
||||
});
|
||||
|
||||
test('successful 2fa code verification', function () {
|
||||
$google2fa = new Google2FA;
|
||||
$secret = encrypt($google2fa->generateSecretKey());
|
||||
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
Event::fake([
|
||||
TwoFactorEnabled::class,
|
||||
]);
|
||||
|
||||
$user = UserFactory::user()->create();
|
||||
$user->two_factor_secret = $secret;
|
||||
$user->save();
|
||||
|
||||
$validCode = $google2fa->getCurrentOtp(decrypt($user->two_factor_secret));
|
||||
|
||||
$this->actingAs($user)
|
||||
->post('two-factor/verify', ['code' => $validCode])
|
||||
->assertRedirect('/profile');
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'id' => $user->id,
|
||||
'two_factor_secret' => $secret,
|
||||
]);
|
||||
|
||||
Event::assertDispatched(TwoFactorEnabled::class);
|
||||
});
|
||||
|
||||
test('user can disable 2fa', function () {
|
||||
$google2fa = new Google2FA;
|
||||
$secret = encrypt($google2fa->generateSecretKey());
|
||||
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
$user = UserFactory::user()->create();
|
||||
$user->two_factor_secret = $secret;
|
||||
$user->two_factor_confirmed_at = Carbon::now();
|
||||
$user->save();
|
||||
|
||||
$this->be($user);
|
||||
|
||||
Event::fake([
|
||||
TwoFactorDisabled::class,
|
||||
]);
|
||||
|
||||
$this->from('/profile')
|
||||
->post('two-factor/disable')
|
||||
->assertRedirect('/profile');
|
||||
|
||||
$this->assertSessionHasSuccess('Two-Factor Authentication disabled successfully.');
|
||||
|
||||
Event::assertDispatched(TwoFactorDisabled::class);
|
||||
});
|
||||
|
||||
test('user can disable 2fa for another user', function () {
|
||||
$google2fa = new Google2FA;
|
||||
$secret = encrypt($google2fa->generateSecretKey());
|
||||
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
Event::fake([
|
||||
TwoFactorDisabled::class,
|
||||
]);
|
||||
|
||||
$this->beAdmin();
|
||||
|
||||
$user = UserFactory::user()->create();
|
||||
$user->two_factor_secret = $secret;
|
||||
$user->two_factor_confirmed_at = Carbon::now();
|
||||
$user->save();
|
||||
|
||||
$this->from("/users/{$user->id}/edit")
|
||||
->post("/users/{$user->id}/two-factor/disable", ['user' => $user->id])
|
||||
->assertRedirect("/users/{$user->id}/edit");
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'id' => $user->id,
|
||||
'two_factor_secret' => null,
|
||||
]);
|
||||
|
||||
$this->assertSessionHasSuccess('Two-Factor Authentication disabled successfully.');
|
||||
Event::assertDispatched(TwoFactorDisabled::class);
|
||||
});
|
||||
|
||||
test('user without appropriate permissions cannot disable 2fa for another user', function () {
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
$this->be(UserFactory::user()->create());
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->post('two-factor/disable', ['user' => $user->id])->assertForbidden();
|
||||
});
|
||||
@@ -0,0 +1,226 @@
|
||||
<?php
|
||||
|
||||
use App\Events\User\ChangedAvatar;
|
||||
use App\Events\User\UpdatedProfileDetails;
|
||||
use App\Models\Role;
|
||||
use App\Support\Enum\UserStatus;
|
||||
use Carbon\Carbon;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->be(UserFactory::create());
|
||||
});
|
||||
|
||||
test('test user can access his profile page', function () {
|
||||
$this->get('/profile')->assertOk();
|
||||
});
|
||||
|
||||
test('test user can update his profile details', function () {
|
||||
Event::fake([
|
||||
UpdatedProfileDetails::class,
|
||||
]);
|
||||
|
||||
$data = getStubDetailsData();
|
||||
|
||||
$this->from('/profile')
|
||||
->put('profile/details', $data)
|
||||
->assertRedirect('/profile');
|
||||
|
||||
$this->assertSessionHasSuccess('Profile updated successfully.');
|
||||
$this->assertDatabaseHas('users', $data + ['id' => auth()->id()]);
|
||||
|
||||
Event::assertDispatched(UpdatedProfileDetails::class);
|
||||
});
|
||||
|
||||
test('test user cannot change his status while updating the profile', function () {
|
||||
$data = getStubDetailsData();
|
||||
|
||||
$this->from('/profile')
|
||||
->put('profile/details', $data + ['status' => UserStatus::BANNED->value])
|
||||
->assertRedirect('/profile');
|
||||
|
||||
$this->assertSessionHasSuccess('Profile updated successfully.');
|
||||
$this->assertDatabaseHas('users', $data + [
|
||||
'id' => auth()->id(),
|
||||
'status' => UserStatus::ACTIVE,
|
||||
]);
|
||||
});
|
||||
|
||||
test('test user cannot change his role while updating the profile', function () {
|
||||
$data = getStubDetailsData();
|
||||
|
||||
$role = Role::factory()->create();
|
||||
|
||||
$this->from('/profile')
|
||||
->put('profile/details', $data + ['role_id' => $role])
|
||||
->assertRedirect('/profile');
|
||||
|
||||
$this->assertSessionHasSuccess('Profile updated successfully.');
|
||||
|
||||
$this->assertNotEquals($role->id, auth()->user()->role_id);
|
||||
});
|
||||
|
||||
test('test update avatar', function () {
|
||||
Event::fake([
|
||||
ChangedAvatar::class,
|
||||
]);
|
||||
|
||||
Storage::fake('public');
|
||||
|
||||
$data = [
|
||||
'avatar' => UploadedFile::fake()->image('photo1.jpg', 300, 300),
|
||||
'points' => [
|
||||
'x1' => 0,
|
||||
'y1' => 0,
|
||||
'x2' => 200,
|
||||
'y2' => 200,
|
||||
],
|
||||
];
|
||||
|
||||
$this->from('profile')
|
||||
->post('/profile/avatar', $data)
|
||||
->assertRedirect('profile');
|
||||
|
||||
$this->assertSessionHasSuccess('Avatar changed successfully.');
|
||||
|
||||
$user = auth()->user()->fresh();
|
||||
|
||||
$this->assertNotNull($user->avatar);
|
||||
Storage::disk('public')->assertExists("upload/users/{$user->avatar}");
|
||||
|
||||
[$width, $height] = getimagesizefromstring(
|
||||
Storage::disk('public')->get("upload/users/{$user->avatar}")
|
||||
);
|
||||
|
||||
$this->assertEquals(160, $width);
|
||||
$this->assertEquals(160, $height);
|
||||
|
||||
Event::assertDispatched(ChangedAvatar::class);
|
||||
});
|
||||
|
||||
test('test update avatar with invalid image file', function () {
|
||||
Storage::fake('public');
|
||||
|
||||
$data = [
|
||||
'avatar' => UploadedFile::fake()->create('foo.txt', 123),
|
||||
'points' => [
|
||||
'x1' => 0,
|
||||
'y1' => 0,
|
||||
'x2' => 200,
|
||||
'y2' => 200,
|
||||
],
|
||||
];
|
||||
|
||||
$this->from('profile')
|
||||
->post('/profile/avatar', $data)
|
||||
->assertRedirect('profile')
|
||||
->assertSessionHasErrors('avatar');
|
||||
|
||||
$user = auth()->user()->fresh();
|
||||
|
||||
Storage::disk('public')->assertMissing("upload/users/{$user->avatar}");
|
||||
$this->assertNull($user->avatar);
|
||||
});
|
||||
|
||||
test('test update avatar external', function () {
|
||||
Event::fake([
|
||||
ChangedAvatar::class,
|
||||
]);
|
||||
|
||||
$data = ['url' => '//www.gravatar.com/avatar'];
|
||||
$this->post(route('profile.update.avatar-external', auth()->id()), $data)
|
||||
->assertRedirect();
|
||||
|
||||
$this->assertSessionHasSuccess('Avatar changed successfully.');
|
||||
|
||||
$this->assertEquals($data['url'], auth()->user()->fresh()->avatar);
|
||||
|
||||
Event::assertDispatched(ChangedAvatar::class);
|
||||
});
|
||||
|
||||
test('test update user login details', function () {
|
||||
$data = [
|
||||
'email' => 'john@doe.com',
|
||||
'username' => 'milos',
|
||||
'password' => 'milos123123',
|
||||
'password_confirmation' => 'milos123123',
|
||||
];
|
||||
|
||||
$this->from('/profile')
|
||||
->put('profile/login-details', $data)
|
||||
->assertRedirect('/profile');
|
||||
|
||||
$this->assertSessionHasSuccess('Login details updated successfully.');
|
||||
|
||||
$user = auth()->user()->fresh();
|
||||
|
||||
$this->assertEquals($data['email'], $user->email);
|
||||
$this->assertEquals($data['username'], $user->username);
|
||||
$this->assertTrue(Hash::check($data['password'], $user->password));
|
||||
});
|
||||
|
||||
test('test password is not changed if omited on update', function () {
|
||||
auth()->user()->update([
|
||||
'email' => 'john@example.com',
|
||||
'password' => '123123',
|
||||
]);
|
||||
|
||||
$data = [
|
||||
'email' => 'test@test.com',
|
||||
'password' => '',
|
||||
'password_confirmation' => '',
|
||||
];
|
||||
|
||||
$this->from('/profile')
|
||||
->put('profile/login-details', $data)
|
||||
->assertRedirect('/profile');
|
||||
|
||||
$user = auth()->user()->fresh();
|
||||
|
||||
$this->assertEquals($data['email'], $user->email);
|
||||
$this->assertTrue(Hash::check('123123', $user->password));
|
||||
});
|
||||
|
||||
test('test user session invalidation', function () {
|
||||
$this->withoutExceptionHandling();
|
||||
|
||||
config(['session.driver' => 'database']);
|
||||
|
||||
$user = UserFactory::withCredentials('foo', 'bar')->create();
|
||||
|
||||
$this->be($user);
|
||||
|
||||
$agent = $this->app['agent'];
|
||||
$device = $agent->device() ?: 'Unknown';
|
||||
$platform = $agent->platform() ?: 'Unknown';
|
||||
|
||||
// Log-in manually to actually create session record in DB
|
||||
$this->post('/login', ['username' => 'foo', 'password' => 'bar']);
|
||||
|
||||
$this->get('/profile/sessions')
|
||||
->assertSee('127.0.0.1')
|
||||
->assertSee($device)
|
||||
->assertSee($platform)
|
||||
->assertSee($agent->browser());
|
||||
|
||||
$sessionId = DB::table('sessions')->where('user_id', $user->id)->first()->id;
|
||||
$this->delete("profile/sessions/{$sessionId}/invalidate");
|
||||
|
||||
$this->assertDatabaseMissing('sessions', ['user_id' => $user->id]);
|
||||
});
|
||||
|
||||
function getStubDetailsData(): array
|
||||
{
|
||||
return [
|
||||
'first_name' => 'foo',
|
||||
'last_name' => 'bar',
|
||||
'birthday' => Carbon::now()->subYears(25)->format('Y-m-d'),
|
||||
'phone' => '12345667',
|
||||
'address' => 'the address',
|
||||
'country_id' => 688, // Serbia,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
config(['session.driver' => 'database']);
|
||||
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('admin can view user sessions', function () {
|
||||
$user = User::factory()->create();
|
||||
$sessionId = insertUserSession($user);
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->get("users/{$user->id}/sessions")
|
||||
->assertOk()
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->component('users/Sessions')
|
||||
->where('adminView', true)
|
||||
->has('sessions', 1)
|
||||
->where('sessions.0.id', $sessionId)
|
||||
);
|
||||
});
|
||||
|
||||
test('admin can invalidate user session', function () {
|
||||
$user = User::factory()->create();
|
||||
$sessionId = insertUserSession($user);
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->delete("users/{$user->id}/sessions/{$sessionId}/invalidate")
|
||||
->assertRedirect(route('user.sessions', $user->id));
|
||||
|
||||
$this->assertDatabaseMissing('sessions', ['id' => $sessionId]);
|
||||
});
|
||||
|
||||
test('non admin cannot view user sessions', function () {
|
||||
$admin = User::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->get("users/{$admin->id}/sessions")
|
||||
->assertStatus(403);
|
||||
});
|
||||
|
||||
test('guest cannot access user sessions', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->get("users/{$user->id}/sessions")->assertRedirect('login');
|
||||
});
|
||||
|
||||
function insertUserSession(User $user): string
|
||||
{
|
||||
$id = Str::random();
|
||||
|
||||
DB::table('sessions')->insert([
|
||||
'id' => $id,
|
||||
'user_id' => $user->id,
|
||||
'ip_address' => fake()->ipv4,
|
||||
'user_agent' => fake()->userAgent,
|
||||
'payload' => 'foo',
|
||||
'last_activity' => Carbon::now()->timestamp,
|
||||
]);
|
||||
|
||||
return $id;
|
||||
}
|
||||
@@ -0,0 +1,644 @@
|
||||
<?php
|
||||
|
||||
use App\Events\User\UpdatedByAdmin;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use App\Support\Enum\UserStatus;
|
||||
use Carbon\Carbon;
|
||||
use Facades\Tests\Setup\RoleFactory;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
use Tests\UpdatesSettings;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
uses(UpdatesSettings::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed', ['--class' => 'RolesSeeder']);
|
||||
$this->artisan('db:seed', ['--class' => 'PermissionsSeeder']);
|
||||
$this->artisan('db:seed', ['--class' => 'CountriesSeeder']);
|
||||
});
|
||||
|
||||
test('guests cannot view the user list page', function () {
|
||||
$this->get('/users')->assertRedirect('/login');
|
||||
});
|
||||
|
||||
test('users without appropriate permission cannot view user list page', function () {
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('users.manage')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$this->actingAs($userA)->get('/users')->assertForbidden();
|
||||
$this->actingAs($userB)->get('/users')->assertOk();
|
||||
});
|
||||
|
||||
test('user collection is being properly passed to the view', function () {
|
||||
$admin = UserFactory::admin()->create();
|
||||
$active = UserFactory::user()->create();
|
||||
$banned = UserFactory::user()->banned()->create();
|
||||
$unconfirmed = UserFactory::user()->unconfirmed()->create();
|
||||
|
||||
$ids = $this->actingAs($admin)->get('users')->viewData('page')['props']['users']['data'];
|
||||
|
||||
$ids = collect($ids)->pluck('id')->all();
|
||||
|
||||
$this->assertCount(4, $ids);
|
||||
$this->assertContains($admin->id, $ids);
|
||||
$this->assertContains($active->id, $ids);
|
||||
$this->assertContains($banned->id, $ids);
|
||||
$this->assertContains($unconfirmed->id, $ids);
|
||||
});
|
||||
|
||||
test('user list is paginated', function () {
|
||||
$this->beAdmin();
|
||||
|
||||
$this->get('users')->assertInertia(function (Assert $page) {
|
||||
$page->component('users/Index')->has('users.data', 1);
|
||||
|
||||
$paginator = $page->toArray()['props']['users'];
|
||||
|
||||
$this->assertArrayHasKey('links', $paginator);
|
||||
$this->assertSame(1, $paginator['current_page']);
|
||||
});
|
||||
});
|
||||
|
||||
test('users can be filtered out by search term', function () {
|
||||
User::factory()->create(['first_name' => 'Milos', 'last_name' => 'Stojanovic']);
|
||||
$user2 = User::factory()->create(['first_name' => 'John', 'last_name' => 'Doe']);
|
||||
$user3 = User::factory()->create(['first_name' => 'Jane', 'last_name' => 'Doe']);
|
||||
|
||||
$ids = $this->actingAsAdmin()->get('users?search=doe')->viewData('page')['props']['users']['data'];
|
||||
|
||||
$ids = collect($ids)->pluck('id')->all();
|
||||
|
||||
$this->assertCount(2, $ids);
|
||||
$this->assertContains($user3->id, $ids);
|
||||
$this->assertContains($user2->id, $ids);
|
||||
});
|
||||
|
||||
test('users can be filtered out by status', function () {
|
||||
User::factory()->times(2)->create();
|
||||
User::factory()->times(3)->create(['status' => UserStatus::UNCONFIRMED]);
|
||||
|
||||
$ids = $this->actingAsAdmin()
|
||||
->get('users?status='.UserStatus::BANNED->value)
|
||||
->viewData('page')['props']['users']['data'];
|
||||
|
||||
$ids = collect($ids)->pluck('id')->all();
|
||||
|
||||
$this->assertCount(0, $ids);
|
||||
});
|
||||
|
||||
test('admin can successfully create new users', function () {
|
||||
$data = validUserParams();
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->post('/users', $data)
|
||||
->assertRedirect('/users');
|
||||
|
||||
$this->assertSessionHasSuccess('User created successfully.');
|
||||
|
||||
$user = User::where('email', $data['email'])->first();
|
||||
|
||||
$this->assertDatabaseHas('users', Arr::except($data, ['password', 'password_confirmation']));
|
||||
$this->assertTrue(Hash::check('123123123', $user->password));
|
||||
});
|
||||
|
||||
test('users without appropriate permissions cannot create new users', function () {
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('users.manage')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$data = validUserParams();
|
||||
|
||||
$this->actingAs($userA)->post('/users', $data)->assertForbidden();
|
||||
$this->assertNUll(User::where('email', $data['email'])->first());
|
||||
|
||||
$this->actingAs($userB)->post('/users', $data)->assertRedirect('/users');
|
||||
$this->assertNotNull(User::where('email', $data['email'])->first());
|
||||
});
|
||||
|
||||
test('email field is required while creating a user', function () {
|
||||
$data = Arr::except(validUserParams(), ['email']);
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/users')
|
||||
->post('/users', $data)
|
||||
->assertRedirect('/users')
|
||||
->assertSessionHasErrors('email');
|
||||
});
|
||||
|
||||
test('email field must be a valid email while creating a user', function () {
|
||||
$data = validUserParams(['email' => 'foo']);
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/users')
|
||||
->post('/users', $data)
|
||||
->assertRedirect('/users')
|
||||
->assertSessionHasErrors('email');
|
||||
});
|
||||
|
||||
test('email must be unique while creating a user', function () {
|
||||
$this->beAdmin();
|
||||
|
||||
UserFactory::user()->email('john@example.com')->create();
|
||||
|
||||
$data = validUserParams(['email' => 'john@example.com']);
|
||||
|
||||
$this->from('/users')
|
||||
->post('/users', $data)
|
||||
->assertRedirect('/users')
|
||||
->assertSessionHasErrors('email');
|
||||
});
|
||||
|
||||
test('username is not required', function () {
|
||||
$this->beAdmin();
|
||||
|
||||
$data = validUserParams();
|
||||
unset($data['username']);
|
||||
|
||||
$this->from('/users')
|
||||
->post('/users', $data)
|
||||
->assertRedirect('/users');
|
||||
|
||||
$this->assertNotNull(User::where('email', $data['email'])->first());
|
||||
});
|
||||
|
||||
test('username must be unique while creating a user', function () {
|
||||
$this->beAdmin();
|
||||
|
||||
User::factory()->create(['username' => 'johndoe']);
|
||||
|
||||
$data = validUserParams(['username' => 'johndoe']);
|
||||
|
||||
$this->from('/users')
|
||||
->post('/users', $data)
|
||||
->assertRedirect('/users')
|
||||
->assertSessionHasErrors('username');
|
||||
});
|
||||
|
||||
test('password is required while creating a user', function () {
|
||||
$this->beAdmin();
|
||||
|
||||
$data = validUserParams();
|
||||
unset($data['password']);
|
||||
|
||||
$this->from('/users')
|
||||
->post('/users', $data)
|
||||
->assertRedirect('/users')
|
||||
->assertSessionHasErrors('password');
|
||||
});
|
||||
|
||||
test('password must be at least 6 characters long while creating a user', function () {
|
||||
$this->beAdmin();
|
||||
|
||||
$data = validUserParams(['password' => '12345']);
|
||||
|
||||
$this->from('/users')
|
||||
->post('/users', $data)
|
||||
->assertRedirect('/users')
|
||||
->assertSessionHasErrors('password');
|
||||
});
|
||||
|
||||
test('password must be confirmed while creating a user', function () {
|
||||
$this->beAdmin();
|
||||
|
||||
$data = validUserParams();
|
||||
unset($data['password_confirmation']);
|
||||
|
||||
$this->from('/users')
|
||||
->post('/users', $data)
|
||||
->assertRedirect('/users')
|
||||
->assertSessionHasErrors('password');
|
||||
});
|
||||
|
||||
test('birthday must be a valid date while creating a user', function () {
|
||||
$this->beAdmin();
|
||||
|
||||
$data = validUserParams(['birthday' => 'foo']);
|
||||
|
||||
$this->from('/users')
|
||||
->post('/users', $data)
|
||||
->assertRedirect('/users')
|
||||
->assertSessionHasErrors('birthday');
|
||||
});
|
||||
|
||||
test('role field is required while creating a user', function () {
|
||||
$this->beAdmin();
|
||||
|
||||
$data = validUserParams();
|
||||
unset($data['role_id']);
|
||||
|
||||
$this->from('/users')
|
||||
->post('/users', $data)
|
||||
->assertRedirect('/users')
|
||||
->assertSessionHasErrors('role_id');
|
||||
});
|
||||
|
||||
test('selected role must exist inside the system while creating a user', function () {
|
||||
$this->beAdmin();
|
||||
|
||||
$data = validUserParams(['role_id' => 123]);
|
||||
|
||||
$this->from('/users')
|
||||
->post('/users', $data)
|
||||
->assertRedirect('/users')
|
||||
->assertSessionHasErrors('role_id');
|
||||
});
|
||||
|
||||
test('country id field is not required while creating a user', function () {
|
||||
$data = validUserParams(['country_id' => 0]);
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->post('/users', $data)
|
||||
->assertRedirect('/users');
|
||||
|
||||
$this->assertSessionHasSuccess('User created successfully.');
|
||||
|
||||
$user = User::where('email', $data['email'])->first();
|
||||
|
||||
$expected = Arr::except($data, ['password', 'password_confirmation']);
|
||||
$expected['country_id'] = null;
|
||||
|
||||
$this->assertDatabaseHas('users', $expected);
|
||||
$this->assertTrue(Hash::check('123123123', $user->password));
|
||||
});
|
||||
|
||||
test('country id must exist inside the system while creating a user', function () {
|
||||
$this->beAdmin();
|
||||
|
||||
$data = validUserParams(['country_id' => 12345]);
|
||||
|
||||
$this->from('/users')
|
||||
->post('/users', $data)
|
||||
->assertRedirect('/users')
|
||||
->assertSessionHasErrors('country_id');
|
||||
});
|
||||
|
||||
test('admin can view users profile', function () {
|
||||
$user = UserFactory::user()->create();
|
||||
|
||||
$this->actingAsAdmin()->get("users/{$user->id}")->assertOk();
|
||||
});
|
||||
|
||||
test('users without appropriate permissions cannot view profile for another user', function () {
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('users.manage')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$this->actingAs($userA)->get("users/{$userB->id}")->assertForbidden();
|
||||
$this->actingAs($userB)->get("users/{$userA->id}")->assertOk();
|
||||
});
|
||||
|
||||
test('update user details', function () {
|
||||
Event::fake([
|
||||
UpdatedByAdmin::class,
|
||||
]);
|
||||
|
||||
$this->beAdmin();
|
||||
|
||||
$user = UserFactory::user()->create();
|
||||
|
||||
$data = validUserParams();
|
||||
|
||||
$this->from("users/{$user->id}/edit")
|
||||
->put("/users/{$user->id}/update/details", $data)
|
||||
->assertRedirect("users/{$user->id}/edit");
|
||||
|
||||
$expected = Arr::except($data, ['password', 'password_confirmation']);
|
||||
|
||||
$this->assertDatabaseHas('users', $expected + ['id' => $user->id]);
|
||||
$this->assertSessionHasSuccess('User updated successfully.');
|
||||
|
||||
Event::assertDispatched(UpdatedByAdmin::class);
|
||||
});
|
||||
|
||||
test('users without appropriate permissions cannot update other users', function () {
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('users.manage')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$data = validUserParams();
|
||||
|
||||
$this->actingAs($userA)
|
||||
->from("users/{$userB->id}/edit")
|
||||
->put("/users/{$userB->id}/update/details", $data)
|
||||
->assertForbidden();
|
||||
|
||||
$this->assertNotEquals($data['email'], $userB->fresh()->email);
|
||||
|
||||
$this->actingAs($userB)
|
||||
->from("users/{$userA->id}/edit")
|
||||
->put("/users/{$userA->id}/update/details", $data)
|
||||
->assertRedirect("users/{$userA->id}/edit");
|
||||
|
||||
$this->assertEquals($data['email'], $userA->fresh()->email);
|
||||
});
|
||||
|
||||
test('banning a user will invalidate all his sessions', function () {
|
||||
config(['session.driver' => 'database']);
|
||||
|
||||
$this->beAdmin();
|
||||
|
||||
$user = UserFactory::user()->create(['remember_token' => Str::random(60)]);
|
||||
|
||||
DB::table('sessions')->insert([
|
||||
'id' => Str::random(40),
|
||||
'user_id' => $user->id,
|
||||
'ip_address' => '127.0.0.1',
|
||||
'user_agent' => 'Foo',
|
||||
'payload' => Str::random(),
|
||||
'last_activity' => Carbon::now()->subMinute()->timestamp,
|
||||
]);
|
||||
|
||||
$data = validUserParams(['status' => UserStatus::BANNED->value]);
|
||||
|
||||
$this->put("/users/{$user->id}/update/details", $data);
|
||||
|
||||
$this->assertDatabaseMissing('sessions', ['user_id' => $user->id]);
|
||||
$this->assertNull($user->fresh()->remember_token);
|
||||
});
|
||||
|
||||
test('admin can update users login details', function () {
|
||||
$this->beAdmin();
|
||||
|
||||
$user = UserFactory::user()->create();
|
||||
|
||||
$data = [
|
||||
'email' => 'john@doe.com',
|
||||
'username' => 'milos',
|
||||
'password' => '123123123',
|
||||
'password_confirmation' => '123123123',
|
||||
];
|
||||
|
||||
$this->put("users/{$user->id}/update/login-details", $data)
|
||||
->assertRedirect("users/{$user->id}/edit");
|
||||
|
||||
$this->assertSessionHasSuccess('Login details updated successfully.');
|
||||
|
||||
$user = $user->fresh();
|
||||
|
||||
$this->assertEquals($data['email'], $user->email);
|
||||
$this->assertEquals($data['username'], $user->username);
|
||||
$this->assertTrue(Hash::check($data['password'], $user->password));
|
||||
});
|
||||
|
||||
test('users without appropriate permissions cannot update other users login details', function () {
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('users.manage')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$data = [
|
||||
'email' => 'john@doe.com',
|
||||
'username' => 'milos',
|
||||
'password' => '123123123',
|
||||
'password_confirmation' => '123123123',
|
||||
];
|
||||
|
||||
$this->actingAs($userA)
|
||||
->from("users/{$userB->id}/edit")
|
||||
->put("users/{$userB->id}/update/login-details", $data)
|
||||
->assertForbidden();
|
||||
|
||||
$this->assertNotEquals($data['email'], $userB->fresh()->email);
|
||||
|
||||
$this->actingAs($userB)
|
||||
->from("users/{$userA->id}/edit")
|
||||
->put("users/{$userA->id}/update/login-details", $data)
|
||||
->assertRedirect("users/{$userA->id}/edit");
|
||||
|
||||
$this->assertEquals($data['email'], $userA->fresh()->email);
|
||||
});
|
||||
|
||||
test('two factor form visibility', function () {
|
||||
config(['services.authy.key' => 'test']);
|
||||
|
||||
$this->setSettings(['2fa.enabled' => false]);
|
||||
|
||||
$user = UserFactory::user()->create();
|
||||
|
||||
// The tab is rendered client side from the shared setting, so that is
|
||||
// what decides whether it appears rather than the markup.
|
||||
$this->assertFalse(usersTwoFactorSetting("users/{$user->id}/edit"));
|
||||
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
$this->assertTrue(usersTwoFactorSetting("users/{$user->id}/edit"));
|
||||
});
|
||||
|
||||
function usersTwoFactorSetting(string $url): bool
|
||||
{
|
||||
return (bool) test()->actingAsAdmin()
|
||||
->get($url)
|
||||
->viewData('page')['props']['settings']['2fa.enabled'];
|
||||
}
|
||||
|
||||
test('admin can update avatar on behalf of a user', function () {
|
||||
Storage::fake('public');
|
||||
|
||||
$data = [
|
||||
'avatar' => UploadedFile::fake()->image('photo1.jpg', 300, 300),
|
||||
'points' => [
|
||||
'x1' => 0,
|
||||
'y1' => 0,
|
||||
'x2' => 200,
|
||||
'y2' => 200,
|
||||
],
|
||||
];
|
||||
|
||||
$user = UserFactory::user()->create();
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from("users/{$user->id}/edit")
|
||||
->post("users/{$user->id}/update/avatar", $data)
|
||||
->assertRedirect("users/{$user->id}/edit");
|
||||
|
||||
$this->assertSessionHasSuccess('Avatar changed successfully.');
|
||||
|
||||
$user->refresh();
|
||||
|
||||
$this->assertNotNull($user->avatar);
|
||||
Storage::disk('public')->assertExists("upload/users/{$user->avatar}");
|
||||
|
||||
[$width, $height] = getimagesizefromstring(
|
||||
Storage::disk('public')->get("upload/users/{$user->avatar}")
|
||||
);
|
||||
|
||||
$this->assertEquals(160, $width);
|
||||
$this->assertEquals(160, $height);
|
||||
});
|
||||
|
||||
test('admin can update avatar on behalf of a user only if valid file is selected', function () {
|
||||
Storage::fake('public');
|
||||
|
||||
$data = [
|
||||
'avatar' => UploadedFile::fake()->create('foo.txt', 123),
|
||||
'points' => [
|
||||
'x1' => 0,
|
||||
'y1' => 0,
|
||||
'x2' => 200,
|
||||
'y2' => 200,
|
||||
],
|
||||
];
|
||||
|
||||
$user = UserFactory::user()->create();
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from("users/{$user->id}/edit")
|
||||
->post("users/{$user->id}/update/avatar", $data)
|
||||
->assertRedirect("users/{$user->id}/edit");
|
||||
|
||||
$this->assertNull($user->fresh()->avatar);
|
||||
});
|
||||
|
||||
test('users without appropriate permissions cannot update avatar for other users', function () {
|
||||
Storage::fake('public');
|
||||
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('users.manage')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$data = [
|
||||
'avatar' => UploadedFile::fake()->image('photo1.jpg', 300, 300),
|
||||
'points' => [
|
||||
'x1' => 0,
|
||||
'y1' => 0,
|
||||
'x2' => 200,
|
||||
'y2' => 200,
|
||||
],
|
||||
];
|
||||
|
||||
$this->actingAs($userA)
|
||||
->from("users/{$userB->id}/edit")
|
||||
->post("users/{$userB->id}/update/avatar", $data)
|
||||
->assertForbidden();
|
||||
|
||||
$this->assertNull($userB->fresh()->avatar);
|
||||
|
||||
$this->actingAs($userB)
|
||||
->from("users/{$userA->id}/edit")
|
||||
->post("users/{$userA->id}/update/avatar", $data)
|
||||
->assertRedirect("users/{$userA->id}/edit");
|
||||
|
||||
$this->assertNotNull($userA->fresh()->avatar);
|
||||
});
|
||||
|
||||
test('session page is not available for non database driver', function () {
|
||||
config(['session.driver' => 'array']);
|
||||
|
||||
$user = UserFactory::admin()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->get('users')
|
||||
->assertDontSee('User Sessions');
|
||||
|
||||
// this page should not be accessible if
|
||||
// database session driver is not being used
|
||||
$this->get("users/{$user->id}/sessions")->assertNotFound();
|
||||
});
|
||||
|
||||
test('invalidate session', function () {
|
||||
config(['session.driver' => 'database']);
|
||||
|
||||
Carbon::setTestNow(Carbon::now());
|
||||
|
||||
$user = UserFactory::admin()->withCredentials('foo', 'bar')->create();
|
||||
|
||||
$agent = $this->app['agent'];
|
||||
$device = $agent->device() ?: 'Unknown';
|
||||
$platform = $agent->platform() ?: 'Unknown';
|
||||
$browser = $agent->browser() ?: 'Unknown';
|
||||
|
||||
// Log-in manually to actually create session record in DB
|
||||
$this->post('/login', ['username' => 'foo', 'password' => 'bar']);
|
||||
|
||||
$sessions = $this->get("users/{$user->id}/sessions")
|
||||
->viewData('page')['props']['sessions'];
|
||||
|
||||
$this->assertCount(1, $sessions);
|
||||
$this->assertSame('127.0.0.1', $sessions[0]['ipAddress']);
|
||||
$this->assertSame($device, $sessions[0]['device']);
|
||||
$this->assertSame($platform, $sessions[0]['platform']);
|
||||
$this->assertSame($browser, $sessions[0]['browser']);
|
||||
|
||||
$sessionId = DB::table('sessions')->where('user_id', $user->id)->first()->id;
|
||||
$this->delete("users/{$user->id}/sessions/{$sessionId}/invalidate");
|
||||
|
||||
$this->assertDatabaseMissing('sessions', ['user_id' => $user->id]);
|
||||
});
|
||||
|
||||
test('admins can delete other users', function () {
|
||||
$user = UserFactory::user()->create();
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->delete(route('users.destroy', $user))
|
||||
->assertRedirect('users');
|
||||
|
||||
$this->assertSessionHasSuccess('User deleted successfully.');
|
||||
|
||||
$this->assertNull($user->fresh());
|
||||
});
|
||||
|
||||
test('users without appropriate permissions cannot delete other users', function () {
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('users.manage')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$this->actingAs($userA)
|
||||
->delete(route('users.destroy', $userB))
|
||||
->assertForbidden();
|
||||
|
||||
$this->assertNotNull($userB->fresh());
|
||||
|
||||
$this->actingAs($userB)
|
||||
->delete(route('users.destroy', $userA))
|
||||
->assertRedirect('/users');
|
||||
|
||||
$this->assertNull($userA->fresh());
|
||||
});
|
||||
|
||||
test('user cannot delete himself', function () {
|
||||
$this->beAdmin();
|
||||
|
||||
$this->delete(route('users.destroy', auth()->id()))
|
||||
->assertRedirect('/users');
|
||||
|
||||
$this->assertNotNull(auth()->user()->fresh());
|
||||
$this->assertSessionHasError('You cannot delete yourself.');
|
||||
});
|
||||
|
||||
function validUserParams(array $overrides = []): array
|
||||
{
|
||||
return array_merge([
|
||||
'role_id' => Role::whereName('User')->first()->id,
|
||||
'status' => UserStatus::ACTIVE->value,
|
||||
'first_name' => 'foo',
|
||||
'last_name' => 'bar',
|
||||
'birthday' => Carbon::now()->subYears(25)->format('Y-m-d'),
|
||||
'phone' => '12345667',
|
||||
'address' => 'the address',
|
||||
'country_id' => 688, // Serbia,
|
||||
'email' => 'john@doe.com',
|
||||
'username' => 'johndoe',
|
||||
'password' => '123123123',
|
||||
'password_confirmation' => '123123123',
|
||||
], $overrides);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\Verified;
|
||||
use Illuminate\Auth\Notifications\VerifyEmail;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
use Tests\UpdatesSettings;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
uses(UpdatesSettings::class);
|
||||
|
||||
test('unverified user sees verification notice', function () {
|
||||
$user = User::factory()->create(['email_verified_at' => null]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get('email/verify')
|
||||
->assertOk()
|
||||
->assertInertia(fn (Assert $page) => $page->component('auth/VerifyEmail'));
|
||||
});
|
||||
|
||||
test('already verified user is redirected from notice page', function () {
|
||||
$user = User::factory()->create(['email_verified_at' => Carbon::now()]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get('email/verify')
|
||||
->assertRedirect('/');
|
||||
});
|
||||
|
||||
test('signed verification link marks email as verified', function () {
|
||||
Event::fake([Verified::class]);
|
||||
|
||||
$user = User::factory()->create(['email_verified_at' => null]);
|
||||
|
||||
$signedUrl = URL::temporarySignedRoute(
|
||||
'verification.verify',
|
||||
Carbon::now()->addMinutes(60),
|
||||
['id' => $user->id, 'hash' => sha1($user->email)]
|
||||
);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get($signedUrl)
|
||||
->assertRedirect('/');
|
||||
|
||||
$this->assertNotNull($user->fresh()->email_verified_at);
|
||||
|
||||
Event::assertDispatched(Verified::class);
|
||||
});
|
||||
|
||||
test('verification link with invalid signature fails', function () {
|
||||
$user = User::factory()->create(['email_verified_at' => null]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('verification.verify', ['id' => $user->id, 'hash' => sha1($user->email)]))
|
||||
->assertStatus(403);
|
||||
|
||||
$this->assertNull($user->fresh()->email_verified_at);
|
||||
});
|
||||
|
||||
test('resend verification email', function () {
|
||||
Notification::fake();
|
||||
|
||||
$user = User::factory()->create(['email_verified_at' => null]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->post('email/resend')
|
||||
->assertRedirect();
|
||||
|
||||
Notification::assertSentTo($user, VerifyEmail::class);
|
||||
});
|
||||
|
||||
test('resend does nothing for already verified user', function () {
|
||||
Notification::fake();
|
||||
|
||||
$user = User::factory()->create(['email_verified_at' => Carbon::now()]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->post('email/resend')
|
||||
->assertRedirect('/');
|
||||
|
||||
Notification::assertNothingSent();
|
||||
});
|
||||
|
||||
test('guest cannot access verification routes', function () {
|
||||
$this->get('email/verify')->assertRedirect('login');
|
||||
$this->post('email/resend')->assertRedirect('login');
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\Feature\ApiTestCase;
|
||||
use Tests\TestCase;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Test Case
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The paths below deliberately do not overlap. ApiTestCase already carries
|
||||
| RefreshDatabase, UpdatesSettings and the seeding every API test depends on,
|
||||
| so those files must not also receive the plain TestCase binding.
|
||||
|
|
||||
| Bind here rather than with uses() inside each file: Pint hoists a uses() call
|
||||
| above the imports, and PHP resolves aliases in source order, so a short class
|
||||
| name in that call stops resolving.
|
||||
|
|
||||
*/
|
||||
|
||||
pest()->extend(TestCase::class)->in('Unit');
|
||||
pest()->extend(TestCase::class)->in('Feature/Web');
|
||||
pest()->extend(TestCase::class)->in('Feature/EnvironmentTemplateTest.php');
|
||||
pest()->extend(ApiTestCase::class)->in('Feature/Api');
|
||||
|
||||
pest()->extend(TestCase::class)
|
||||
->use(RefreshDatabase::class)
|
||||
->group('browser')
|
||||
->in('Browser');
|
||||
|
||||
pest()->extend(TestCase::class)
|
||||
->use(RefreshDatabase::class)
|
||||
->group('visual')
|
||||
->in('Visual');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Browser and visual helpers
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Sign a user in through the login form.
|
||||
*
|
||||
* Browser tests cannot use actingAs(): the browser carries its own session
|
||||
* cookie, which the guard in the test process does not share. The password is
|
||||
* assigned in plain text because User::setPasswordAttribute() hashes it.
|
||||
*
|
||||
* Call this inside the test, never from beforeEach. Pest connects the browser
|
||||
* client as the test starts, so a visit() from beforeEach fails with
|
||||
* "WebSocket client is not connected" unless an earlier test happened to
|
||||
* connect first.
|
||||
*/
|
||||
function browserLogin(?User $user = null, string $password = 'password'): User
|
||||
{
|
||||
$user ??= UserFactory::admin()->create(['password' => $password]);
|
||||
|
||||
visit('/login')
|
||||
->fill('#username', $user->email)
|
||||
->fill('#password', $password)
|
||||
->click('Log In')
|
||||
->assertPathIs('/');
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign the seeded admin in through the login form.
|
||||
*
|
||||
* Visual tests cannot use browserLogin(): that mints a faker user whose random
|
||||
* name lands in the navbar and the users table, which would change every
|
||||
* capture. UserSeeder creates exactly one deterministic user.
|
||||
*/
|
||||
function visualLogin(): User
|
||||
{
|
||||
visit('/login')
|
||||
->fill('#username', 'admin')
|
||||
->fill('#password', 'admin123')
|
||||
->click('Log In')
|
||||
->assertPathIs('/');
|
||||
|
||||
return User::where('username', 'admin')->firstOrFail();
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Setup;
|
||||
|
||||
use App\Models\Permission;
|
||||
use App\Models\Role;
|
||||
|
||||
class RoleFactory
|
||||
{
|
||||
protected $permissions = [];
|
||||
|
||||
protected $removable = false;
|
||||
|
||||
public function withPermissions($permissions)
|
||||
{
|
||||
$this->permissions = func_get_args();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removable()
|
||||
{
|
||||
$this->removable = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function unremovable()
|
||||
{
|
||||
$this->removable = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
$role = Role::factory()->create([
|
||||
'removable' => $this->removable,
|
||||
]);
|
||||
|
||||
foreach ($this->permissions as $name) {
|
||||
$role->attachPermission(Permission::where('name', $name)->first());
|
||||
}
|
||||
|
||||
return $role;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Setup;
|
||||
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use App\Support\Enum\UserStatus;
|
||||
use Facades\Tests\Setup\RoleFactory;
|
||||
|
||||
class UserFactory
|
||||
{
|
||||
protected $params = [];
|
||||
|
||||
protected $role;
|
||||
|
||||
public function withCredentials($username, $password)
|
||||
{
|
||||
$this->params['username'] = $username;
|
||||
$this->params['password'] = $password;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function banned()
|
||||
{
|
||||
$this->params['status'] = UserStatus::BANNED;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function unconfirmed()
|
||||
{
|
||||
$this->params['status'] = UserStatus::UNCONFIRMED;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function unverified()
|
||||
{
|
||||
$this->params['email_verified_at'] = null;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function role(Role $role)
|
||||
{
|
||||
$this->role = $role;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function admin()
|
||||
{
|
||||
return $this->role(Role::where('name', 'Admin')->first());
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->role(Role::where('name', 'User')->first());
|
||||
}
|
||||
|
||||
public function email($email)
|
||||
{
|
||||
$this->params['email'] = $email;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withPermissions($permissions)
|
||||
{
|
||||
$permissions = func_get_args();
|
||||
|
||||
// Spread, because RoleFactory::withPermissions() also reads
|
||||
// func_get_args(). Passing the array whole nested it one level deeper,
|
||||
// and Permission::where('name', $nested) then quietly matched only the
|
||||
// first entry, so a two permission user came out with one.
|
||||
$this->role(
|
||||
RoleFactory::withPermissions(...$permissions)->create()
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function create(array $overrides = [])
|
||||
{
|
||||
if ($this->role) {
|
||||
$this->params['role_id'] = $this->role;
|
||||
}
|
||||
|
||||
return User::factory()->create(array_merge($this->params, $overrides));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
/**
|
||||
* Assert that session has provided error.
|
||||
*/
|
||||
protected function assertSessionHasError($message)
|
||||
{
|
||||
$errors = session()->get('errors')->getBag('default');
|
||||
|
||||
$this->assertEquals($message, $errors->first());
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that session has provided error.
|
||||
*/
|
||||
protected function assertSessionHasSuccess($message)
|
||||
{
|
||||
$this->assertEquals($message, session()->get('success'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that session has the provided flashed error.
|
||||
*
|
||||
* The sibling above reads the error bag, which is where a failed
|
||||
* validation lands. This one reads the flash key the layouts turn into a
|
||||
* toast, which is where back()->with('error', ...) lands.
|
||||
*/
|
||||
protected function assertSessionHasFlashError($message)
|
||||
{
|
||||
$this->assertEquals($message, session()->get('error'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and log in an admin user.
|
||||
*
|
||||
* @return TestCase
|
||||
*/
|
||||
protected function beAdmin()
|
||||
{
|
||||
return $this->actingAsAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and log in an admin user.
|
||||
*
|
||||
* @return TestCase
|
||||
*/
|
||||
protected function actingAsAdmin()
|
||||
{
|
||||
$this->be(UserFactory::admin()->create());
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Filters\UserKeywordSearch;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('matches username', function () {
|
||||
User::factory()->create(['username' => 'alice', 'email' => 'a@a.com']);
|
||||
User::factory()->create(['username' => 'bob', 'email' => 'b@b.com']);
|
||||
|
||||
$results = applyFilter('alic')->get();
|
||||
|
||||
$this->assertCount(1, $results);
|
||||
$this->assertSame('alice', $results->first()->username);
|
||||
});
|
||||
|
||||
test('matches email', function () {
|
||||
User::factory()->create(['email' => 'alice@example.com']);
|
||||
User::factory()->create(['email' => 'bob@example.com']);
|
||||
|
||||
$results = applyFilter('alice@')->get();
|
||||
|
||||
$this->assertCount(1, $results);
|
||||
});
|
||||
|
||||
test('matches first name and last name', function () {
|
||||
User::factory()->create(['first_name' => 'Alice', 'last_name' => 'Cooper']);
|
||||
User::factory()->create(['first_name' => 'Bob', 'last_name' => 'Dylan']);
|
||||
|
||||
$byFirst = applyFilter('Alic')->get();
|
||||
$byLast = applyFilter('Coop')->get();
|
||||
|
||||
$this->assertCount(1, $byFirst);
|
||||
$this->assertCount(1, $byLast);
|
||||
$this->assertSame('Alice', $byFirst->first()->first_name);
|
||||
$this->assertSame('Cooper', $byLast->first()->last_name);
|
||||
});
|
||||
|
||||
test('returns empty when no match', function () {
|
||||
User::factory()->create(['username' => 'alice']);
|
||||
|
||||
$results = applyFilter('xyzabc')->get();
|
||||
|
||||
$this->assertCount(0, $results);
|
||||
});
|
||||
|
||||
function applyFilter(string $search)
|
||||
{
|
||||
$query = User::query();
|
||||
|
||||
(new UserKeywordSearch)($query, $search);
|
||||
|
||||
return $query;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Middleware\CheckIfBanned;
|
||||
use App\Models\User;
|
||||
use App\Support\Enum\UserStatus;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('guest passes through', function () {
|
||||
$request = Request::create('/');
|
||||
|
||||
$response = (new CheckIfBanned)->handle($request, fn () => 'ok');
|
||||
|
||||
$this->assertSame('ok', $response);
|
||||
});
|
||||
|
||||
test('active user passes through', function () {
|
||||
$user = User::factory()->create(['status' => UserStatus::ACTIVE]);
|
||||
|
||||
$request = Request::create('/');
|
||||
$request->setUserResolver(fn () => $user);
|
||||
|
||||
$response = (new CheckIfBanned)->handle($request, fn () => 'ok');
|
||||
|
||||
$this->assertSame('ok', $response);
|
||||
});
|
||||
|
||||
test('banned user is blocked', function () {
|
||||
$user = User::factory()->create(['status' => UserStatus::BANNED]);
|
||||
|
||||
$request = Request::create('/');
|
||||
$request->setUserResolver(fn () => $user);
|
||||
|
||||
$this->expectException(HttpException::class);
|
||||
|
||||
(new CheckIfBanned)->handle($request, fn () => 'ok');
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Middleware\CheckPermissions;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('guest is blocked', function () {
|
||||
$request = Request::create('/');
|
||||
|
||||
$this->expectException(HttpException::class);
|
||||
|
||||
permissionMiddleware()->handle($request, fn () => 'ok', 'users.manage');
|
||||
});
|
||||
|
||||
test('user without permission is blocked', function () {
|
||||
$user = UserFactory::user()->create();
|
||||
$this->be($user);
|
||||
|
||||
$this->expectException(HttpException::class);
|
||||
|
||||
permissionMiddleware()->handle(permissionRequestFor($user), fn () => 'ok', 'users.manage');
|
||||
});
|
||||
|
||||
test('user with permission passes', function () {
|
||||
$user = UserFactory::withPermissions('users.manage')->create();
|
||||
$this->be($user);
|
||||
|
||||
$response = permissionMiddleware()->handle(permissionRequestFor($user), fn () => 'ok', 'users.manage');
|
||||
|
||||
$this->assertSame('ok', $response);
|
||||
});
|
||||
|
||||
test('multiple permissions separated by pipe', function () {
|
||||
$role = Role::factory()->create();
|
||||
$role->attachPermission(Permission::where('name', 'users.manage')->first());
|
||||
$role->attachPermission(Permission::where('name', 'roles.manage')->first());
|
||||
|
||||
$user = User::factory()->create(['role_id' => $role->id]);
|
||||
$this->be($user);
|
||||
|
||||
$response = permissionMiddleware()->handle(permissionRequestFor($user), fn () => 'ok', 'users.manage|roles.manage');
|
||||
|
||||
$this->assertSame('ok', $response);
|
||||
});
|
||||
|
||||
function permissionRequestFor($user): Request
|
||||
{
|
||||
$request = Request::create('/');
|
||||
$request->setUserResolver(fn () => $user);
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
function permissionMiddleware(): CheckPermissions
|
||||
{
|
||||
return new CheckPermissions(auth()->guard());
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Middleware\CheckRole;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('guest is blocked', function () {
|
||||
$this->expectException(HttpException::class);
|
||||
|
||||
roleMiddleware()->handle(Request::create('/'), fn () => 'ok', 'Admin');
|
||||
});
|
||||
|
||||
test('user with matching role passes', function () {
|
||||
$role = Role::factory()->create(['name' => 'Editor']);
|
||||
$user = User::factory()->create(['role_id' => $role->id]);
|
||||
$this->be($user);
|
||||
|
||||
$response = roleMiddleware()->handle(roleRequestFor($user), fn () => 'ok', 'Editor');
|
||||
|
||||
$this->assertSame('ok', $response);
|
||||
});
|
||||
|
||||
test('user with wrong role is blocked', function () {
|
||||
$role = Role::factory()->create(['name' => 'Editor']);
|
||||
$user = User::factory()->create(['role_id' => $role->id]);
|
||||
$this->be($user);
|
||||
|
||||
$this->expectException(HttpException::class);
|
||||
|
||||
roleMiddleware()->handle(roleRequestFor($user), fn () => 'ok', 'Admin');
|
||||
});
|
||||
|
||||
function roleRequestFor($user): Request
|
||||
{
|
||||
$request = Request::create('/');
|
||||
$request->setUserResolver(fn () => $user);
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
function roleMiddleware(): CheckRole
|
||||
{
|
||||
return new CheckRole(auth()->guard());
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Middleware\EnsureUserIsApproved;
|
||||
use App\Models\User;
|
||||
use App\Support\Enum\UserStatus;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('guest passes through', function () {
|
||||
$response = (new EnsureUserIsApproved)->handle(Request::create('/'), fn () => 'ok');
|
||||
|
||||
$this->assertSame('ok', $response);
|
||||
});
|
||||
|
||||
test('active user passes through', function () {
|
||||
$user = User::factory()->create(['status' => UserStatus::ACTIVE]);
|
||||
$request = Request::create('/');
|
||||
$request->setUserResolver(fn () => $user);
|
||||
|
||||
$response = (new EnsureUserIsApproved)->handle($request, fn () => 'ok');
|
||||
|
||||
$this->assertSame('ok', $response);
|
||||
});
|
||||
|
||||
test('waiting user redirects to approval notice for web', function () {
|
||||
Route::get('/approval-notice', fn () => 'notice')->name('approval.notice');
|
||||
|
||||
$user = User::factory()->create(['status' => UserStatus::WAITING_APPROVAL]);
|
||||
$request = Request::create('/');
|
||||
$request->setUserResolver(fn () => $user);
|
||||
|
||||
$response = (new EnsureUserIsApproved)->handle($request, fn () => 'ok');
|
||||
|
||||
$this->assertSame(302, $response->getStatusCode());
|
||||
$this->assertStringContainsString('/approval-notice', $response->headers->get('Location'));
|
||||
});
|
||||
|
||||
test('waiting user aborts for json request', function () {
|
||||
$user = User::factory()->create(['status' => UserStatus::WAITING_APPROVAL]);
|
||||
$request = Request::create('/');
|
||||
$request->headers->set('Accept', 'application/json');
|
||||
$request->setUserResolver(fn () => $user);
|
||||
|
||||
$this->expectException(HttpException::class);
|
||||
|
||||
(new EnsureUserIsApproved)->handle($request, fn () => 'ok');
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Middleware\PasswordResetEnabled;
|
||||
use App\Http\Middleware\RegistrationEnabled;
|
||||
use App\Http\Middleware\TwoFactorEnabled;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Tests\UpdatesSettings;
|
||||
|
||||
uses(UpdatesSettings::class);
|
||||
|
||||
test('registration enabled aborts when disabled', function () {
|
||||
$this->setSettings(['reg_enabled' => false]);
|
||||
|
||||
$this->expectException(NotFoundHttpException::class);
|
||||
|
||||
(new RegistrationEnabled)->handle(Request::create('/'), fn () => 'ok');
|
||||
});
|
||||
|
||||
test('registration enabled passes when enabled', function () {
|
||||
$this->setSettings(['reg_enabled' => true]);
|
||||
|
||||
$this->assertSame('ok', (new RegistrationEnabled)->handle(Request::create('/'), fn () => 'ok'));
|
||||
});
|
||||
|
||||
test('password reset aborts when disabled', function () {
|
||||
$this->setSettings(['forgot_password' => false]);
|
||||
|
||||
$this->expectException(NotFoundHttpException::class);
|
||||
|
||||
(new PasswordResetEnabled)->handle(Request::create('/'), fn () => 'ok');
|
||||
});
|
||||
|
||||
test('password reset passes when enabled', function () {
|
||||
$this->setSettings(['forgot_password' => true]);
|
||||
|
||||
$this->assertSame('ok', (new PasswordResetEnabled)->handle(Request::create('/'), fn () => 'ok'));
|
||||
});
|
||||
|
||||
test('two factor aborts when disabled', function () {
|
||||
$this->setSettings(['2fa.enabled' => false]);
|
||||
|
||||
$this->expectException(NotFoundHttpException::class);
|
||||
|
||||
(new TwoFactorEnabled)->handle(Request::create('/'), fn () => 'ok');
|
||||
});
|
||||
|
||||
test('two factor passes when enabled', function () {
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
$this->assertSame('ok', (new TwoFactorEnabled)->handle(Request::create('/'), fn () => 'ok'));
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Middleware\SocialLogin;
|
||||
|
||||
test('allows configured provider', function () {
|
||||
config(['auth.social.providers' => ['github', 'google']]);
|
||||
|
||||
$response = $this->get('auth/github/login');
|
||||
|
||||
// Either the controller handles the request or another middleware
|
||||
// intervenes — but the SocialLogin middleware itself should not 404.
|
||||
$this->assertNotEquals(404, $response->getStatusCode());
|
||||
});
|
||||
|
||||
test('middleware throws when provider not in config', function () {
|
||||
config(['auth.social.providers' => ['github']]);
|
||||
|
||||
// Build a real route so $request->route()->parameter('provider') resolves.
|
||||
Route::middleware(SocialLogin::class)
|
||||
->get('/test-social/{provider}', fn ($provider) => 'ok');
|
||||
|
||||
$response = $this->get('/test-social/unknown');
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Middleware\UseApiGuard;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
test('middleware switches default guard to sanctum and sets accept header', function () {
|
||||
$request = Request::create('/');
|
||||
|
||||
$response = (new UseApiGuard(app('auth')))->handle($request, fn ($req) => $req);
|
||||
|
||||
$this->assertSame('application/json', $response->headers->get('Accept'));
|
||||
$this->assertSame('sanctum', auth()->getDefaultDriver());
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Resources\CountryResource;
|
||||
use App\Models\Country;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('resource has expected keys', function () {
|
||||
$country = Country::first();
|
||||
|
||||
$array = (new CountryResource($country))->toArray(new Request);
|
||||
|
||||
$this->assertSame([
|
||||
'id', 'name', 'full_name', 'capital', 'citizenship', 'country_code',
|
||||
'currency', 'currency_code', 'currency_sub_unit', 'currency_symbol',
|
||||
'iso_3166_2', 'iso_3166_3', 'region_code', 'sub_region_code',
|
||||
'eea', 'calling_code', 'flag',
|
||||
], array_keys($array));
|
||||
});
|
||||
|
||||
test('flag resolves to url when set', function () {
|
||||
$country = Country::first();
|
||||
$country->flag = 'UK.png';
|
||||
|
||||
$array = (new CountryResource($country))->toArray(new Request);
|
||||
|
||||
$this->assertStringContainsString('flags/UK.png', $array['flag']);
|
||||
});
|
||||
|
||||
test('flag is null when missing', function () {
|
||||
$country = Country::first();
|
||||
$country->flag = null;
|
||||
|
||||
$array = (new CountryResource($country))->toArray(new Request);
|
||||
|
||||
$this->assertNull($array['flag']);
|
||||
});
|
||||
|
||||
test('numeric casts', function () {
|
||||
$country = Country::first();
|
||||
|
||||
$array = (new CountryResource($country))->toArray(new Request);
|
||||
|
||||
$this->assertIsInt($array['id']);
|
||||
$this->assertIsInt($array['country_code']);
|
||||
$this->assertIsInt($array['region_code']);
|
||||
$this->assertIsBool($array['eea']);
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Resources\PermissionResource;
|
||||
use App\Models\Permission;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('resource has expected keys', function () {
|
||||
$permission = Permission::factory()->create();
|
||||
|
||||
$array = (new PermissionResource($permission))->toArray(new Request);
|
||||
|
||||
$this->assertSame([
|
||||
'id', 'name', 'display_name', 'description', 'removable',
|
||||
'updated_at', 'created_at',
|
||||
], array_keys($array));
|
||||
});
|
||||
|
||||
test('id is int removable is bool', function () {
|
||||
$permission = Permission::factory()->create(['removable' => 1]);
|
||||
|
||||
$array = (new PermissionResource($permission))->toArray(new Request);
|
||||
|
||||
$this->assertIsInt($array['id']);
|
||||
$this->assertTrue($array['removable']);
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Resources\RoleResource;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Role;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\Request;
|
||||
use Spatie\QueryBuilder\AllowedInclude;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('resource includes permissions when loaded', function () {
|
||||
$role = Role::factory()->create();
|
||||
$perm = Permission::factory()->create();
|
||||
$role->attachPermission($perm);
|
||||
$role->load('permissions');
|
||||
|
||||
$array = (new RoleResource($role))->toArray(new Request);
|
||||
|
||||
$this->assertNotEmpty($array['permissions']);
|
||||
});
|
||||
|
||||
test('users count is int when present', function () {
|
||||
$role = Role::factory()->create();
|
||||
$role->users_count = '5';
|
||||
|
||||
$array = (new RoleResource($role))->toArray(new Request);
|
||||
|
||||
$this->assertSame(5, $array['users_count']);
|
||||
});
|
||||
|
||||
test('users count is null when not set', function () {
|
||||
$role = Role::factory()->create();
|
||||
|
||||
$array = (new RoleResource($role))->toArray(new Request);
|
||||
|
||||
$this->assertNull($array['users_count']);
|
||||
});
|
||||
|
||||
test('allowed includes returns permissions and users count', function () {
|
||||
$includes = RoleResource::allowedIncludes();
|
||||
|
||||
$this->assertContains('permissions', $includes);
|
||||
$this->assertCount(2, $includes);
|
||||
$this->assertTrue(collect($includes)->contains(fn ($i) => $i instanceof AllowedInclude || $i === 'permissions'));
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Resources\SessionResource;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
test('resource has expected keys', function () {
|
||||
$session = (object) [
|
||||
'id' => 'abc123',
|
||||
'user_id' => 42,
|
||||
'ip_address' => '127.0.0.1',
|
||||
'user_agent' => 'PHPUnit',
|
||||
'browser' => 'Chrome',
|
||||
'platform' => 'macOS',
|
||||
'device' => 'Desktop',
|
||||
'last_activity' => '2026-01-01 00:00:00',
|
||||
];
|
||||
|
||||
$array = (new SessionResource($session))->toArray(new Request);
|
||||
|
||||
$this->assertSame([
|
||||
'id', 'user_id', 'ip_address', 'user_agent',
|
||||
'browser', 'platform', 'device', 'last_activity',
|
||||
], array_keys($array));
|
||||
|
||||
$this->assertSame(42, $array['user_id']);
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Resources\UserResource;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('resource has expected keys', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$array = (new UserResource($user))->toArray(fakeRequest($user));
|
||||
|
||||
$expected = [
|
||||
'id', 'first_name', 'last_name', 'username', 'email', 'phone', 'avatar',
|
||||
'address', 'country_id', 'role_id', 'status', 'birthday', 'last_login',
|
||||
'two_factor_country_code', 'two_factor_phone', 'two_factor_options',
|
||||
'email_verified_at', 'created_at', 'updated_at', 'country',
|
||||
];
|
||||
|
||||
foreach ($expected as $key) {
|
||||
$this->assertArrayHasKey($key, $array, "missing key: $key");
|
||||
}
|
||||
});
|
||||
|
||||
test('role is hidden without permission', function () {
|
||||
$role = Role::factory()->create();
|
||||
$user = User::factory()->create(['role_id' => $role->id]);
|
||||
$user->load('role');
|
||||
|
||||
$serialized = serializeResource($user);
|
||||
|
||||
$this->assertArrayNotHasKey('role', $serialized);
|
||||
});
|
||||
|
||||
test('role is visible with permission', function () {
|
||||
$admin = UserFactory::withPermissions('roles.manage')->create();
|
||||
$admin->load('role');
|
||||
|
||||
$serialized = serializeResource($admin);
|
||||
|
||||
$this->assertArrayHasKey('role', $serialized);
|
||||
});
|
||||
|
||||
test('allowed includes', function () {
|
||||
$this->assertSame(['role', 'country'], UserResource::allowedIncludes());
|
||||
});
|
||||
|
||||
function fakeRequest(User $user): Request
|
||||
{
|
||||
$request = Request::create('/');
|
||||
$request->setUserResolver(fn () => $user);
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize the resource through JSON to filter out MissingValue placeholders.
|
||||
*/
|
||||
function serializeResource(User $user): array
|
||||
{
|
||||
return json_decode(
|
||||
(new UserResource($user))->toResponse(fakeRequest($user))->getContent(),
|
||||
true,
|
||||
)['data'];
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use App\Events\User\Approved;
|
||||
use App\Listeners\Approve\SendApprovedNotification;
|
||||
use App\Mail\UserApproved;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Tests\UpdatesSettings;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
uses(UpdatesSettings::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('mail is sent when approval enabled', function () {
|
||||
$this->setSettings(['approval.enabled' => true]);
|
||||
|
||||
Mail::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
(new SendApprovedNotification)->handle(new Approved($user));
|
||||
|
||||
Mail::assertQueued(UserApproved::class, fn ($mail) => $mail->hasTo($user->email));
|
||||
});
|
||||
|
||||
test('mail is not sent when approval disabled', function () {
|
||||
$this->setSettings(['approval.enabled' => false]);
|
||||
|
||||
Mail::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
(new SendApprovedNotification)->handle(new Approved($user));
|
||||
|
||||
Mail::assertNothingQueued();
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
use App\Listeners\Approve\SendWaitingApprovalNotification;
|
||||
use App\Mail\WaitingApproval;
|
||||
use App\Repositories\User\UserRepository;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Auth\Events\Verified;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Tests\UpdatesSettings;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
uses(UpdatesSettings::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('mail is sent to each admin when approval enabled', function () {
|
||||
$this->setSettings(['approval.enabled' => true]);
|
||||
|
||||
Mail::fake();
|
||||
|
||||
$admin1 = UserFactory::admin()->create();
|
||||
$admin2 = UserFactory::admin()->create();
|
||||
$regular = UserFactory::user()->create();
|
||||
|
||||
$listener = new SendWaitingApprovalNotification(app(UserRepository::class));
|
||||
$listener->handle(new Verified($regular));
|
||||
|
||||
Mail::assertQueued(WaitingApproval::class, fn ($mail) => $mail->hasTo($admin1->email));
|
||||
Mail::assertQueued(WaitingApproval::class, fn ($mail) => $mail->hasTo($admin2->email));
|
||||
Mail::assertNotQueued(WaitingApproval::class, fn ($mail) => $mail->hasTo($regular->email));
|
||||
});
|
||||
|
||||
test('mail is not sent when approval disabled', function () {
|
||||
$this->setSettings(['approval.enabled' => false]);
|
||||
|
||||
Mail::fake();
|
||||
|
||||
UserFactory::admin()->create();
|
||||
$user = UserFactory::user()->create();
|
||||
|
||||
$listener = new SendWaitingApprovalNotification(app(UserRepository::class));
|
||||
$listener->handle(new Verified($user));
|
||||
|
||||
Mail::assertNothingQueued();
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use App\Events\User\LoggedIn;
|
||||
use App\Listeners\Login\UpdateLastLoginTimestamp;
|
||||
use App\Models\User;
|
||||
use App\Repositories\User\UserRepository;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('last login is set to now for authenticated user', function () {
|
||||
Carbon::setTestNow(Carbon::now());
|
||||
|
||||
$user = User::factory()->create(['last_login' => null]);
|
||||
$this->be($user);
|
||||
|
||||
$listener = new UpdateLastLoginTimestamp(app(UserRepository::class), auth()->guard());
|
||||
$listener->handle(new LoggedIn);
|
||||
|
||||
$this->assertEquals(Carbon::now()->timestamp, $user->fresh()->last_login->timestamp);
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
use App\Listeners\Registration\SendSignUpNotification;
|
||||
use App\Mail\UserRegistered;
|
||||
use App\Repositories\User\UserRepository;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Tests\UpdatesSettings;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
uses(UpdatesSettings::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('admins receive notification when signup emails enabled', function () {
|
||||
$this->setSettings(['notifications_signup_email' => true]);
|
||||
|
||||
Mail::fake();
|
||||
|
||||
$admin = UserFactory::admin()->create();
|
||||
$newUser = UserFactory::user()->create();
|
||||
|
||||
$listener = new SendSignUpNotification(app(UserRepository::class));
|
||||
$listener->handle(new Registered($newUser));
|
||||
|
||||
Mail::assertQueued(UserRegistered::class, fn ($mail) => $mail->hasTo($admin->email));
|
||||
});
|
||||
|
||||
test('no mail when signup emails disabled', function () {
|
||||
$this->setSettings(['notifications_signup_email' => false]);
|
||||
|
||||
Mail::fake();
|
||||
|
||||
UserFactory::admin()->create();
|
||||
$newUser = UserFactory::user()->create();
|
||||
|
||||
$listener = new SendSignUpNotification(app(UserRepository::class));
|
||||
$listener->handle(new Registered($newUser));
|
||||
|
||||
Mail::assertNothingQueued();
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use App\Listeners\Users\ActivateUser;
|
||||
use App\Models\User;
|
||||
use App\Repositories\User\UserRepository;
|
||||
use App\Support\Enum\UserStatus;
|
||||
use Illuminate\Auth\Events\Verified;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\UpdatesSettings;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
uses(UpdatesSettings::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('user becomes active when approval disabled', function () {
|
||||
$this->setSettings(['approval.enabled' => false]);
|
||||
|
||||
$user = User::factory()->create(['status' => UserStatus::UNCONFIRMED]);
|
||||
|
||||
$listener = new ActivateUser(app(UserRepository::class));
|
||||
$listener->handle(new Verified($user));
|
||||
|
||||
$this->assertSame(UserStatus::ACTIVE, $user->fresh()->status);
|
||||
});
|
||||
|
||||
test('user waits for approval when approval enabled', function () {
|
||||
$this->setSettings(['approval.enabled' => true]);
|
||||
|
||||
$user = User::factory()->create(['status' => UserStatus::UNCONFIRMED]);
|
||||
|
||||
$listener = new ActivateUser(app(UserRepository::class));
|
||||
$listener->handle(new Verified($user));
|
||||
|
||||
$this->assertSame(UserStatus::WAITING_APPROVAL, $user->fresh()->status);
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use App\Events\User\Banned;
|
||||
use App\Listeners\Users\InvalidateSessions;
|
||||
use App\Models\User;
|
||||
use App\Repositories\Session\SessionRepository;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('all user sessions are invalidated', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
DB::table('sessions')->insert([
|
||||
'id' => Str::random(),
|
||||
'user_id' => $user->id,
|
||||
'ip_address' => fake()->ipv4,
|
||||
'user_agent' => fake()->userAgent,
|
||||
'payload' => 'foo',
|
||||
'last_activity' => Carbon::now()->timestamp,
|
||||
]);
|
||||
|
||||
$listener = new InvalidateSessions(app(SessionRepository::class));
|
||||
$listener->handle(new Banned($user));
|
||||
|
||||
$this->assertDatabaseMissing('sessions', ['user_id' => $user->id]);
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use App\Mail\ResetPassword;
|
||||
|
||||
test('subject includes app name', function () {
|
||||
Setting::set('app_name', 'TestApp');
|
||||
|
||||
$mailable = new ResetPassword('the-token');
|
||||
$mailable->build();
|
||||
|
||||
$mailable->assertHasSubject('[TestApp] '.__('Password Reset Request'));
|
||||
});
|
||||
|
||||
test('uses reset password markdown view', function () {
|
||||
$mailable = new ResetPassword('the-token');
|
||||
|
||||
$mailable->assertSeeInText('reset');
|
||||
});
|
||||
|
||||
test('token is publicly accessible', function () {
|
||||
$mailable = new ResetPassword('the-token');
|
||||
|
||||
$this->assertSame('the-token', $mailable->token);
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
use App\Mail\UserApproved;
|
||||
|
||||
test('subject includes app name', function () {
|
||||
Setting::set('app_name', 'TestApp');
|
||||
|
||||
$mailable = new UserApproved;
|
||||
$mailable->build();
|
||||
|
||||
$mailable->assertHasSubject('[TestApp] '.__('Your Account Has Been Approved'));
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use App\Mail\UserRegistered;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('subject includes app name', function () {
|
||||
Setting::set('app_name', 'TestApp');
|
||||
|
||||
$user = User::factory()->create();
|
||||
$mailable = new UserRegistered($user);
|
||||
$mailable->build();
|
||||
|
||||
$mailable->assertHasSubject('[TestApp] '.__('New User Registration'));
|
||||
});
|
||||
|
||||
test('user is publicly accessible', function () {
|
||||
$user = User::factory()->create();
|
||||
$mailable = new UserRegistered($user);
|
||||
|
||||
$this->assertTrue($user->is($mailable->user));
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use App\Mail\WaitingApproval;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('subject includes app name', function () {
|
||||
Setting::set('app_name', 'TestApp');
|
||||
|
||||
$user = User::factory()->create();
|
||||
$mailable = new WaitingApproval($user);
|
||||
$mailable->build();
|
||||
|
||||
$mailable->assertHasSubject('[TestApp] '.__('User Approval'));
|
||||
});
|
||||
|
||||
test('user is publicly accessible', function () {
|
||||
$user = User::factory()->create();
|
||||
$mailable = new WaitingApproval($user);
|
||||
|
||||
$this->assertTrue($user->is($mailable->user));
|
||||
});
|
||||
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use App\Support\Sidebar\Item;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Vanguard\Plugins\Contracts\Slot;
|
||||
use Vanguard\Plugins\Plugin;
|
||||
use Vanguard\Plugins\Vanguard;
|
||||
use Vanguard\Plugins\Widget;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
afterEach(function () {
|
||||
Vanguard::$slots = [];
|
||||
Vanguard::$widgets = [];
|
||||
});
|
||||
|
||||
test('slot handlers are registered per position', function () {
|
||||
Vanguard::$slots = [];
|
||||
|
||||
$this->assertFalse(Vanguard::hasSlot('navbar:items'));
|
||||
|
||||
Vanguard::slot('navbar:items', FakeSlot::class);
|
||||
|
||||
$this->assertTrue(Vanguard::hasSlot('navbar:items'));
|
||||
$this->assertSame([FakeSlot::class], Vanguard::slotHandlers('navbar:items'));
|
||||
});
|
||||
|
||||
test('multiple handlers may share a position', function () {
|
||||
Vanguard::$slots = [];
|
||||
|
||||
Vanguard::slot('navbar:items', FakeSlot::class);
|
||||
Vanguard::slot('navbar:items', OtherFakeSlot::class);
|
||||
|
||||
$this->assertCount(2, Vanguard::slotHandlers('navbar:items'));
|
||||
});
|
||||
|
||||
test('registering the same handler twice keeps a single entry', function () {
|
||||
Vanguard::$slots = [];
|
||||
|
||||
// Handlers are declared as class names, so a repeat only ever means the
|
||||
// service provider booted again: under Octane, or between tests, where the
|
||||
// static registry outlives the container. Appending would render the slot
|
||||
// once per boot.
|
||||
Vanguard::slot('navbar:items', FakeSlot::class);
|
||||
Vanguard::slot('navbar:items', FakeSlot::class);
|
||||
|
||||
$this->assertSame([FakeSlot::class], Vanguard::slotHandlers('navbar:items'));
|
||||
});
|
||||
|
||||
test('registering the same widget twice keeps a single entry', function () {
|
||||
Vanguard::$widgets = [];
|
||||
|
||||
Vanguard::widgets([new FakeWidget]);
|
||||
Vanguard::widgets([new FakeWidget]);
|
||||
|
||||
$this->assertCount(1, Vanguard::availableWidgets(User::factory()->create()));
|
||||
});
|
||||
|
||||
test('resolving a slot returns component and props', function () {
|
||||
Vanguard::$slots = [];
|
||||
|
||||
Vanguard::slot('user:show', FakeSlot::class);
|
||||
|
||||
$this->assertSame([
|
||||
[
|
||||
'component' => 'fake::Component',
|
||||
'props' => ['greeting' => 'hello world'],
|
||||
],
|
||||
], Vanguard::resolveSlot('user:show', ['greeting' => 'hello world']));
|
||||
});
|
||||
|
||||
test('resolving an unregistered slot returns an empty array', function () {
|
||||
Vanguard::$slots = [];
|
||||
|
||||
$this->assertSame([], Vanguard::resolveSlot('nope:nothing'));
|
||||
});
|
||||
|
||||
test('widget serialises to component props and width', function () {
|
||||
$widget = new FakeWidget;
|
||||
|
||||
$this->assertSame([
|
||||
'component' => 'fake::Widget',
|
||||
'props' => ['count' => 7],
|
||||
'width' => '4',
|
||||
], $widget->toArray());
|
||||
});
|
||||
|
||||
test('widget without permissions authorizes everyone', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->assertTrue((new FakeWidget)->authorize($user));
|
||||
});
|
||||
|
||||
test('widget permissions may be a closure', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$widget = (new FakeWidget)->permissions(fn () => false);
|
||||
|
||||
$this->assertFalse($widget->authorize($user));
|
||||
});
|
||||
|
||||
test('plugin declares no assets by default', function () {
|
||||
$plugin = new FakePlugin($this->app);
|
||||
|
||||
$this->assertSame([], $plugin->assets());
|
||||
});
|
||||
|
||||
test('registry collects assets from plugins that declare them', function () {
|
||||
Vanguard::$plugins = [
|
||||
FakePlugin::class => new FakePlugin($this->app),
|
||||
FakePluginWithAssets::class => new FakePluginWithAssets($this->app),
|
||||
];
|
||||
|
||||
$this->assertSame(
|
||||
[['js' => '/vendor/plugins/fake/plugin.js']],
|
||||
Vanguard::assets()
|
||||
);
|
||||
});
|
||||
|
||||
class FakeSlot implements Slot
|
||||
{
|
||||
public function component(): string
|
||||
{
|
||||
return 'fake::Component';
|
||||
}
|
||||
|
||||
public function data(array $context = []): array
|
||||
{
|
||||
return ['greeting' => $context['greeting'] ?? null];
|
||||
}
|
||||
}
|
||||
|
||||
class OtherFakeSlot extends FakeSlot {}
|
||||
|
||||
class FakeWidget extends Widget
|
||||
{
|
||||
public ?string $width = '4';
|
||||
|
||||
public function component(): string
|
||||
{
|
||||
return 'fake::Widget';
|
||||
}
|
||||
|
||||
public function data(): array
|
||||
{
|
||||
return ['count' => 7];
|
||||
}
|
||||
}
|
||||
|
||||
class FakePlugin extends Plugin
|
||||
{
|
||||
public function sidebar(): ?Item
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class FakePluginWithAssets extends Plugin
|
||||
{
|
||||
public function assets(): array
|
||||
{
|
||||
return ['js' => '/vendor/plugins/fake/plugin.js'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
use App\Presenters\Traits\Presentable;
|
||||
use App\Presenters\UserPresenter;
|
||||
use App\Support\Enum\UserStatus;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->user = new PresentableUser;
|
||||
});
|
||||
|
||||
test('test test name', function () {
|
||||
$this->assertEquals('John Doe', $this->user->present()->name);
|
||||
});
|
||||
|
||||
test('test name or email', function () {
|
||||
$this->assertEquals('John Doe', $this->user->present()->nameOrEmail);
|
||||
|
||||
$this->user->first_name = '';
|
||||
$this->assertEquals('Doe', $this->user->present()->nameOrEmail);
|
||||
|
||||
$this->user->last_name = '';
|
||||
$this->assertEquals('john.doe@test.com', $this->user->present()->nameOrEmail);
|
||||
});
|
||||
|
||||
test('test avatar', function () {
|
||||
$this->assertEquals(url('assets/img/profile.png'), $this->user->present()->avatar);
|
||||
|
||||
$this->user->avatar = '//www.gravatar.com/avatar';
|
||||
$this->assertEquals('//www.gravatar.com/avatar', $this->user->present()->avatar);
|
||||
|
||||
$this->user->avatar = 'http://somewebsite.com/avatar.png';
|
||||
$this->assertEquals('http://somewebsite.com/avatar.png', $this->user->present()->avatar);
|
||||
|
||||
$this->user->avatar = 'foo.png';
|
||||
$this->assertEquals(url('storage/upload/users/foo.png'), $this->user->present()->avatar);
|
||||
});
|
||||
|
||||
test('test birthday', function () {
|
||||
$this->assertEquals('N/A', $this->user->present()->birthday);
|
||||
|
||||
$this->user->birthday = Carbon::now()->subYears(20);
|
||||
$this->assertEquals($this->user->birthday->toDateString(), $this->user->present()->birthday);
|
||||
});
|
||||
|
||||
test('test full address', function () {
|
||||
$this->assertEquals('Some Country', $this->user->present()->fullAddress);
|
||||
|
||||
$this->user->address = 'Street 123';
|
||||
$this->assertEquals('Street 123, Some Country', $this->user->present()->fullAddress);
|
||||
|
||||
$this->user->country_id = null;
|
||||
$this->user->country = null;
|
||||
$this->assertEquals('Street 123', $this->user->present()->fullAddress);
|
||||
|
||||
$this->user->address = '';
|
||||
$this->assertEquals('N/A', $this->user->present()->fullAddress);
|
||||
});
|
||||
|
||||
test('test last login', function () {
|
||||
$this->assertEquals('N/A', $this->user->present()->lastLogin);
|
||||
|
||||
$date = Carbon::now()->subDay(1);
|
||||
$this->user->last_login = $date->copy();
|
||||
$this->assertEquals($date->diffForHumans(), $this->user->present()->lastLogin);
|
||||
});
|
||||
|
||||
test('test label class', function () {
|
||||
$this->assertEquals('success', $this->user->present()->labelClass);
|
||||
|
||||
$this->user->status = UserStatus::BANNED;
|
||||
$this->assertEquals('danger', $this->user->present()->labelClass);
|
||||
|
||||
$this->user->status = UserStatus::UNCONFIRMED;
|
||||
$this->assertEquals('warning', $this->user->present()->labelClass);
|
||||
});
|
||||
|
||||
class PresentableUser extends Model
|
||||
{
|
||||
use Presentable;
|
||||
|
||||
protected $presenter = UserPresenter::class;
|
||||
|
||||
public $first_name = 'John';
|
||||
|
||||
public $last_name = 'Doe';
|
||||
|
||||
public $email = 'john.doe@test.com';
|
||||
|
||||
public $avatar = null;
|
||||
|
||||
public $birthday = null;
|
||||
|
||||
public $country_id = 1;
|
||||
|
||||
public $country;
|
||||
|
||||
public $address = '';
|
||||
|
||||
public $last_login = null;
|
||||
|
||||
public $status = UserStatus::ACTIVE;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->country = new StubCountry;
|
||||
}
|
||||
}
|
||||
|
||||
class StubCountry
|
||||
{
|
||||
public $id = 1;
|
||||
|
||||
public $name = 'Some Country';
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Country;
|
||||
use App\Repositories\Country\EloquentCountry;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->repo = app(EloquentCountry::class);
|
||||
});
|
||||
|
||||
test('lists', function () {
|
||||
$countries = Country::factory()->times(8)->create();
|
||||
$countries = $countries->sortBy(function ($country) {
|
||||
return $country->name;
|
||||
})->pluck('name', 'id');
|
||||
|
||||
$this->assertEquals($countries->toArray(), $this->repo->lists()->toArray());
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
use App\Events\Permission\Created;
|
||||
use App\Events\Permission\Deleted;
|
||||
use App\Events\Permission\Updated;
|
||||
use App\Models\Permission;
|
||||
use App\Repositories\Permission\EloquentPermission;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->repo = app(EloquentPermission::class);
|
||||
});
|
||||
|
||||
test('all', function () {
|
||||
$permissions = Permission::factory()->times(4)->create();
|
||||
|
||||
$this->assertEquals($permissions->toArray(), $this->repo->all()->toArray());
|
||||
});
|
||||
|
||||
test('create permission', function () {
|
||||
Event::fake([
|
||||
Created::class,
|
||||
]);
|
||||
|
||||
$data = getPermissionStubData();
|
||||
|
||||
$perm = $this->repo->create($data);
|
||||
|
||||
$this->assertDatabaseHas('permissions', $data + ['id' => $perm->id]);
|
||||
|
||||
Event::assertDispatched(Created::class);
|
||||
});
|
||||
|
||||
test('update permission', function () {
|
||||
Event::fake([
|
||||
Updated::class,
|
||||
]);
|
||||
|
||||
Cache::put('foo', 'bar');
|
||||
|
||||
$data = getPermissionStubData();
|
||||
|
||||
$perm = Permission::factory()->create();
|
||||
|
||||
$this->repo->update($perm->id, $data);
|
||||
|
||||
$this->assertDatabaseHas('permissions', $data + ['id' => $perm->id])
|
||||
->assertNull(Cache::get('foo'));
|
||||
|
||||
Event::assertDispatched(Updated::class);
|
||||
});
|
||||
|
||||
test('delete permission', function () {
|
||||
Event::fake([
|
||||
Deleted::class,
|
||||
]);
|
||||
|
||||
Cache::put('foo', 'bar');
|
||||
|
||||
$perm = Permission::factory()->create();
|
||||
|
||||
$this->repo->delete($perm->id);
|
||||
|
||||
$this->assertDatabaseMissing('permissions', ['id' => $perm->id])
|
||||
->assertNull(Cache::get('foo'));
|
||||
|
||||
Event::assertDispatched(Deleted::class);
|
||||
});
|
||||
|
||||
function getPermissionStubData(): array
|
||||
{
|
||||
return [
|
||||
'name' => Str::random(5),
|
||||
'display_name' => Str::random(5),
|
||||
'description' => 'foo',
|
||||
'removable' => true,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
use App\Events\Role\Created;
|
||||
use App\Events\Role\Deleted;
|
||||
use App\Events\Role\Updated;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Role;
|
||||
use App\Repositories\Role\EloquentRole;
|
||||
use Facades\Tests\Setup\RoleFactory;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->repo = app(EloquentRole::class);
|
||||
});
|
||||
|
||||
test('all', function () {
|
||||
$roles = Role::factory()->times(4)->create();
|
||||
|
||||
$this->assertEquals(
|
||||
$roles->toArray(),
|
||||
$this->repo->all()->toArray()
|
||||
);
|
||||
});
|
||||
|
||||
test('get all with users count', function () {
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::create();
|
||||
$roleC = RoleFactory::create();
|
||||
|
||||
UserFactory::role($roleA)->create();
|
||||
UserFactory::role($roleA)->create();
|
||||
UserFactory::role($roleB)->create();
|
||||
|
||||
$roleA->users_count = 2;
|
||||
$roleB->users_count = 1;
|
||||
$roleC->users_count = 0;
|
||||
|
||||
$this->assertEquals(
|
||||
[$roleA->toArray(), $roleB->toArray(), $roleC->toArray()],
|
||||
$this->repo->getAllWithUsersCount()->toArray()
|
||||
);
|
||||
});
|
||||
|
||||
test('create', function () {
|
||||
Event::fake([
|
||||
Created::class,
|
||||
]);
|
||||
|
||||
$data = ['name' => 'foo', 'display_name' => 'Foo'];
|
||||
$role = $this->repo->create($data);
|
||||
|
||||
$this->assertDatabaseHas('roles', $data + ['id' => $role->id]);
|
||||
|
||||
Event::assertDispatched(Created::class);
|
||||
});
|
||||
|
||||
test('update', function () {
|
||||
Event::fake([
|
||||
Updated::class,
|
||||
]);
|
||||
|
||||
$role = Role::factory()->create();
|
||||
|
||||
$data = ['name' => 'foo', 'display_name' => 'Foo'];
|
||||
|
||||
$this->repo->update($role->id, $data);
|
||||
|
||||
$this->assertDatabaseHas('roles', $data + ['id' => $role->id]);
|
||||
|
||||
Event::assertDispatched(Updated::class);
|
||||
});
|
||||
|
||||
test('delete role', function () {
|
||||
Event::fake([
|
||||
Deleted::class,
|
||||
]);
|
||||
|
||||
$role = Role::factory()->create();
|
||||
|
||||
$this->repo->delete($role->id);
|
||||
|
||||
$this->assertDatabaseMissing('roles', ['id' => $role->id]);
|
||||
|
||||
Event::assertDispatched(Deleted::class);
|
||||
});
|
||||
|
||||
test('update permissions', function () {
|
||||
$role = Role::factory()->create();
|
||||
$permissions = Permission::factory()->times(2)->create();
|
||||
|
||||
$this->repo->updatePermissions($role->id, $permissions->pluck('id')->toArray());
|
||||
|
||||
$this->assertDatabaseHas('permission_role', ['role_id' => $role->id, 'permission_id' => $permissions[0]->id]);
|
||||
$this->assertDatabaseHas('permission_role', ['role_id' => $role->id, 'permission_id' => $permissions[1]->id]);
|
||||
});
|
||||
|
||||
test('lists', function () {
|
||||
$roles = Role::factory()->times(4)->create();
|
||||
$roles = $roles->pluck('display_name', 'id');
|
||||
|
||||
$this->assertEquals($roles->toArray(), $this->repo->lists()->toArray());
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use App\Repositories\Session\DbSession;
|
||||
use Carbon\Carbon;
|
||||
use Faker\Generator;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->repo = app(DbSession::class);
|
||||
});
|
||||
|
||||
test('get user session', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
Carbon::setTestNow(Carbon::now());
|
||||
|
||||
$data1 = getSessionStubData($user);
|
||||
$data2 = getSessionStubData($user);
|
||||
|
||||
DB::table('sessions')->insert($data1);
|
||||
DB::table('sessions')->insert($data2);
|
||||
|
||||
$expected = collect([
|
||||
(object) addAddMissingFields($data1),
|
||||
(object) addAddMissingFields($data2),
|
||||
]);
|
||||
$expected = $expected->sortBy('id')->keyBy('id')->toArray();
|
||||
|
||||
$actual = $this->repo->getUserSessions($user->id)
|
||||
->sortBy('id')
|
||||
->keyBy('id')
|
||||
->toArray();
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
});
|
||||
|
||||
function addAddMissingFields(array $data)
|
||||
{
|
||||
$agent = app('agent');
|
||||
$agent->setUserAgent($data['user_agent']);
|
||||
|
||||
return array_merge($data, [
|
||||
'browser' => $agent->browser(),
|
||||
'platform' => $agent->platform(),
|
||||
'device' => $agent->device(),
|
||||
'last_activity' => Carbon::createFromTimestamp($data['last_activity']),
|
||||
]);
|
||||
}
|
||||
|
||||
test('if get user sessions will return active sessions only', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
Carbon::setTestNow(Carbon::now());
|
||||
|
||||
$data1 = getSessionStubData($user);
|
||||
$data2 = getSessionStubData($user);
|
||||
$data2['last_activity'] = Carbon::now()->subMinutes(config('session.lifetime') + 1)->timestamp;
|
||||
|
||||
DB::table('sessions')->insert($data1);
|
||||
DB::table('sessions')->insert($data2);
|
||||
|
||||
$expected = collect([
|
||||
(object) addAddMissingFields($data1),
|
||||
]);
|
||||
|
||||
$expected = $expected->sortBy('id')->keyBy('id')->toArray();
|
||||
|
||||
$actual = $this->repo->getUserSessions($user->id)
|
||||
->sortBy('id')
|
||||
->keyBy('id')
|
||||
->toArray();
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
});
|
||||
|
||||
test('invalidate user session', function () {
|
||||
$user = User::factory()->create([
|
||||
'remember_token' => Str::random(60),
|
||||
]);
|
||||
|
||||
$data = getSessionStubData($user);
|
||||
DB::table('sessions')->insert($data);
|
||||
|
||||
$this->repo->invalidateSession($data['id']);
|
||||
|
||||
$this->assertDatabaseMissing('sessions', $data)
|
||||
->assertDatabaseHas('users', ['remember_token' => null]);
|
||||
});
|
||||
|
||||
test('invalidate all sessions for user', function () {
|
||||
$user = User::factory()->create([
|
||||
'remember_token' => Str::random(60),
|
||||
]);
|
||||
|
||||
$data = getSessionStubData($user);
|
||||
DB::table('sessions')->insert($data);
|
||||
|
||||
$this->repo->invalidateAllSessionsForUser($user->id);
|
||||
|
||||
$this->assertDatabaseMissing('sessions', ['user_id' => $user->id])
|
||||
->assertDatabaseHas('users', ['remember_token' => null]);
|
||||
});
|
||||
|
||||
function getSessionStubData($user)
|
||||
{
|
||||
$faker = app(Generator::class);
|
||||
|
||||
return [
|
||||
'id' => Str::random(),
|
||||
'user_id' => $user->id,
|
||||
'ip_address' => $faker->ipv4,
|
||||
'user_agent' => $faker->userAgent,
|
||||
'payload' => 'foo',
|
||||
'last_activity' => Carbon::now()->timestamp,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use App\Repositories\User\EloquentUser;
|
||||
use App\Support\Enum\UserStatus;
|
||||
use Carbon\Carbon;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Testing\Assert;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->repo = app(EloquentUser::class);
|
||||
});
|
||||
|
||||
test('test find', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->assertTrue($user->is($this->repo->findByEmail($user->email)));
|
||||
|
||||
$this->assertNull($this->repo->find(123));
|
||||
});
|
||||
|
||||
test('test find by email', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->assertTrue($user->is($this->repo->findByEmail($user->email)));
|
||||
|
||||
$this->assertNull($this->repo->findByEmail('foo@bar.com'));
|
||||
});
|
||||
|
||||
test('test find by social id', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
DB::table('social_logins')->insert([
|
||||
'user_id' => $user->id,
|
||||
'provider' => 'foo',
|
||||
'provider_id' => '123',
|
||||
'avatar' => '',
|
||||
'created_at' => Carbon::now(),
|
||||
]);
|
||||
|
||||
Assert::assertArraySubset(
|
||||
$user->toArray(),
|
||||
$this->repo->findBySocialId('foo', '123')->toArray()
|
||||
);
|
||||
|
||||
$this->assertNull($this->repo->findBySocialId('bar', '111'));
|
||||
});
|
||||
|
||||
test('test find by session id', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$sessionId = Str::random(40);
|
||||
|
||||
DB::table('sessions')->insert([
|
||||
'id' => $sessionId,
|
||||
'user_id' => $user->id,
|
||||
'ip_address' => '127.0.0.1',
|
||||
'user_agent' => 'foo',
|
||||
'payload' => Str::random(),
|
||||
'last_activity' => Carbon::now(),
|
||||
]);
|
||||
|
||||
Assert::assertArraySubset(
|
||||
$user->toArray(),
|
||||
$this->repo->findBySessionId($sessionId)->toArray()
|
||||
);
|
||||
});
|
||||
|
||||
test('test create', function () {
|
||||
$data = User::factory()->make()->toArray();
|
||||
|
||||
$this->repo->create($data + ['password' => 'foo']);
|
||||
|
||||
$this->assertDatabaseHas('users', $data);
|
||||
});
|
||||
|
||||
test('test associate facebook account to a user', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
Carbon::setTestNow(Carbon::now());
|
||||
|
||||
$socialUser = new Laravel\Socialite\One\User;
|
||||
$socialUser->map([
|
||||
'id' => '123',
|
||||
'avatar' => 'foo',
|
||||
'avatar_original' => 'foo?width=1920',
|
||||
]);
|
||||
|
||||
$this->repo->associateSocialAccountForUser($user->id, 'facebook', $socialUser);
|
||||
|
||||
$this->assertDatabaseHas('social_logins', [
|
||||
'user_id' => $user->id,
|
||||
'provider' => 'facebook',
|
||||
'provider_id' => '123',
|
||||
'avatar' => 'foo?width=150',
|
||||
'created_at' => Carbon::now(),
|
||||
]);
|
||||
|
||||
Carbon::setTestNow(null);
|
||||
});
|
||||
|
||||
test('test associate twitter account to a user', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
Carbon::setTestNow(Carbon::now());
|
||||
|
||||
$socialUser = new Laravel\Socialite\One\User;
|
||||
$socialUser->map([
|
||||
'id' => '123',
|
||||
'avatar' => 'foo_normal.jpg',
|
||||
]);
|
||||
|
||||
$this->repo->associateSocialAccountForUser($user->id, 'twitter', $socialUser);
|
||||
|
||||
$this->assertDatabaseHas('social_logins', [
|
||||
'user_id' => $user->id,
|
||||
'provider' => 'twitter',
|
||||
'provider_id' => '123',
|
||||
'avatar' => 'foo_200x200.jpg',
|
||||
'created_at' => Carbon::now(),
|
||||
]);
|
||||
|
||||
Carbon::setTestNow(null);
|
||||
});
|
||||
|
||||
test('test associate google account to a user', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
Carbon::setTestNow(Carbon::now());
|
||||
|
||||
$socialUser = new Laravel\Socialite\One\User;
|
||||
$socialUser->map([
|
||||
'id' => '123',
|
||||
'avatar' => 'avatar.jpg',
|
||||
'avatar_original' => 'avatar_original.jpg',
|
||||
]);
|
||||
|
||||
$this->repo->associateSocialAccountForUser($user->id, 'google', $socialUser);
|
||||
|
||||
$this->assertDatabaseHas('social_logins', [
|
||||
'user_id' => $user->id,
|
||||
'provider' => 'google',
|
||||
'provider_id' => '123',
|
||||
'avatar' => 'avatar_original.jpg?sz=150',
|
||||
'created_at' => Carbon::now(),
|
||||
]);
|
||||
|
||||
Carbon::setTestNow(null);
|
||||
});
|
||||
|
||||
test('test paginate', function () {
|
||||
$users = User::factory()->times(5)->create();
|
||||
$users = $users->sortByDesc('id')->values();
|
||||
|
||||
$result = $this->repo->paginate(2)->toArray();
|
||||
|
||||
$this->assertEquals(2, count($result['data']));
|
||||
$this->assertEquals(5, $result['total']);
|
||||
Assert::assertArraySubset($users[0]->toArray(), $result['data'][0]);
|
||||
Assert::assertArraySubset($users[1]->toArray(), $result['data'][1]);
|
||||
});
|
||||
|
||||
test('test paginate with status', function () {
|
||||
User::factory()->times(3)->create();
|
||||
User::factory()->create(['status' => UserStatus::BANNED]);
|
||||
|
||||
$active = $this->repo->paginate(2, null, UserStatus::ACTIVE->value)->toArray();
|
||||
$banned = $this->repo->paginate(2, null, UserStatus::BANNED->value)->toArray();
|
||||
|
||||
$this->assertEquals(2, count($active['data']));
|
||||
$this->assertEquals(3, $active['total']);
|
||||
|
||||
$this->assertEquals(1, count($banned['data']));
|
||||
$this->assertEquals(1, $banned['total']);
|
||||
});
|
||||
|
||||
test('test paginate with search', function () {
|
||||
User::factory()->create([
|
||||
'first_name' => 'John',
|
||||
'last_name' => 'Doe',
|
||||
'username' => 'jdoe',
|
||||
'email' => 'joe@test.com',
|
||||
]);
|
||||
|
||||
User::factory()->create([
|
||||
'first_name' => 'Jane',
|
||||
'last_name' => 'Doe',
|
||||
'username' => 'janedoe',
|
||||
'email' => 'jane@doe.com',
|
||||
]);
|
||||
|
||||
User::factory()->create([
|
||||
'first_name' => 'Milos',
|
||||
'last_name' => 'Stojanovic',
|
||||
'email' => 'test@test.com',
|
||||
]);
|
||||
|
||||
$this->assertEquals(2, $this->repo->paginate(25, 'doe')->total());
|
||||
$this->assertEquals(1, $this->repo->paginate(25, 'Milos')->total());
|
||||
$this->assertEquals(2, $this->repo->paginate(25, 'test')->total());
|
||||
$this->assertEquals(2, $this->repo->paginate(25, 'an')->total());
|
||||
});
|
||||
|
||||
test('test update user', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$data = [
|
||||
'first_name' => 'John',
|
||||
'last_name' => 'Doe',
|
||||
'username' => 'foo',
|
||||
'email' => 'test@test.com',
|
||||
];
|
||||
|
||||
$this->repo->update($user->id, $data);
|
||||
|
||||
$this->assertDatabaseHas('users', $data + ['id' => $user->id]);
|
||||
});
|
||||
|
||||
test('test update when provided country is zero', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$data = [
|
||||
'first_name' => 'John',
|
||||
'last_name' => 'Doe',
|
||||
'username' => 'foo',
|
||||
'email' => 'test@test.com',
|
||||
'country_id' => 0,
|
||||
];
|
||||
|
||||
$this->repo->update($user->id, $data);
|
||||
|
||||
$this->assertDatabaseHas('users', array_merge($data, ['id' => $user->id, 'country_id' => null]));
|
||||
});
|
||||
|
||||
test('test delete user', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->repo->delete($user->id);
|
||||
|
||||
$this->assertDatabaseMissing('users', ['id' => $user->id]);
|
||||
});
|
||||
|
||||
test('test count users', function () {
|
||||
User::factory()->times(7)->create();
|
||||
|
||||
$this->assertEquals(7, $this->repo->count());
|
||||
});
|
||||
|
||||
test('test new users count', function () {
|
||||
Carbon::setTestNow(Carbon::now()->startOfMonth()->subMonth());
|
||||
User::factory()->times(3)->create();
|
||||
|
||||
Carbon::setTestNow(null);
|
||||
User::factory()->times(5)->create();
|
||||
|
||||
$this->assertEquals(5, $this->repo->newUsersCount());
|
||||
});
|
||||
|
||||
test('test count by status', function () {
|
||||
User::factory()->times(3)->create();
|
||||
User::factory()->create(['status' => UserStatus::BANNED]);
|
||||
User::factory()->times(2)->create(['status' => UserStatus::UNCONFIRMED]);
|
||||
|
||||
$this->assertEquals(3, $this->repo->countByStatus(UserStatus::ACTIVE));
|
||||
$this->assertEquals(1, $this->repo->countByStatus(UserStatus::BANNED));
|
||||
$this->assertEquals(2, $this->repo->countByStatus(UserStatus::UNCONFIRMED));
|
||||
});
|
||||
|
||||
test('test latest', function () {
|
||||
Carbon::setTestNow(now()->subDay());
|
||||
$user4 = User::factory()->create();
|
||||
|
||||
Carbon::setTestNow(now()->subMinutes(3));
|
||||
$user3 = User::factory()->create();
|
||||
|
||||
Carbon::setTestNow(now()->subMinutes(2));
|
||||
$user2 = User::factory()->create();
|
||||
|
||||
Carbon::setTestNow(now()->subMinutes(1));
|
||||
$user1 = User::factory()->create();
|
||||
|
||||
$latestTwo = $this->repo->latest(2);
|
||||
$latestFour = $this->repo->latest(4);
|
||||
|
||||
$this->assertEquals(2, count($latestTwo));
|
||||
$this->assertEquals(4, count($latestFour));
|
||||
|
||||
Assert::assertArraySubset($user4->toArray(), $latestTwo[0]->toArray());
|
||||
Assert::assertArraySubset($user3->toArray(), $latestTwo[1]->toArray());
|
||||
Assert::assertArraySubset($user1->toArray(), $latestFour[3]->toArray());
|
||||
});
|
||||
|
||||
test('test count of new users per month', function () {
|
||||
$from = now()->startOfYear();
|
||||
|
||||
Carbon::setTestNow($from);
|
||||
User::factory()->times(2)->create();
|
||||
|
||||
Carbon::setTestNow($from->copy()->addMonths(2));
|
||||
User::factory()->times(4)->create();
|
||||
|
||||
Carbon::setTestNow($from->copy()->addMonths(6));
|
||||
User::factory()->times(2)->create();
|
||||
|
||||
Carbon::setTestNow($from->copy()->addMonths(7));
|
||||
User::factory()->times(1)->create();
|
||||
|
||||
Carbon::setTestNow($from->copy()->addMonths(10));
|
||||
User::factory()->times(4)->create();
|
||||
|
||||
Carbon::setTestNow(null);
|
||||
|
||||
$currentYear = now()->year;
|
||||
|
||||
$expected = [
|
||||
"January {$currentYear}" => 2,
|
||||
"February {$currentYear}" => 0,
|
||||
"March {$currentYear}" => 4,
|
||||
"April {$currentYear}" => 0,
|
||||
"May {$currentYear}" => 0,
|
||||
"June {$currentYear}" => 0,
|
||||
"July {$currentYear}" => 2,
|
||||
"August {$currentYear}" => 1,
|
||||
"September {$currentYear}" => 0,
|
||||
"October {$currentYear}" => 0,
|
||||
"November {$currentYear}" => 4,
|
||||
"December {$currentYear}" => 0,
|
||||
];
|
||||
|
||||
$usersPerMonth = $this->repo->countOfNewUsersPerMonthPerRole(
|
||||
now()->startOfYear(),
|
||||
now()->endOfYear()
|
||||
);
|
||||
|
||||
$this->assertEquals($expected, $usersPerMonth);
|
||||
});
|
||||
|
||||
test('test get users with specific role', function () {
|
||||
$this->artisan('db:seed --class=RolesSeeder');
|
||||
|
||||
$admin = UserFactory::admin()->create();
|
||||
$user = UserFactory::user()->create();
|
||||
|
||||
$result = $this->repo->getUsersWithRole('Admin');
|
||||
|
||||
$this->assertEquals(1, $result->count());
|
||||
$this->assertTrue($admin->is($result[0]));
|
||||
|
||||
$result = $this->repo->getUsersWithRole('User');
|
||||
$this->assertEquals(1, $result->count());
|
||||
$this->assertTrue($user->is($result[0]));
|
||||
});
|
||||
|
||||
test('test set role', function () {
|
||||
$this->artisan('db:seed --class=RolesSeeder');
|
||||
|
||||
$user = UserFactory::user()->create();
|
||||
$role = Role::where('name', 'Admin')->first();
|
||||
|
||||
$this->repo->setRole($user->id, $role->id);
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'role_id' => $role->id,
|
||||
'id' => $user->id,
|
||||
]);
|
||||
});
|
||||
|
||||
test('test switch roles for a user', function () {
|
||||
$this->artisan('db:seed --class=RolesSeeder');
|
||||
|
||||
$adminRole = Role::where('name', 'Admin')->first();
|
||||
|
||||
$userA = UserFactory::user()->create();
|
||||
$userB = UserFactory::user()->create();
|
||||
|
||||
$this->repo->switchRolesForUsers($userA->role_id, $adminRole->id);
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'role_id' => $adminRole->id,
|
||||
'id' => $userA->id,
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'role_id' => $adminRole->id,
|
||||
'id' => $userB->id,
|
||||
]);
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
use App\Rules\ValidPermissionName;
|
||||
|
||||
test('passes for valid names', function (string $name) {
|
||||
$rule = new ValidPermissionName;
|
||||
|
||||
$this->assertTrue($rule->passes('name', $name));
|
||||
})->with('validNames');
|
||||
|
||||
test('fails for invalid names', function (mixed $name) {
|
||||
$rule = new ValidPermissionName;
|
||||
|
||||
$this->assertFalse($rule->passes('name', $name));
|
||||
})->with('invalidNames');
|
||||
|
||||
test('validate calls fail with translated message when invalid', function () {
|
||||
$rule = new ValidPermissionName;
|
||||
$messages = [];
|
||||
|
||||
$rule->validate('name', 'has space', function ($msg) use (&$messages) {
|
||||
$messages[] = $msg;
|
||||
});
|
||||
|
||||
$this->assertNotEmpty($messages);
|
||||
});
|
||||
|
||||
test('validate does not call fail when valid', function () {
|
||||
$rule = new ValidPermissionName;
|
||||
$called = false;
|
||||
|
||||
$rule->validate('name', 'users.manage', function () use (&$called) {
|
||||
$called = true;
|
||||
});
|
||||
|
||||
$this->assertFalse($called);
|
||||
});
|
||||
|
||||
test('to string returns regex form', function () {
|
||||
$rule = new ValidPermissionName;
|
||||
|
||||
$this->assertStringStartsWith('regex:', (string) $rule);
|
||||
});
|
||||
|
||||
dataset('validNames', function () {
|
||||
return [
|
||||
'alphanumeric' => ['users123'],
|
||||
'dot-separated' => ['users.manage'],
|
||||
'underscore' => ['users_manage'],
|
||||
'hyphen' => ['users-manage'],
|
||||
'numeric string' => ['12345'],
|
||||
];
|
||||
});
|
||||
|
||||
dataset('invalidNames', function () {
|
||||
return [
|
||||
'space' => ['has space'],
|
||||
'special char' => ['users@manage'],
|
||||
'slash' => ['users/manage'],
|
||||
'array' => [['users.manage']],
|
||||
];
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use App\Repositories\Role\RoleRepository;
|
||||
use App\Services\Auth\Social\SocialManager;
|
||||
use App\Support\Enum\UserStatus;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Laravel\Socialite\Contracts\User as SocialUser;
|
||||
use Tests\UpdatesSettings;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
uses(UpdatesSettings::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('associates existing user when email matches', function () {
|
||||
$existing = User::factory()->create(['email' => 'foo@bar.com']);
|
||||
|
||||
$socialUser = makeSocialUser('foo@bar.com', 'John Doe');
|
||||
|
||||
$manager = app(SocialManager::class);
|
||||
$user = $manager->associate($socialUser, 'facebook');
|
||||
|
||||
$this->assertTrue($existing->is($user));
|
||||
$this->assertDatabaseHas('social_logins', [
|
||||
'user_id' => $user->id,
|
||||
'provider' => 'facebook',
|
||||
]);
|
||||
});
|
||||
|
||||
test('creates new user when email does not exist', function () {
|
||||
$this->setSettings(['approval.enabled' => false]);
|
||||
|
||||
$socialUser = makeSocialUser('new@bar.com', 'Jane Smith');
|
||||
|
||||
$manager = app(SocialManager::class);
|
||||
$user = $manager->associate($socialUser, 'google');
|
||||
|
||||
$this->assertSame('new@bar.com', $user->email);
|
||||
$this->assertSame('Jane', $user->first_name);
|
||||
$this->assertSame('Smith', $user->last_name);
|
||||
$this->assertSame(UserStatus::ACTIVE, $user->status);
|
||||
$this->assertNotNull($user->email_verified_at);
|
||||
$this->assertDatabaseHas('social_logins', [
|
||||
'user_id' => $user->id,
|
||||
'provider' => 'google',
|
||||
]);
|
||||
});
|
||||
|
||||
test('new user status is waiting approval when approval enabled', function () {
|
||||
$this->setSettings(['approval.enabled' => true]);
|
||||
|
||||
$socialUser = makeSocialUser('newer@bar.com', 'Foo Bar');
|
||||
|
||||
$manager = app(SocialManager::class);
|
||||
$user = $manager->associate($socialUser, 'google');
|
||||
|
||||
$this->assertSame(UserStatus::WAITING_APPROVAL, $user->status);
|
||||
});
|
||||
|
||||
test('single name is parsed without last name', function () {
|
||||
$socialUser = makeSocialUser('single@bar.com', 'Cher');
|
||||
|
||||
$manager = app(SocialManager::class);
|
||||
$user = $manager->associate($socialUser, 'facebook');
|
||||
|
||||
$this->assertSame('Cher', $user->first_name);
|
||||
$this->assertSame('', $user->last_name);
|
||||
});
|
||||
|
||||
test('new user gets default user role', function () {
|
||||
$socialUser = makeSocialUser('role@bar.com', 'Role User');
|
||||
|
||||
$manager = app(SocialManager::class);
|
||||
$user = $manager->associate($socialUser, 'facebook');
|
||||
|
||||
$this->assertSame(
|
||||
app(RoleRepository::class)->findByName(Role::DEFAULT_USER_ROLE)->id,
|
||||
$user->role_id,
|
||||
);
|
||||
});
|
||||
|
||||
test('facebook avatar url is resized', function () {
|
||||
$socialUser = makeSocialUser('fb@bar.com', 'FB User');
|
||||
$socialUser->avatar_original = 'https://avatar.fb/me?width=1920';
|
||||
|
||||
$manager = app(SocialManager::class);
|
||||
$user = $manager->associate($socialUser, 'facebook');
|
||||
|
||||
$this->assertSame('https://avatar.fb/me?width=150', $user->avatar);
|
||||
});
|
||||
|
||||
function makeSocialUser(string $email, string $name): SocialUser
|
||||
{
|
||||
$socialUser = Mockery::mock(SocialUser::class);
|
||||
$socialUser->shouldReceive('getId')->andReturn((string) fake()->randomNumber());
|
||||
$socialUser->shouldReceive('getEmail')->andReturn($email);
|
||||
$socialUser->shouldReceive('getName')->andReturn($name);
|
||||
$socialUser->shouldReceive('getAvatar')->andReturn('https://example.com/avatar.jpg');
|
||||
$socialUser->avatar_original = 'https://example.com/avatar.jpg';
|
||||
|
||||
return $socialUser;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
use App\Services\Auth\TwoFactor\Facade;
|
||||
|
||||
test('facade accessor is authy', function () {
|
||||
$method = new ReflectionMethod(Facade::class, 'getFacadeAccessor');
|
||||
$method->setAccessible(true);
|
||||
|
||||
$this->assertSame('authy', $method->invoke(null));
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user