first commit

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